agent-claw: automated task changes

This commit is contained in:
daniel
2026-05-06 18:55:16 -05:00
parent 38905bb1e9
commit 732b00fb66
8494 changed files with 2018127 additions and 4 deletions

View File

@@ -0,0 +1,5 @@
import { Construct } from "constructs";
import * as lambda from "../../../aws-lambda";
export declare class CertificateRequestCertificateRequestFunction extends lambda.Function {
constructor(scope: Construct, id: string, props?: lambda.FunctionOptions);
}

View File

@@ -0,0 +1 @@
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.CertificateRequestCertificateRequestFunction=void 0;const path=require("path"),lambda=require("../../../aws-lambda");class CertificateRequestCertificateRequestFunction extends lambda.Function{constructor(scope,id,props){super(scope,id,{...props,code:lambda.Code.fromAsset(path.join(__dirname,"dns-validated-certificate-handler")),handler:"index.certificateRequestHandler",runtime:lambda.determineLatestNodeRuntime(scope)}),this.node.addMetadata("aws:cdk:is-custom-resource-handler-runtime-family",this.runtime.family)}}exports.CertificateRequestCertificateRequestFunction=CertificateRequestCertificateRequestFunction;

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,13 @@
import { Construct } from "constructs";
import { CustomResourceProviderBase, CustomResourceProviderOptions } from "../../../core";
export declare class CrossRegionStringParamReaderProvider extends CustomResourceProviderBase {
/**
* Returns a stack-level singleton ARN (service token) for the custom resource provider.
*/
static getOrCreate(scope: Construct, uniqueid: string, props?: CustomResourceProviderOptions): string;
/**
* Returns a stack-level singleton for the custom resource provider.
*/
static getOrCreateProvider(scope: Construct, uniqueid: string, props?: CustomResourceProviderOptions): CrossRegionStringParamReaderProvider;
private constructor();
}

View File

@@ -0,0 +1 @@
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.CrossRegionStringParamReaderProvider=void 0;const path=require("path"),core_1=require("../../../core");class CrossRegionStringParamReaderProvider extends core_1.CustomResourceProviderBase{static getOrCreate(scope,uniqueid,props){return this.getOrCreateProvider(scope,uniqueid,props).serviceToken}static getOrCreateProvider(scope,uniqueid,props){const id=`${uniqueid}CustomResourceProvider`,stack=core_1.Stack.of(scope);return stack.node.tryFindChild(id)??new CrossRegionStringParamReaderProvider(stack,id,props)}constructor(scope,id,props){super(scope,id,{...props,codeDirectory:path.join(__dirname,"edge-function"),runtimeName:(0,core_1.determineLatestNodeRuntimeName)(scope)}),this.node.addMetadata("aws:cdk:is-custom-resource-handler-customResourceProvider",!0)}}exports.CrossRegionStringParamReaderProvider=CrossRegionStringParamReaderProvider;

View File

@@ -0,0 +1 @@
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.handler=a;var t=require("@aws-sdk/client-ssm");async function a(n){let e=n.ResourceProperties;if(console.info(`Reading function ARN from SSM parameter ${e.ParameterName} in region ${e.Region}`),n.RequestType==="Create"||n.RequestType==="Update"){let r=await new t.SSM({region:e.Region}).getParameter({Name:e.ParameterName});return console.info("Response: %j",r),{Data:{FunctionArn:r.Parameter?.Value??""}}}}

View File

@@ -0,0 +1 @@
"use strict";var n=Object.defineProperty,d=Object.getOwnPropertyDescriptor,u=Object.getOwnPropertyNames,m=Object.prototype.hasOwnProperty,b=(e,s)=>{for(var t in s)n(e,t,{get:s[t],enumerable:!0})},T=(e,s,t,o)=>{if(s&&typeof s=="object"||typeof s=="function")for(let a of u(s))!m.call(e,a)&&a!==t&&n(e,a,{get:()=>s[a],enumerable:!(o=d(s,a))||o.enumerable});return e},g=e=>T(n({},"__esModule",{value:!0}),e),f={};b(f,{isCompleteHandler:()=>y,onEventHandler:()=>C}),module.exports=g(f);var c=require("@aws-sdk/client-dynamodb");async function C(e){console.log("Event: %j",{...e,ResponseURL:"..."});let s=new c.DynamoDB({}),t=e.ResourceProperties.TableName,o=e.ResourceProperties.Region,a=e.ResourceProperties.SkipReplicaDeletion==="true",i;if(e.RequestType==="Create"||e.RequestType==="Delete")i=e.RequestType;else{let l=await s.describeTable({TableName:t});console.log("Describe table: %j",l),i=l.Table?.Replicas?.some(p=>p.RegionName===o)?void 0:"Create"}if(i)if(i==="Delete"&&a)console.log("Skipping deleting replica table as replica table is set to retain.");else{let l=await s.updateTable({TableName:t,ReplicaUpdates:[{[i]:{RegionName:o}}]});console.log("Update table: %j",l)}else console.log("Skipping updating Table, as a replica in '%s' already exists",o);return e.RequestType==="Create"||e.RequestType==="Update"?{PhysicalResourceId:`${t}-${o}`}:{}}async function y(e){console.log("Event: %j",{...e,ResponseURL:"..."});let t=await new c.DynamoDB({}).describeTable({TableName:e.ResourceProperties.TableName});console.log("Describe table: %j",t);let o=t.Table?.TableStatus==="ACTIVE",i=(t.Table?.Replicas??[]).find(R=>R.RegionName===e.ResourceProperties.Region),l=i?.ReplicaStatus==="ACTIVE",r=e.ResourceProperties.SkipReplicationCompletedWait==="true",p=e.ResourceProperties.SkipReplicaDeletion==="true";switch(e.RequestType){case"Create":case"Update":return{IsComplete:o&&(l||r)};case"Delete":return p?(console.log("Skipping replica deletion check since replica is set to retain."),{IsComplete:!0}):{IsComplete:o&&i===void 0}}}

View File

@@ -0,0 +1,8 @@
import { Construct } from "constructs";
import * as lambda from "../../../aws-lambda";
export declare class ReplicaOnEventFunction extends lambda.Function {
constructor(scope: Construct, id: string, props?: lambda.FunctionOptions);
}
export declare class ReplicaIsCompleteFunction extends lambda.Function {
constructor(scope: Construct, id: string, props?: lambda.FunctionOptions);
}

View File

@@ -0,0 +1 @@
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.ReplicaIsCompleteFunction=exports.ReplicaOnEventFunction=void 0;const path=require("path"),lambda=require("../../../aws-lambda");class ReplicaOnEventFunction extends lambda.Function{constructor(scope,id,props){super(scope,id,{...props,code:lambda.Code.fromAsset(path.join(__dirname,"replica-handler")),handler:"index.onEventHandler",runtime:lambda.determineLatestNodeRuntime(scope)}),this.node.addMetadata("aws:cdk:is-custom-resource-handler-runtime-family",this.runtime.family)}}exports.ReplicaOnEventFunction=ReplicaOnEventFunction;class ReplicaIsCompleteFunction extends lambda.Function{constructor(scope,id,props){super(scope,id,{...props,code:lambda.Code.fromAsset(path.join(__dirname,"replica-handler")),handler:"index.isCompleteHandler",runtime:lambda.determineLatestNodeRuntime(scope)}),this.node.addMetadata("aws:cdk:is-custom-resource-handler-runtime-family",this.runtime.family)}}exports.ReplicaIsCompleteFunction=ReplicaIsCompleteFunction;

View File

@@ -0,0 +1 @@
"use strict";var I=Object.create,t=Object.defineProperty,y=Object.getOwnPropertyDescriptor,P=Object.getOwnPropertyNames,g=Object.getPrototypeOf,l=Object.prototype.hasOwnProperty,G=(r,e)=>{for(var o in e)t(r,o,{get:e[o],enumerable:!0})},n=(r,e,o,i)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of P(e))!l.call(r,s)&&s!==o&&t(r,s,{get:()=>e[s],enumerable:!(i=y(e,s))||i.enumerable});return r},R=(r,e,o)=>(o=r!=null?I(g(r)):{},n(e||!r||!r.__esModule?t(o,"default",{value:r,enumerable:!0}):o,r)),S=r=>n(t({},"__esModule",{value:!0}),r),k={};G(k,{handler:()=>f}),module.exports=S(k);var a=R(require("@aws-sdk/client-ec2")),u=new a.EC2({});function c(r,e){return{GroupId:r,IpPermissions:[{UserIdGroupPairs:[{GroupId:r,UserId:e}],IpProtocol:"-1"}]}}function d(r){return{GroupId:r,IpPermissions:[{IpRanges:[{CidrIp:"0.0.0.0/0"}],IpProtocol:"-1"}]}}async function f(r){let e=r.ResourceProperties.DefaultSecurityGroupId,o=r.ResourceProperties.Account;switch(r.RequestType){case"Create":return p(e,o);case"Update":return h(r);case"Delete":return m(e,o)}}async function h(r){let e=r.OldResourceProperties.DefaultSecurityGroupId,o=r.ResourceProperties.DefaultSecurityGroupId;e!==o&&(await m(e,r.ResourceProperties.Account),await p(o,r.ResourceProperties.Account))}async function p(r,e){try{await u.revokeSecurityGroupEgress(d(r))}catch(o){if(o.name!=="InvalidPermission.NotFound")throw o}try{await u.revokeSecurityGroupIngress(c(r,e))}catch(o){if(o.name!=="InvalidPermission.NotFound")throw o}}async function m(r,e){await u.authorizeSecurityGroupIngress(c(r,e)),await u.authorizeSecurityGroupEgress(d(r))}

View File

@@ -0,0 +1,13 @@
import { Construct } from "constructs";
import { CustomResourceProviderBase, CustomResourceProviderOptions } from "../../../core";
export declare class RestrictDefaultSgProvider extends CustomResourceProviderBase {
/**
* Returns a stack-level singleton ARN (service token) for the custom resource provider.
*/
static getOrCreate(scope: Construct, uniqueid: string, props?: CustomResourceProviderOptions): string;
/**
* Returns a stack-level singleton for the custom resource provider.
*/
static getOrCreateProvider(scope: Construct, uniqueid: string, props?: CustomResourceProviderOptions): RestrictDefaultSgProvider;
private constructor();
}

View File

@@ -0,0 +1 @@
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.RestrictDefaultSgProvider=void 0;const path=require("path"),core_1=require("../../../core");class RestrictDefaultSgProvider extends core_1.CustomResourceProviderBase{static getOrCreate(scope,uniqueid,props){return this.getOrCreateProvider(scope,uniqueid,props).serviceToken}static getOrCreateProvider(scope,uniqueid,props){const id=`${uniqueid}CustomResourceProvider`,stack=core_1.Stack.of(scope);return stack.node.tryFindChild(id)??new RestrictDefaultSgProvider(stack,id,props)}constructor(scope,id,props){super(scope,id,{...props,codeDirectory:path.join(__dirname,"restrict-default-security-group-handler"),runtimeName:(0,core_1.determineLatestNodeRuntimeName)(scope)}),this.node.addMetadata("aws:cdk:is-custom-resource-handler-customResourceProvider",!0)}}exports.RestrictDefaultSgProvider=RestrictDefaultSgProvider;

View File

@@ -0,0 +1,2 @@
"use strict";var C=Object.create,c=Object.defineProperty,w=Object.getOwnPropertyDescriptor,S=Object.getOwnPropertyNames,A=Object.getPrototypeOf,P=Object.prototype.hasOwnProperty,L=(e,t)=>{for(var o in t)c(e,o,{get:t[o],enumerable:!0})},d=(e,t,o,s)=>{if(t&&typeof t=="object"||typeof t=="function")for(let r of S(t))!P.call(e,r)&&r!==o&&c(e,r,{get:()=>t[r],enumerable:!(s=w(t,r))||s.enumerable});return e},l=(e,t,o)=>(o=e!=null?C(A(e)):{},d(t||!e||!e.__esModule?c(o,"default",{value:e,enumerable:!0}):o,e)),T=e=>d(c({},"__esModule",{value:!0}),e),W={};L(W,{autoDeleteHandler:()=>I,handler:()=>k}),module.exports=T(W);var h=require("@aws-sdk/client-ecr"),m=l(require("https")),R=l(require("url")),n={sendHttpRequest:x,log:N,includeStackTraces:!0,userHandlerIndex:"./index"},p="AWSCDK::CustomResourceProviderFramework::CREATE_FAILED",D="AWSCDK::CustomResourceProviderFramework::MISSING_PHYSICAL_ID";function y(e){return async(t,o)=>{let s={...t,ResponseURL:"..."};if(n.log(JSON.stringify(s,void 0,2)),t.RequestType==="Delete"&&t.PhysicalResourceId===p){n.log("ignoring DELETE event caused by a failed CREATE event"),await u("SUCCESS",t);return}try{let r=await e(s,o),a=b(t,r);await u("SUCCESS",a)}catch(r){let a={...t,Reason:n.includeStackTraces?r.stack:r.message};a.PhysicalResourceId||(t.RequestType==="Create"?(n.log("CREATE failed, responding with a marker physical resource id so that the subsequent DELETE will be ignored"),a.PhysicalResourceId=p):n.log(`ERROR: Malformed event. "PhysicalResourceId" is required: ${JSON.stringify(t)}`)),await u("FAILED",a)}}}function b(e,t={}){let o=t.PhysicalResourceId??e.PhysicalResourceId??e.RequestId;if(e.RequestType==="Delete"&&o!==e.PhysicalResourceId)throw new Error(`DELETE: cannot change the physical resource ID from "${e.PhysicalResourceId}" to "${t.PhysicalResourceId}" during deletion`);return{...e,...t,PhysicalResourceId:o}}async function u(e,t){let o={Status:e,Reason:t.Reason??e,StackId:t.StackId,RequestId:t.RequestId,PhysicalResourceId:t.PhysicalResourceId||D,LogicalResourceId:t.LogicalResourceId,NoEcho:t.NoEcho,Data:t.Data},s=R.parse(t.ResponseURL),r=`${s.protocol}//${s.hostname}/${s.pathname}?***`;n.log("submit response to cloudformation",r,o);let a=JSON.stringify(o),E={hostname:s.hostname,path:s.path,method:"PUT",headers:{"content-type":"","content-length":Buffer.byteLength(a,"utf8")}};await F({attempts:5,sleep:1e3},n.sendHttpRequest)(E,a)}async function x(e,t){return new Promise((o,s)=>{try{let r=m.request(e,a=>{a.resume(),!a.statusCode||a.statusCode>=400?s(new Error(`Unsuccessful HTTP response: ${a.statusCode}`)):o()});r.on("error",s),r.write(t),r.end()}catch(r){s(r)}})}function N(e,...t){console.log(e,...t)}function F(e,t){return async(...o)=>{let s=e.attempts,r=e.sleep;for(;;)try{return await t(...o)}catch(a){if(s--<=0)throw a;await H(Math.floor(Math.random()*r)),r*=2}}}async function H(e){return new Promise(t=>setTimeout(t,e))}var g="aws-cdk:auto-delete-images",i=new h.ECR({}),k=y(I);async function I(e){switch(e.RequestType){case"Create":break;case"Update":return{PhysicalResourceId:(await q(e)).PhysicalResourceId};case"Delete":return U(e.ResourceProperties?.RepositoryName)}}async function q(e){let t=e,o=t.OldResourceProperties?.RepositoryName;return{PhysicalResourceId:t.ResourceProperties?.RepositoryName??o}}async function f(e){let t=await i.listImages(e),o=[],s=[];(t.imageIds??[]).forEach(a=>{"imageTag"in a?s.push(a):o.push(a)});let r=t.nextToken??null;o.length===0&&s.length===0||(s.length!==0&&await i.batchDeleteImage({repositoryName:e.repositoryName,imageIds:s}),o.length!==0&&await i.batchDeleteImage({repositoryName:e.repositoryName,imageIds:o}),r&&await f({...e,nextToken:r}))}async function U(e){if(!e)throw new Error("No RepositoryName was provided.");let o=(await i.describeRepositories({repositoryNames:[e]})).repositories?.find(s=>s.repositoryName===e);if(!await _(o?.repositoryArn)){process.stdout.write(`Repository does not have '${g}' tag, skipping cleaning.
`);return}try{await f({repositoryName:e})}catch(s){if(s.name!=="RepositoryNotFoundException")throw s}}async function _(e){return(await i.listTagsForResource({resourceArn:e})).tags?.some(o=>o.Key===g&&o.Value==="true")}

View File

@@ -0,0 +1,13 @@
import { Construct } from "constructs";
import { CustomResourceProviderBase, CustomResourceProviderOptions } from "../../../core";
export declare class AutoDeleteImagesProvider extends CustomResourceProviderBase {
/**
* Returns a stack-level singleton ARN (service token) for the custom resource provider.
*/
static getOrCreate(scope: Construct, uniqueid: string, props?: CustomResourceProviderOptions): string;
/**
* Returns a stack-level singleton for the custom resource provider.
*/
static getOrCreateProvider(scope: Construct, uniqueid: string, props?: CustomResourceProviderOptions): AutoDeleteImagesProvider;
private constructor();
}

View File

@@ -0,0 +1 @@
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.AutoDeleteImagesProvider=void 0;const path=require("path"),core_1=require("../../../core");class AutoDeleteImagesProvider extends core_1.CustomResourceProviderBase{static getOrCreate(scope,uniqueid,props){return this.getOrCreateProvider(scope,uniqueid,props).serviceToken}static getOrCreateProvider(scope,uniqueid,props){const id=`${uniqueid}CustomResourceProvider`,stack=core_1.Stack.of(scope);return stack.node.tryFindChild(id)??new AutoDeleteImagesProvider(stack,id,props)}constructor(scope,id,props){super(scope,id,{...props,codeDirectory:path.join(__dirname,"auto-delete-images-handler"),runtimeName:(0,core_1.determineLatestNodeRuntimeName)(scope)}),this.node.addMetadata("aws:cdk:is-custom-resource-handler-customResourceProvider",!0)}}exports.AutoDeleteImagesProvider=AutoDeleteImagesProvider;

View File

@@ -0,0 +1,5 @@
import { Construct } from "constructs";
import * as lambda from "../../../aws-lambda";
export declare class DrainHookLambda_Function extends lambda.Function {
constructor(scope: Construct, id: string, props?: lambda.FunctionOptions);
}

View File

@@ -0,0 +1 @@
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.DrainHookLambda_Function=void 0;const path=require("path"),lambda=require("../../../aws-lambda");class DrainHookLambda_Function extends lambda.Function{constructor(scope,id,props){super(scope,id,{...props,code:lambda.Code.fromAsset(path.join(__dirname,"lambda-source")),handler:"index.lambda_handler",runtime:lambda.Runtime.PYTHON_3_13}),this.node.addMetadata("aws:cdk:is-custom-resource-handler-runtime-family",this.runtime.family)}}exports.DrainHookLambda_Function=DrainHookLambda_Function;

View File

@@ -0,0 +1,90 @@
import boto3, json, os, time
ecs = boto3.client('ecs')
autoscaling = boto3.client('autoscaling')
def lambda_handler(event, context):
print(json.dumps(dict(event, ResponseURL='...')))
cluster = os.environ['CLUSTER']
snsTopicArn = event['Records'][0]['Sns']['TopicArn']
lifecycle_event = json.loads(event['Records'][0]['Sns']['Message'])
instance_id = lifecycle_event.get('EC2InstanceId')
if not instance_id:
print(f"Got event without EC2InstanceId: { json.dumps(dict(event, ResponseURL='...')) }")
return
instance_arn = container_instance_arn(cluster, instance_id)
print('Instance %s has container instance ARN %s' % (lifecycle_event['EC2InstanceId'], instance_arn))
if not instance_arn:
return
task_arns = container_instance_task_arns(cluster, instance_arn)
if task_arns:
print('Instance ARN %s has task ARNs %s' % (instance_arn, ', '.join(task_arns)))
while has_tasks(cluster, instance_arn, task_arns):
time.sleep(10)
try:
print('Terminating instance %s' % instance_id)
autoscaling.complete_lifecycle_action(
LifecycleActionResult='CONTINUE',
**pick(lifecycle_event, 'LifecycleHookName', 'LifecycleActionToken', 'AutoScalingGroupName'))
except Exception as e:
# Lifecycle action may have already completed.
print(str(e))
def container_instance_arn(cluster, instance_id):
"""Turn an instance ID into a container instance ARN."""
arns = ecs.list_container_instances(cluster=cluster, filter='ec2InstanceId==' + instance_id)['containerInstanceArns']
if not arns:
return None
return arns[0]
def container_instance_task_arns(cluster, instance_arn):
"""Fetch tasks for a container instance ARN."""
arns = ecs.list_tasks(cluster=cluster, containerInstance=instance_arn)['taskArns']
return arns
def has_tasks(cluster, instance_arn, task_arns):
"""Return True if the instance is running tasks for the given cluster."""
instances = ecs.describe_container_instances(cluster=cluster, containerInstances=[instance_arn])['containerInstances']
if not instances:
return False
instance = instances[0]
if instance['status'] == 'ACTIVE':
# Start draining, then try again later
set_container_instance_to_draining(cluster, instance_arn)
return True
task_count = None
if task_arns:
# Fetch details for tasks running on the container instance
tasks = ecs.describe_tasks(cluster=cluster, tasks=task_arns)['tasks']
if tasks:
# Consider any non-stopped tasks as running
task_count = sum(task['lastStatus'] != 'STOPPED' for task in tasks) + instance['pendingTasksCount']
if not task_count:
# Fallback to instance task counts if detailed task information is unavailable
task_count = instance['runningTasksCount'] + instance['pendingTasksCount']
print('Instance %s has %s tasks' % (instance_arn, task_count))
return task_count > 0
def set_container_instance_to_draining(cluster, instance_arn):
ecs.update_container_instances_state(
cluster=cluster,
containerInstances=[instance_arn], status='DRAINING')
def pick(dct, *keys):
"""Pick a subset of a dict."""
return {k: v for k, v in dct.items() if k in keys}

View File

@@ -0,0 +1,93 @@
import json
import logging
import os
import subprocess
logger = logging.getLogger()
logger.setLevel(logging.INFO)
# these are coming from the kubectl layer
os.environ['PATH'] = '/opt/kubectl:/opt/awscli:' + os.environ['PATH']
outdir = os.environ.get('TEST_OUTDIR', '/tmp')
kubeconfig = os.path.join(outdir, 'kubeconfig')
def apply_handler(event, context):
logger.info(json.dumps(dict(event, ResponseURL='...')))
request_type = event['RequestType']
props = event['ResourceProperties']
# resource properties (all required)
cluster_name = props['ClusterName']
manifest_text = props['Manifest']
prune_label = props.get('PruneLabel', None)
overwrite = props.get('Overwrite', 'false').lower() == 'true'
skip_validation = props.get('SkipValidation', 'false').lower() == 'true'
# "log in" to the cluster
cmd = [ 'aws', 'eks', 'update-kubeconfig',
'--name', cluster_name,
'--kubeconfig', kubeconfig
]
logger.info(f'Running command: {cmd}')
subprocess.check_call(cmd)
if os.path.isfile(kubeconfig):
os.chmod(kubeconfig, 0o600)
# write resource manifests in sequence: { r1 }{ r2 }{ r3 } (this is how
# a stream of JSON objects can be included in a k8s manifest).
manifest_list = json.loads(manifest_text)
manifest_file = os.path.join(outdir, 'manifest.yaml')
with open(manifest_file, "w") as f:
f.writelines(map(lambda obj: json.dumps(obj), manifest_list))
logger.info("manifest written to: %s" % manifest_file)
kubectl_opts = []
if skip_validation:
kubectl_opts.extend(['--validate=false'])
if request_type == 'Create':
# if "overwrite" is enabled, then we use "apply" for CREATE operations
# which technically means we can determine the desired state of an
# existing resource.
if overwrite:
kubectl('apply', manifest_file, *kubectl_opts)
else:
# --save-config will allow us to use "apply" later
kubectl_opts.extend(['--save-config'])
kubectl('create', manifest_file, *kubectl_opts)
elif request_type == 'Update':
if prune_label is not None:
kubectl_opts.extend(['--prune', '-l', prune_label])
kubectl('apply', manifest_file, *kubectl_opts)
elif request_type == "Delete":
try:
kubectl('delete', manifest_file)
except Exception as e:
logger.info("delete error: %s" % e)
def kubectl(verb, file, *opts):
maxAttempts = 3
retry = maxAttempts
while retry > 0:
try:
cmd = ['kubectl', verb, '--kubeconfig', kubeconfig, '-f', file] + list(opts)
logger.info(f'Running command: {cmd}')
output = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
except subprocess.CalledProcessError as exc:
output = exc.output
if b'i/o timeout' in output and retry > 0:
retry = retry - 1
logger.info("kubectl timed out, retries left: %s" % retry)
else:
raise Exception(output)
else:
logger.info(output)
return
raise Exception(f'Operation failed after {maxAttempts} attempts: {output}')

View File

@@ -0,0 +1,86 @@
import json
import logging
import os
import subprocess
import time
logger = logging.getLogger()
logger.setLevel(logging.INFO)
# these are coming from the kubectl layer
os.environ['PATH'] = '/opt/kubectl:/opt/awscli:' + os.environ['PATH']
outdir = os.environ.get('TEST_OUTDIR', '/tmp')
kubeconfig = os.path.join(outdir, 'kubeconfig')
def get_handler(event, context):
logger.info(json.dumps(dict(event, ResponseURL='...')))
request_type = event['RequestType']
props = event['ResourceProperties']
# resource properties (all required)
cluster_name = props['ClusterName']
# "log in" to the cluster
subprocess.check_call([ 'aws', 'eks', 'update-kubeconfig',
'--name', cluster_name,
'--kubeconfig', kubeconfig
])
if os.path.isfile(kubeconfig):
os.chmod(kubeconfig, 0o600)
object_type = props['ObjectType']
object_name = props['ObjectName']
object_namespace = props['ObjectNamespace']
json_path = props['JsonPath']
timeout_seconds = props['TimeoutSeconds']
# json path should be surrouded with '{}'
path = '{{{0}}}'.format(json_path)
if request_type == 'Create' or request_type == 'Update':
output = wait_for_output(['get', '-n', object_namespace, object_type, object_name, "-o=jsonpath='{{{0}}}'".format(json_path)], int(timeout_seconds))
return {'Data': {'Value': output}}
elif request_type == 'Delete':
pass
else:
raise Exception("invalid request type %s" % request_type)
def wait_for_output(args, timeout_seconds):
end_time = time.time() + timeout_seconds
error = None
while time.time() < end_time:
try:
# the output is surrounded with '', so we unquote
output = kubectl(args).decode('utf-8')[1:-1]
if output:
return output
except Exception as e:
error = str(e)
# also a recoverable error
if 'NotFound' in error:
pass
time.sleep(10)
raise RuntimeError(f'Timeout waiting for output from kubectl command: {args} (last_error={error})')
def kubectl(args):
retry = 3
while retry > 0:
try:
cmd = [ 'kubectl', '--kubeconfig', kubeconfig ] + args
output = subprocess.check_output(cmd, stderr=subprocess.PIPE)
except subprocess.CalledProcessError as exc:
output = exc.output + exc.stderr
if b'i/o timeout' in output and retry > 0:
logger.info("kubectl timed out, retries left: %s" % retry)
retry = retry - 1
else:
raise Exception(output)
else:
logger.info(output)
return output

View File

@@ -0,0 +1,255 @@
import json
import logging
import os
import re
import subprocess
import shutil
import tempfile
import zipfile
import boto3
logger = logging.getLogger()
logger.setLevel(logging.INFO)
# these are coming from the kubectl layer
os.environ['PATH'] = '/opt/helm:/opt/awscli:' + os.environ['PATH']
outdir = os.environ.get('TEST_OUTDIR', '/tmp')
kubeconfig = os.path.join(outdir, 'kubeconfig')
def get_chart_asset_from_url(chart_asset_url):
chart_zip = os.path.join(outdir, 'chart.zip')
shutil.rmtree(chart_zip, ignore_errors=True)
subprocess.check_call(['aws', 's3', 'cp', chart_asset_url, chart_zip])
chart_dir = os.path.join(outdir, 'chart')
shutil.rmtree(chart_dir, ignore_errors=True)
os.mkdir(chart_dir)
with zipfile.ZipFile(chart_zip, 'r') as zip_ref:
zip_ref.extractall(chart_dir)
return chart_dir
def is_ecr_public_available(region):
s = boto3.Session()
return s.get_partition_for_region(region) == 'aws'
def helm_handler(event, context):
logger.info(json.dumps(dict(event, ResponseURL='...')))
request_type = event['RequestType']
props = event['ResourceProperties']
# resource properties
cluster_name = props['ClusterName']
release = props['Release']
chart = props.get('Chart', None)
chart_asset_url = props.get('ChartAssetURL', None)
version = props.get('Version', None)
wait = props.get('Wait', False)
atomic = props.get('Atomic', False)
timeout = props.get('Timeout', None)
namespace = props.get('Namespace', None)
create_namespace = props.get('CreateNamespace', None)
repository = props.get('Repository', None)
values_text = props.get('Values', None)
skip_crds = props.get('SkipCrds', False)
# "log in" to the cluster
subprocess.check_call([ 'aws', 'eks', 'update-kubeconfig',
'--name', cluster_name,
'--kubeconfig', kubeconfig
])
if os.path.isfile(kubeconfig):
os.chmod(kubeconfig, 0o600)
# Write out the values to a file and include them with the install and upgrade
values_file = None
if not request_type == "Delete" and not values_text is None:
values = json.loads(values_text)
values_file = os.path.join(outdir, 'values.yaml')
with open(values_file, "w") as f:
f.write(json.dumps(values, indent=2))
if request_type == 'Create' or request_type == 'Update':
# Ensure chart or chart_asset_url are set
if chart == None and chart_asset_url == None:
raise RuntimeError(f'chart or chartAsset must be specified')
if chart_asset_url != None:
assert(chart==None)
assert(repository==None)
assert(version==None)
if not chart_asset_url.startswith('s3://'):
raise RuntimeError(f'ChartAssetURL must point to as s3 location but is {chart_asset_url}')
# future work: support versions from s3 assets
chart = get_chart_asset_from_url(chart_asset_url)
if repository is not None and repository.startswith('oci://'):
tmpdir = tempfile.TemporaryDirectory()
chart_dir = get_chart_from_oci(tmpdir.name, repository, version)
chart = chart_dir
# Chart is now local — clear repository and version so helm() doesn't
# pass --repo/--version to "helm upgrade". Helm v4 (kubectl-v35+)
# rejects --repo with oci:// URLs ("invalid reference"), unlike v3.
repository = None
version = None
helm('upgrade', release, chart, repository, values_file, namespace, version, wait, timeout, create_namespace, atomic=atomic)
elif request_type == "Delete":
try:
helm('uninstall', release, namespace=namespace, wait=wait, timeout=timeout)
except Exception as e:
logger.info("delete error: %s" % e)
def get_oci_cmd(repository, version):
# Generates OCI command based on pattern. Public ECR vs Private ECR are treated differently.
private_ecr_pattern = 'oci://(?P<registry>\d+\.dkr\.ecr\.(?P<region>[a-z0-9\-]+)\.(?P<domain>[a-z0-9\.-]+))*'
public_ecr_pattern = 'oci://(?P<registry>public\.ecr\.aws)*'
private_registry = re.match(private_ecr_pattern, repository).groupdict()
public_registry = re.match(public_ecr_pattern, repository).groupdict()
# Build helm pull command as array
helm_cmd = ['helm', 'pull', repository, '--version', version , '--untar']
if private_registry['registry'] is not None:
logger.info("Found AWS private repository")
ecr_login = ['aws', 'ecr', 'get-login-password', '--region', private_registry['region']]
helm_registry_login = ['helm', 'registry', 'login', '--username', 'AWS', '--password-stdin', private_registry['registry']]
return {'ecr_login': ecr_login, 'helm_registry_login': helm_registry_login, 'helm': helm_cmd}
elif public_registry['registry'] is not None:
logger.info("Found AWS public repository, will use default region as deployment")
region = os.environ.get('AWS_REGION', 'us-east-1')
if is_ecr_public_available(region):
# Public ECR auth is always in us-east-1: https://docs.aws.amazon.com/AmazonECR/latest/public/public-registry-auth.html
ecr_login = ['aws', 'ecr-public', 'get-login-password', '--region', 'us-east-1']
helm_registry_login = ['helm', 'registry', 'login', '--username', 'AWS', '--password-stdin', public_registry['registry']]
return {'ecr_login': ecr_login, 'helm_registry_login': helm_registry_login, 'helm': helm_cmd}
else:
# No login required for public ECR in non-aws regions
# see https://helm.sh/docs/helm/helm_registry_login/
return {'helm': helm_cmd}
else:
logger.error("OCI repository format not recognized, falling back to helm pull")
return {'helm': helm_cmd}
def get_chart_from_oci(tmpdir, repository=None, version=None):
from subprocess import Popen, PIPE
commands = get_oci_cmd(repository, version)
maxAttempts = 3
retry = maxAttempts
while retry > 0:
try:
# Execute login commands if needed
if 'ecr_login' in commands and 'helm_registry_login' in commands:
logger.info("Running login command: %s", commands['ecr_login'])
logger.info("Running registry login command: %s", commands['helm_registry_login'])
# Start first process: aws ecr get-login-password
# NOTE: We do NOT call p1.wait() here before starting p2.
# Doing so could deadlock if p1's output fills the pipe buffer
# before p2 starts reading. Instead, start p2 immediately so it
# can consume p1's stdout as it's produced.
p1 = Popen(commands['ecr_login'], stdout=PIPE, stderr=PIPE, cwd=tmpdir)
# Start second process: helm registry login
p2 = Popen(commands['helm_registry_login'], stdin=p1.stdout, stdout=PIPE, stderr=PIPE, cwd=tmpdir)
p1.stdout.close() # Allow p1 to receive SIGPIPE if p2 exits early
# Wait for p2 to finish first (ensures full pipeline completes)
_, p2_err = p2.communicate()
# Now wait for p1 so we have a complete stderr and an exit code
p1.wait()
# Handle p1 failure
if p1.returncode != 0:
p1_err = p1.stderr.read().decode('utf-8', errors='replace') if p1.stderr else ''
logger.error(
"ECR get-login-password failed for repository %s. Error: %s",
repository,
p1_err or "No error details"
)
raise subprocess.CalledProcessError(p1.returncode, commands['ecr_login'], p1_err.encode())
# Handle p2 failure
if p2.returncode != 0:
logger.error(
"Helm registry authentication failed for repository %s. Error: %s",
repository,
p2_err.decode('utf-8', errors='replace') or "No error details"
)
raise subprocess.CalledProcessError(p2.returncode, commands['helm_registry_login'], p2_err)
# Execute helm pull command
logger.info("Running helm command: %s", commands['helm'])
output = subprocess.check_output(commands['helm'], stderr=subprocess.STDOUT, cwd=tmpdir)
logger.info(output)
# effectively returns "$tmpDir/$lastPartOfOCIUrl", because this is how helm pull saves OCI artifact.
# Eg. if we have oci://9999999999.dkr.ecr.us-east-1.amazonaws.com/foo/bar/pet-service repository, helm saves artifact under $tmpDir/pet-service
return os.path.join(tmpdir, repository.rpartition('/')[-1])
except subprocess.CalledProcessError as exc:
output = exc.output
if b'Broken pipe' in output:
retry = retry - 1
logger.info("Broken pipe, retries left: %s" % retry)
else:
raise Exception(output)
raise Exception(f'Operation failed after {maxAttempts} attempts: {output}')
def helm(verb, release, chart = None, repo = None, file = None, namespace = None, version = None, wait = False, timeout = None, create_namespace = None, skip_crds = False, atomic = False):
cmnd = ['helm', verb, release]
if not chart is None:
cmnd.append(chart)
if verb == 'upgrade':
cmnd.append('--install')
if create_namespace:
cmnd.append('--create-namespace')
if not repo is None:
cmnd.extend(['--repo', repo])
if not file is None:
cmnd.extend(['--values', file])
if not version is None:
cmnd.extend(['--version', version])
if not namespace is None:
cmnd.extend(['--namespace', namespace])
if wait:
cmnd.append('--wait')
if skip_crds:
cmnd.append('--skip-crds')
if not timeout is None:
cmnd.extend(['--timeout', timeout])
if atomic:
cmnd.append('--atomic')
cmnd.extend(['--kubeconfig', kubeconfig])
# Log the full helm command for better troubleshooting
logger.info("Running command: %s", cmnd)
maxAttempts = 3
retry = maxAttempts
while retry > 0:
try:
output = subprocess.check_output(cmnd, stderr=subprocess.STDOUT, cwd=outdir)
logger.info(output.decode('utf-8', errors='replace'))
return
except subprocess.CalledProcessError as exc:
output = exc.output
if b'Broken pipe' in output:
retry = retry - 1
logger.info("Broken pipe, retries left: %s" % retry)
else:
error_message = output.decode('utf-8', errors='replace')
logger.error("Command failed: %s", cmnd)
logger.error("Error output: %s", error_message)
raise Exception(output)
raise Exception(f'Operation failed after {maxAttempts} attempts: {output.decode("utf-8", errors="replace")}')

View File

@@ -0,0 +1,26 @@
import json
import logging
from apply import apply_handler
from helm import helm_handler
from patch import patch_handler
from get import get_handler
def handler(event, context):
print(json.dumps(dict(event, ResponseURL='...')))
resource_type = event['ResourceType']
if resource_type == 'Custom::AWSCDK-EKS-KubernetesResource':
return apply_handler(event, context)
if resource_type == 'Custom::AWSCDK-EKS-HelmChart':
return helm_handler(event, context)
if resource_type == 'Custom::AWSCDK-EKS-KubernetesPatch':
return patch_handler(event, context)
if resource_type == 'Custom::AWSCDK-EKS-KubernetesObjectValue':
return get_handler(event, context)
raise Exception("unknown resource type %s" % resource_type)

View File

@@ -0,0 +1,68 @@
import json
import logging
import os
import subprocess
logger = logging.getLogger()
logger.setLevel(logging.INFO)
# these are coming from the kubectl layer
os.environ['PATH'] = '/opt/kubectl:/opt/awscli:' + os.environ['PATH']
outdir = os.environ.get('TEST_OUTDIR', '/tmp')
kubeconfig = os.path.join(outdir, 'kubeconfig')
def patch_handler(event, context):
logger.info(json.dumps(dict(event, ResponseURL='...')))
request_type = event['RequestType']
props = event['ResourceProperties']
# resource properties (all required)
cluster_name = props['ClusterName']
# "log in" to the cluster
subprocess.check_call([ 'aws', 'eks', 'update-kubeconfig',
'--name', cluster_name,
'--kubeconfig', kubeconfig
])
if os.path.isfile(kubeconfig):
os.chmod(kubeconfig, 0o600)
resource_name = props['ResourceName']
resource_namespace = props['ResourceNamespace']
apply_patch_json = props['ApplyPatchJson']
restore_patch_json = props['RestorePatchJson']
patch_type = props['PatchType']
patch_json = None
if request_type == 'Create' or request_type == 'Update':
patch_json = apply_patch_json
elif request_type == 'Delete':
patch_json = restore_patch_json
else:
raise Exception("invalid request type %s" % request_type)
kubectl([ 'patch', resource_name, '-n', resource_namespace, '-p', patch_json, '--type', patch_type ])
def kubectl(args):
maxAttempts = 3
retry = maxAttempts
while retry > 0:
try:
cmd = [ 'kubectl', '--kubeconfig', kubeconfig ] + args
output = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
except subprocess.CalledProcessError as exc:
output = exc.output
if b'i/o timeout' in output and retry > 0:
retry = retry - 1
logger.info("kubectl timed out, retries left: %s" % retry)
else:
raise Exception(output)
else:
logger.info(output)
return
raise Exception(f'Operation failed after {maxAttempts} attempts: {output}')

View File

@@ -0,0 +1,5 @@
import { Construct } from "constructs";
import * as lambda from "../../../aws-lambda";
export declare class KubectlFunction extends lambda.Function {
constructor(scope: Construct, id: string, props?: lambda.FunctionOptions);
}

View File

@@ -0,0 +1 @@
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.KubectlFunction=void 0;const path=require("path"),lambda=require("../../../aws-lambda");class KubectlFunction extends lambda.Function{constructor(scope,id,props){super(scope,id,{...props,code:lambda.Code.fromAsset(path.join(__dirname,"kubectl-handler")),handler:"index.handler",runtime:lambda.Runtime.PYTHON_3_13}),this.node.addMetadata("aws:cdk:is-custom-resource-handler-runtime-family",this.runtime.family)}}exports.KubectlFunction=KubectlFunction;

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,8 @@
import { Construct } from "constructs";
import * as lambda from "../../../aws-lambda";
export declare class ClusterResourceOnEventFunction extends lambda.Function {
constructor(scope: Construct, id: string, props?: lambda.FunctionOptions);
}
export declare class ClusterResourceIsCompleteFunction extends lambda.Function {
constructor(scope: Construct, id: string, props?: lambda.FunctionOptions);
}

View File

@@ -0,0 +1 @@
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.ClusterResourceIsCompleteFunction=exports.ClusterResourceOnEventFunction=void 0;const path=require("path"),lambda=require("../../../aws-lambda");class ClusterResourceOnEventFunction extends lambda.Function{constructor(scope,id,props){super(scope,id,{...props,code:lambda.Code.fromAsset(path.join(__dirname,"cluster-resource-handler")),handler:"index.onEvent",runtime:lambda.determineLatestNodeRuntime(scope)}),this.node.addMetadata("aws:cdk:is-custom-resource-handler-runtime-family",this.runtime.family)}}exports.ClusterResourceOnEventFunction=ClusterResourceOnEventFunction;class ClusterResourceIsCompleteFunction extends lambda.Function{constructor(scope,id,props){super(scope,id,{...props,code:lambda.Code.fromAsset(path.join(__dirname,"cluster-resource-handler")),handler:"index.isComplete",runtime:lambda.determineLatestNodeRuntime(scope)}),this.node.addMetadata("aws:cdk:is-custom-resource-handler-runtime-family",this.runtime.family)}}exports.ClusterResourceIsCompleteFunction=ClusterResourceIsCompleteFunction;

View File

@@ -0,0 +1,95 @@
import json
import logging
import os
import subprocess
logger = logging.getLogger()
logger.setLevel(logging.INFO)
# these are coming from the kubectl layer
os.environ['PATH'] = '/opt/kubectl:/opt/awscli:' + os.environ['PATH']
outdir = os.environ.get('TEST_OUTDIR', '/tmp')
kubeconfig = os.path.join(outdir, 'kubeconfig')
def apply_handler(event, context):
logger.info(json.dumps(dict(event, ResponseURL='...')))
request_type = event['RequestType']
props = event['ResourceProperties']
# resource properties (all required)
cluster_name = props['ClusterName']
manifest_text = props['Manifest']
role_arn = props['RoleArn']
prune_label = props.get('PruneLabel', None)
overwrite = props.get('Overwrite', 'false').lower() == 'true'
skip_validation = props.get('SkipValidation', 'false').lower() == 'true'
# "log in" to the cluster
cmd = [ 'aws', 'eks', 'update-kubeconfig',
'--role-arn', role_arn,
'--name', cluster_name,
'--kubeconfig', kubeconfig
]
logger.info(f'Running command: {cmd}')
subprocess.check_call(cmd)
if os.path.isfile(kubeconfig):
os.chmod(kubeconfig, 0o600)
# write resource manifests in sequence: { r1 }{ r2 }{ r3 } (this is how
# a stream of JSON objects can be included in a k8s manifest).
manifest_list = json.loads(manifest_text)
manifest_file = os.path.join(outdir, 'manifest.yaml')
with open(manifest_file, "w") as f:
f.writelines(map(lambda obj: json.dumps(obj), manifest_list))
logger.info("manifest written to: %s" % manifest_file)
kubectl_opts = []
if skip_validation:
kubectl_opts.extend(['--validate=false'])
if request_type == 'Create':
# if "overwrite" is enabled, then we use "apply" for CREATE operations
# which technically means we can determine the desired state of an
# existing resource.
if overwrite:
kubectl('apply', manifest_file, *kubectl_opts)
else:
# --save-config will allow us to use "apply" later
kubectl_opts.extend(['--save-config'])
kubectl('create', manifest_file, *kubectl_opts)
elif request_type == 'Update':
if prune_label is not None:
kubectl_opts.extend(['--prune', '-l', prune_label])
kubectl('apply', manifest_file, *kubectl_opts)
elif request_type == "Delete":
try:
kubectl('delete', manifest_file)
except Exception as e:
logger.info("delete error: %s" % e)
def kubectl(verb, file, *opts):
maxAttempts = 3
retry = maxAttempts
while retry > 0:
try:
cmd = ['kubectl', verb, '--kubeconfig', kubeconfig, '-f', file] + list(opts)
logger.info(f'Running command: {cmd}')
output = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
except subprocess.CalledProcessError as exc:
output = exc.output
if b'i/o timeout' in output and retry > 0:
retry = retry - 1
logger.info("kubectl timed out, retries left: %s" % retry)
else:
raise Exception(output)
else:
logger.info(output)
return
raise Exception(f'Operation failed after {maxAttempts} attempts: {output}')

View File

@@ -0,0 +1,88 @@
import json
import logging
import os
import subprocess
import time
logger = logging.getLogger()
logger.setLevel(logging.INFO)
# these are coming from the kubectl layer
os.environ['PATH'] = '/opt/kubectl:/opt/awscli:' + os.environ['PATH']
outdir = os.environ.get('TEST_OUTDIR', '/tmp')
kubeconfig = os.path.join(outdir, 'kubeconfig')
def get_handler(event, context):
logger.info(json.dumps(dict(event, ResponseURL='...')))
request_type = event['RequestType']
props = event['ResourceProperties']
# resource properties (all required)
cluster_name = props['ClusterName']
role_arn = props['RoleArn']
# "log in" to the cluster
subprocess.check_call([ 'aws', 'eks', 'update-kubeconfig',
'--role-arn', role_arn,
'--name', cluster_name,
'--kubeconfig', kubeconfig
])
if os.path.isfile(kubeconfig):
os.chmod(kubeconfig, 0o600)
object_type = props['ObjectType']
object_name = props['ObjectName']
object_namespace = props['ObjectNamespace']
json_path = props['JsonPath']
timeout_seconds = props['TimeoutSeconds']
# json path should be surrounded with '{}'
path = '{{{0}}}'.format(json_path)
if request_type == 'Create' or request_type == 'Update':
output = wait_for_output(['get', '-n', object_namespace, object_type, object_name, "-o=jsonpath='{{{0}}}'".format(json_path)], int(timeout_seconds))
return {'Data': {'Value': output}}
elif request_type == 'Delete':
pass
else:
raise Exception("invalid request type %s" % request_type)
def wait_for_output(args, timeout_seconds):
end_time = time.time() + timeout_seconds
error = None
while time.time() < end_time:
try:
# the output is surrounded with '', so we unquote
output = kubectl(args).decode('utf-8')[1:-1]
if output:
return output
except Exception as e:
error = str(e)
# also a recoverable error
if 'NotFound' in error:
pass
time.sleep(10)
raise RuntimeError(f'Timeout waiting for output from kubectl command: {args} (last_error={error})')
def kubectl(args):
retry = 3
while retry > 0:
try:
cmd = [ 'kubectl', '--kubeconfig', kubeconfig ] + args
output = subprocess.check_output(cmd, stderr=subprocess.PIPE)
except subprocess.CalledProcessError as exc:
output = exc.output + exc.stderr
if b'i/o timeout' in output and retry > 0:
logger.info("kubectl timed out, retries left: %s" % retry)
retry = retry - 1
else:
raise Exception(output)
else:
logger.info(output)
return output

View File

@@ -0,0 +1,263 @@
import json
import logging
import os
import re
import subprocess
import shutil
import tempfile
import zipfile
import boto3
logger = logging.getLogger()
logger.setLevel(logging.INFO)
# these are coming from the kubectl layer
os.environ['PATH'] = '/opt/helm:/opt/awscli:' + os.environ['PATH']
outdir = os.environ.get('TEST_OUTDIR', '/tmp')
kubeconfig = os.path.join(outdir, 'kubeconfig')
def get_chart_asset_from_url(chart_asset_url):
chart_zip = os.path.join(outdir, 'chart.zip')
shutil.rmtree(chart_zip, ignore_errors=True)
subprocess.check_call(['aws', 's3', 'cp', chart_asset_url, chart_zip])
chart_dir = os.path.join(outdir, 'chart')
shutil.rmtree(chart_dir, ignore_errors=True)
os.mkdir(chart_dir)
with zipfile.ZipFile(chart_zip, 'r') as zip_ref:
zip_ref.extractall(chart_dir)
return chart_dir
def is_ecr_public_available(region):
s = boto3.Session()
return s.get_partition_for_region(region) == 'aws'
def helm_handler(event, context):
logger.info(json.dumps(dict(event, ResponseURL='...')))
request_type = event['RequestType']
props = event['ResourceProperties']
# resource properties
cluster_name = props['ClusterName']
role_arn = props['RoleArn']
release = props['Release']
chart = props.get('Chart', None)
chart_asset_url = props.get('ChartAssetURL', None)
version = props.get('Version', None)
wait = props.get('Wait', False)
atomic = props.get('Atomic', False)
timeout = props.get('Timeout', None)
namespace = props.get('Namespace', None)
create_namespace = props.get('CreateNamespace', None)
repository = props.get('Repository', None)
values_text = props.get('Values', None)
skip_crds = props.get('SkipCrds', False)
# "log in" to the cluster
subprocess.check_call([ 'aws', 'eks', 'update-kubeconfig',
'--role-arn', role_arn,
'--name', cluster_name,
'--kubeconfig', kubeconfig
])
if os.path.isfile(kubeconfig):
os.chmod(kubeconfig, 0o600)
# Write out the values to a file and include them with the install and upgrade
values_file = None
if not request_type == "Delete" and not values_text is None:
values = json.loads(values_text)
values_file = os.path.join(outdir, 'values.yaml')
with open(values_file, "w") as f:
f.write(json.dumps(values, indent=2))
if request_type == 'Create' or request_type == 'Update':
# Ensure chart or chart_asset_url are set
if chart == None and chart_asset_url == None:
raise RuntimeError(f'chart or chartAsset must be specified')
if chart_asset_url != None:
assert(chart==None)
assert(repository==None)
assert(version==None)
if not chart_asset_url.startswith('s3://'):
raise RuntimeError(f'ChartAssetURL must point to as s3 location but is {chart_asset_url}')
# future work: support versions from s3 assets
chart = get_chart_asset_from_url(chart_asset_url)
if repository is not None and repository.startswith('oci://'):
tmpdir = tempfile.TemporaryDirectory()
chart_dir = get_chart_from_oci(tmpdir.name, repository, version)
chart = chart_dir
# Chart is now local — clear repository and version so helm() doesn't
# pass --repo/--version to "helm upgrade". Helm v4 (kubectl-v35+)
# rejects --repo with oci:// URLs ("invalid reference"), unlike v3.
repository = None
version = None
helm('upgrade', release, chart, repository, values_file, namespace, version, wait, timeout, create_namespace, skip_crds, atomic=atomic)
elif request_type == "Delete":
try:
helm('uninstall', release, namespace=namespace, wait=wait, timeout=timeout)
except Exception as e:
logger.error("Delete error: %s", str(e))
def get_oci_cmd(repository, version):
# Generates OCI command based on pattern. Public ECR vs Private ECR are treated differently.
private_ecr_pattern = 'oci://(?P<registry>\d+\.dkr\.ecr\.(?P<region>[a-z0-9\-]+)\.(?P<domain>[a-z0-9\.-]+))*'
public_ecr_pattern = 'oci://(?P<registry>public\.ecr\.aws)*'
private_registry = re.match(private_ecr_pattern, repository).groupdict()
public_registry = re.match(public_ecr_pattern, repository).groupdict()
# Build helm pull command as array
helm_cmd = ['helm', 'pull', repository, '--version', version , '--untar']
if private_registry['registry'] is not None:
logger.info("Found AWS private repository")
ecr_login = ['aws', 'ecr', 'get-login-password', '--region', private_registry['region']]
helm_registry_login = ['helm', 'registry', 'login', '--username', 'AWS', '--password-stdin', private_registry['registry']]
return {'ecr_login': ecr_login, 'helm_registry_login': helm_registry_login, 'helm': helm_cmd}
elif public_registry['registry'] is not None:
logger.info("Found AWS public repository, will use default region as deployment")
region = os.environ.get('AWS_REGION', 'us-east-1')
if is_ecr_public_available(region):
# Public ECR auth is always in us-east-1: https://docs.aws.amazon.com/AmazonECR/latest/public/public-registry-auth.html
ecr_login = ['aws', 'ecr-public', 'get-login-password', '--region', 'us-east-1']
helm_registry_login = ['helm', 'registry', 'login', '--username', 'AWS', '--password-stdin', public_registry['registry']]
return {'ecr_login': ecr_login, 'helm_registry_login': helm_registry_login, 'helm': helm_cmd}
else:
# No login required for public ECR in non-aws regions
# see https://helm.sh/docs/helm/helm_registry_login/
return {'helm': helm_cmd}
else:
logger.error("OCI repository format not recognized, falling back to helm pull")
return {'helm': helm_cmd}
def get_chart_from_oci(tmpdir, repository=None, version=None):
from subprocess import Popen, PIPE
commands = get_oci_cmd(repository, version)
maxAttempts = 3
retry = maxAttempts
while retry > 0:
try:
# Execute login commands if needed
if 'ecr_login' in commands and 'helm_registry_login' in commands:
logger.info("Running login command: %s", commands['ecr_login'])
logger.info("Running registry login command: %s", commands['helm_registry_login'])
# Start first process: aws ecr get-login-password
# NOTE: We do NOT call p1.wait() here before starting p2.
# Doing so could deadlock if p1's output fills the pipe buffer
# before p2 starts reading. Instead, start p2 immediately so it
# can consume p1's stdout as it's produced.
p1 = Popen(commands['ecr_login'], stdout=PIPE, stderr=PIPE, cwd=tmpdir)
# Start second process: helm registry login
p2 = Popen(commands['helm_registry_login'], stdin=p1.stdout, stdout=PIPE, stderr=PIPE, cwd=tmpdir)
p1.stdout.close() # Allow p1 to receive SIGPIPE if p2 exits early
# Wait for p2 to finish first (ensures full pipeline completes)
_, p2_err = p2.communicate()
# Now wait for p1 so we have a complete stderr and an exit code
p1.wait()
# Handle p1 failure
if p1.returncode != 0:
p1_err = p1.stderr.read().decode('utf-8', errors='replace') if p1.stderr else ''
logger.error(
"ECR get-login-password failed for repository %s. Error: %s",
repository,
p1_err or "No error details"
)
raise subprocess.CalledProcessError(p1.returncode, commands['ecr_login'], p1_err.encode())
# Handle p2 failure
if p2.returncode != 0:
p1.kill()
logger.error(
"Helm registry authentication failed for repository %s. Error: %s",
repository,
p2_err.decode('utf-8', errors='replace') or "No error details"
)
raise subprocess.CalledProcessError(p2.returncode, commands['helm_registry_login'], p2_err)
# Execute helm pull command
logger.info("Running helm command: %s", commands['helm'])
output = subprocess.check_output(commands['helm'], stderr=subprocess.STDOUT, cwd=tmpdir)
logger.info(output.decode('utf-8', errors='replace'))
# effectively returns "$tmpDir/$lastPartOfOCIUrl", because this is how helm pull saves OCI artifact.
# Eg. if we have oci://9999999999.dkr.ecr.us-east-1.amazonaws.com/foo/bar/pet-service repository, helm saves artifact under $tmpDir/pet-service
return os.path.join(tmpdir, repository.rpartition('/')[-1])
except subprocess.CalledProcessError as exc:
output = exc.output
if b'Broken pipe' in output:
retry = retry - 1
logger.info("Broken pipe, retries left: %s" % retry)
else:
error_message = output.decode('utf-8', errors='replace')
logger.error("OCI command failed: %s", commands['helm'])
logger.error("Error output: %s", error_message)
raise Exception(output)
raise Exception(f'Operation failed after {maxAttempts} attempts: {output.decode("utf-8", errors="replace")}')
def helm(verb, release, chart = None, repo = None, file = None, namespace = None, version = None, wait = False, timeout = None, create_namespace = None, skip_crds = False, atomic = False):
import subprocess
cmnd = ['helm', verb, release]
if not chart is None:
cmnd.append(chart)
if verb == 'upgrade':
cmnd.append('--install')
if create_namespace:
cmnd.append('--create-namespace')
if not repo is None:
cmnd.extend(['--repo', repo])
if not file is None:
cmnd.extend(['--values', file])
if not version is None:
cmnd.extend(['--version', version])
if not namespace is None:
cmnd.extend(['--namespace', namespace])
if wait:
cmnd.append('--wait')
if skip_crds:
cmnd.append('--skip-crds')
if not timeout is None:
cmnd.extend(['--timeout', timeout])
if atomic:
cmnd.append('--atomic')
cmnd.extend(['--kubeconfig', kubeconfig])
# Log the full helm command for better troubleshooting
logger.info("Running command: %s", cmnd)
maxAttempts = 3
retry = maxAttempts
while retry > 0:
try:
output = subprocess.check_output(cmnd, stderr=subprocess.STDOUT, cwd=outdir)
logger.info(output.decode('utf-8', errors='replace'))
return
except subprocess.CalledProcessError as exc:
output = exc.output
if b'Broken pipe' in output:
retry = retry - 1
logger.info("Broken pipe, retries left: %s" % retry)
else:
error_message = output.decode('utf-8', errors='replace')
logger.error("Command failed: %s", cmnd)
logger.error("Error output: %s", error_message)
raise Exception(output)
raise Exception(f'Operation failed after {maxAttempts} attempts: {output.decode("utf-8", errors="replace")}')

View File

@@ -0,0 +1,26 @@
import json
import logging
from apply import apply_handler
from helm import helm_handler
from patch import patch_handler
from get import get_handler
def handler(event, context):
print(json.dumps(dict(event, ResponseURL='...')))
resource_type = event['ResourceType']
if resource_type == 'Custom::AWSCDK-EKS-KubernetesResource':
return apply_handler(event, context)
if resource_type == 'Custom::AWSCDK-EKS-HelmChart':
return helm_handler(event, context)
if resource_type == 'Custom::AWSCDK-EKS-KubernetesPatch':
return patch_handler(event, context)
if resource_type == 'Custom::AWSCDK-EKS-KubernetesObjectValue':
return get_handler(event, context)
raise Exception("unknown resource type %s" % resource_type)

View File

@@ -0,0 +1,70 @@
import json
import logging
import os
import subprocess
logger = logging.getLogger()
logger.setLevel(logging.INFO)
# these are coming from the kubectl layer
os.environ['PATH'] = '/opt/kubectl:/opt/awscli:' + os.environ['PATH']
outdir = os.environ.get('TEST_OUTDIR', '/tmp')
kubeconfig = os.path.join(outdir, 'kubeconfig')
def patch_handler(event, context):
logger.info(json.dumps(dict(event, ResponseURL='...')))
request_type = event['RequestType']
props = event['ResourceProperties']
# resource properties (all required)
cluster_name = props['ClusterName']
role_arn = props['RoleArn']
# "log in" to the cluster
subprocess.check_call([ 'aws', 'eks', 'update-kubeconfig',
'--role-arn', role_arn,
'--name', cluster_name,
'--kubeconfig', kubeconfig
])
if os.path.isfile(kubeconfig):
os.chmod(kubeconfig, 0o600)
resource_name = props['ResourceName']
resource_namespace = props['ResourceNamespace']
apply_patch_json = props['ApplyPatchJson']
restore_patch_json = props['RestorePatchJson']
patch_type = props['PatchType']
patch_json = None
if request_type == 'Create' or request_type == 'Update':
patch_json = apply_patch_json
elif request_type == 'Delete':
patch_json = restore_patch_json
else:
raise Exception("invalid request type %s" % request_type)
kubectl([ 'patch', resource_name, '-n', resource_namespace, '-p', patch_json, '--type', patch_type ])
def kubectl(args):
maxAttempts = 3
retry = maxAttempts
while retry > 0:
try:
cmd = [ 'kubectl', '--kubeconfig', kubeconfig ] + args
output = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
except subprocess.CalledProcessError as exc:
output = exc.output
if b'i/o timeout' in output and retry > 0:
retry = retry - 1
logger.info("kubectl timed out, retries left: %s" % retry)
else:
raise Exception(output)
else:
logger.info(output)
return
raise Exception(f'Operation failed after {maxAttempts} attempts: {output}')

View File

@@ -0,0 +1,5 @@
import { Construct } from "constructs";
import * as lambda from "../../../aws-lambda";
export declare class KubectlFunction extends lambda.Function {
constructor(scope: Construct, id: string, props?: lambda.FunctionOptions);
}

View File

@@ -0,0 +1 @@
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.KubectlFunction=void 0;const path=require("path"),lambda=require("../../../aws-lambda");class KubectlFunction extends lambda.Function{constructor(scope,id,props){super(scope,id,{...props,code:lambda.Code.fromAsset(path.join(__dirname,"kubectl-handler")),handler:"index.handler",runtime:lambda.Runtime.PYTHON_3_13}),this.node.addMetadata("aws:cdk:is-custom-resource-handler-runtime-family",this.runtime.family)}}exports.KubectlFunction=KubectlFunction;

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,27 @@
import { Construct } from "constructs";
import * as lambda from "../../../aws-lambda";
export declare class AwsApiSingletonFunction extends lambda.SingletonFunction {
constructor(scope: Construct, id: string, props: AwsApiSingletonFunctionProps);
}
/**
* Initialization properties for AwsApiSingletonFunction
*/
export interface AwsApiSingletonFunctionProps extends lambda.FunctionOptions {
/**
* A unique identifier to identify this Lambda.
*
* The identifier should be unique across all custom resource providers.
* We recommend generating a UUID per provider.
*/
readonly uuid: string;
/**
* A descriptive name for the purpose of this Lambda.
*
* If the Lambda does not have a physical name, this string will be
* reflected in its generated name. The combination of lambdaPurpose
* and uuid must be unique.
*
* @default SingletonLambda
*/
readonly lambdaPurpose?: string;
}

View File

@@ -0,0 +1 @@
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.AwsApiSingletonFunction=void 0;const path=require("path"),lambda=require("../../../aws-lambda");class AwsApiSingletonFunction extends lambda.SingletonFunction{constructor(scope,id,props){super(scope,id,{...props,code:lambda.Code.fromAsset(path.join(__dirname,"aws-api-handler")),handler:"index.handler",runtime:lambda.determineLatestNodeRuntime(scope)}),this.addMetadata("aws:cdk:is-custom-resource-handler-singleton",!0),this.addMetadata("aws:cdk:is-custom-resource-handler-runtime-family",this.runtime.family),props?.logGroup&&this.logGroup.node.addMetadata("aws:cdk:is-custom-resource-handler-logGroup",!0),props?.logRetention&&this.lambdaFunction._logRetention?.node.addMetadata("aws:cdk:is-custom-resource-handler-logRetention",!0)}}exports.AwsApiSingletonFunction=AwsApiSingletonFunction;

View File

@@ -0,0 +1 @@
"use strict";var y=Object.create,l=Object.defineProperty,v=Object.getOwnPropertyDescriptor,O=Object.getOwnPropertyNames,w=Object.getPrototypeOf,R=Object.prototype.hasOwnProperty,A=(e,r)=>{for(var t in r)l(e,t,{get:r[t],enumerable:!0})},D=(e,r,t,i)=>{if(r&&typeof r=="object"||typeof r=="function")for(let o of O(r))!R.call(e,o)&&o!==t&&l(e,o,{get:()=>r[o],enumerable:!(i=v(r,o))||i.enumerable});return e},m=(e,r,t)=>(t=e!=null?y(w(e)):{},D(r||!e||!e.__esModule?l(t,"default",{value:e,enumerable:!0}):t,e)),$=e=>D(l({},"__esModule",{value:!0}),e),j={};A(j,{handler:()=>x}),module.exports=$(j);function h(e,r){let t=new Set(e),i=new Set;for(let o of new Set(r))t.has(o)?t.delete(o):i.add(o);return{adds:Array.from(i),deletes:Array.from(t)}}var g=m(require("tls")),P=m(require("url")),T=m(require("@aws-sdk/client-iam")),C;function u(){return C||(C=new T.IAM({})),C}function U(e,...r){console.log(e,...r)}async function L(e,r){return new Promise((t,i)=>{let o=P.parse(e),p=o.port?parseInt(o.port,10):443;if(!o.host)return i(new Error(`unable to determine host from issuer url ${e}`));n.log(`Fetching x509 certificate chain from issuer ${e}`);let s=g.connect(p,o.host,{rejectUnauthorized:r,servername:o.host});s.once("error",i),s.once("secureConnect",()=>{let a=s.getPeerX509Certificate();if(!a)throw new Error(`Unable to retrieve X509 certificate from host ${o.host}`);for(;a.issuerCertificate;)E(a),a=a.issuerCertificate;let d=new Date(a.validTo),c=S(d);if(c<0)return i(new Error(`The certificate has already expired on: ${d.toUTCString()}`));c<180&&console.warn(`The root certificate obtained would expire in ${c} days!`),s.end();let I=f(a);n.log(`Certificate Authority thumbprint for ${e} is ${I}`),t(I)})})}function f(e){return e.fingerprint.split(":").join("")}function E(e){n.log("-------------BEGIN CERT----------------"),n.log(`Thumbprint: ${f(e)}`),n.log(`Valid To: ${e.validTo}`),e.issuerCertificate&&n.log(`Issuer Thumbprint: ${f(e.issuerCertificate)}`),n.log(`Issuer: ${e.issuer}`),n.log(`Subject: ${e.subject}`),n.log("-------------END CERT------------------")}function S(e){let t=new Date;return Math.round((e.getTime()-t.getTime())/864e5)}var n={downloadThumbprint:L,log:U,createOpenIDConnectProvider:e=>u().createOpenIDConnectProvider(e),deleteOpenIDConnectProvider:e=>u().deleteOpenIDConnectProvider(e),updateOpenIDConnectProviderThumbprint:e=>u().updateOpenIDConnectProviderThumbprint(e),addClientIDToOpenIDConnectProvider:e=>u().addClientIDToOpenIDConnectProvider(e),removeClientIDFromOpenIDConnectProvider:e=>u().removeClientIDFromOpenIDConnectProvider(e)};async function x(e){if(e.RequestType==="Create")return b(e);if(e.RequestType==="Update")return F(e);if(e.RequestType==="Delete")return k(e);throw new Error("invalid request type")}async function b(e){let r=e.ResourceProperties.Url,t=(e.ResourceProperties.ThumbprintList??[]).sort(),i=(e.ResourceProperties.ClientIDList??[]).sort(),o=e.ResourceProperties.RejectUnauthorized??!1;return t.length===0&&t.push(await n.downloadThumbprint(r,o)),{PhysicalResourceId:(await n.createOpenIDConnectProvider({Url:r,ClientIDList:i,ThumbprintList:t})).OpenIDConnectProviderArn,Data:{Thumbprints:JSON.stringify(t)}}}async function F(e){let r=e.ResourceProperties.Url,t=(e.ResourceProperties.ThumbprintList??[]).sort(),i=(e.ResourceProperties.ClientIDList??[]).sort(),o=e.ResourceProperties.RejectUnauthorized??!1;if(e.OldResourceProperties.Url!==r)return b({...e,RequestType:"Create"});let s=e.PhysicalResourceId;t.length===0&&t.push(await n.downloadThumbprint(r,o)),n.log("updating thumbprint to",t),await n.updateOpenIDConnectProviderThumbprint({OpenIDConnectProviderArn:s,ThumbprintList:t});let a=(e.OldResourceProperties.ClientIDList||[]).sort(),d=h(a,i);n.log(`client ID diff: ${JSON.stringify(d)}`);for(let c of d.adds)n.log(`adding client id "${c}" to provider ${s}`),await n.addClientIDToOpenIDConnectProvider({OpenIDConnectProviderArn:s,ClientID:c});for(let c of d.deletes)n.log(`removing client id "${c}" from provider ${s}`),await n.removeClientIDFromOpenIDConnectProvider({OpenIDConnectProviderArn:s,ClientID:c});return{Data:{Thumbprints:JSON.stringify(t)}}}async function k(e){await n.deleteOpenIDConnectProvider({OpenIDConnectProviderArn:e.PhysicalResourceId})}

View File

@@ -0,0 +1,13 @@
import { Construct } from "constructs";
import { CustomResourceProviderBase, CustomResourceProviderOptions } from "../../../core";
export declare class OidcProvider extends CustomResourceProviderBase {
/**
* Returns a stack-level singleton ARN (service token) for the custom resource provider.
*/
static getOrCreate(scope: Construct, uniqueid: string, props?: CustomResourceProviderOptions): string;
/**
* Returns a stack-level singleton for the custom resource provider.
*/
static getOrCreateProvider(scope: Construct, uniqueid: string, props?: CustomResourceProviderOptions): OidcProvider;
private constructor();
}

View File

@@ -0,0 +1 @@
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.OidcProvider=void 0;const path=require("path"),core_1=require("../../../core");class OidcProvider extends core_1.CustomResourceProviderBase{static getOrCreate(scope,uniqueid,props){return this.getOrCreateProvider(scope,uniqueid,props).serviceToken}static getOrCreateProvider(scope,uniqueid,props){const id=`${uniqueid}CustomResourceProvider`,stack=core_1.Stack.of(scope);return stack.node.tryFindChild(id)??new OidcProvider(stack,id,props)}constructor(scope,id,props){super(scope,id,{...props,codeDirectory:path.join(__dirname,"oidc-handler"),runtimeName:(0,core_1.determineLatestNodeRuntimeName)(scope)}),this.node.addMetadata("aws:cdk:is-custom-resource-handler-customResourceProvider",!0)}}exports.OidcProvider=OidcProvider;

View File

@@ -0,0 +1 @@
"use strict";var h=Object.create,d=Object.defineProperty,P=Object.getOwnPropertyDescriptor,C=Object.getOwnPropertyNames,b=Object.getPrototypeOf,S=Object.prototype.hasOwnProperty,E=(e,o)=>{for(var n in o)d(e,n,{get:o[n],enumerable:!0})},p=(e,o,n,t)=>{if(o&&typeof o=="object"||typeof o=="function")for(let r of C(o))!S.call(e,r)&&r!==n&&d(e,r,{get:()=>o[r],enumerable:!(t=P(o,r))||t.enumerable});return e},G=(e,o,n)=>(n=e!=null?h(b(e)):{},p(o||!e||!e.__esModule?d(n,"default",{value:e,enumerable:!0}):n,e)),x=e=>p(d({},"__esModule",{value:!0}),e),O={};E(O,{disableSleepForTesting:()=>I,handler:()=>q}),module.exports=x(O);var i=G(require("@aws-sdk/client-cloudwatch-logs")),w=!1;function I(){w=!0}async function R(e,o,n){await n(async()=>{try{let t={logGroupName:e},r=new i.CreateLogGroupCommand(t);await o.send(r)}catch(t){if(t.name==="ResourceAlreadyExistsException")return;throw t}})}async function k(e,o,n){await n(async()=>{try{let t={logGroupName:e},r=new i.DeleteLogGroupCommand(t);await o.send(r)}catch(t){if(t.name==="ResourceNotFoundException")return;throw t}})}async function y(e,o,n,t){await n(async()=>{if(t){let r={logGroupName:e,retentionInDays:t},s=new i.PutRetentionPolicyCommand(r);await o.send(s)}else{let r={logGroupName:e},s=new i.DeleteRetentionPolicyCommand(r);await o.send(s)}})}async function q(e,o){try{console.log(JSON.stringify({...e,ResponseURL:"..."}));let t=e.ResourceProperties.LogGroupName,r=e.ResourceProperties.LogGroupRegion,s=L(e.ResourceProperties.SdkRetry?.maxRetries)??10,a=N(s),m={logger:console,region:r},c=new i.CloudWatchLogsClient(m);if((e.RequestType==="Create"||e.RequestType==="Update")&&(await R(t,c,a),await y(t,c,a,L(e.ResourceProperties.RetentionInDays)),e.RequestType==="Create")){let g=new i.CloudWatchLogsClient({logger:console,region:process.env.AWS_REGION});await R(`/aws/lambda/${o.functionName}`,g,a),await y(`/aws/lambda/${o.functionName}`,g,a,1)}e.RequestType==="Delete"&&e.ResourceProperties.RemovalPolicy==="destroy"&&await k(t,c,a),await n("SUCCESS","OK",t)}catch(t){console.log(t),await n("FAILED",t.message,e.ResourceProperties.LogGroupName)}function n(t,r,s){let a=JSON.stringify({Status:t,Reason:r,PhysicalResourceId:s,StackId:e.StackId,RequestId:e.RequestId,LogicalResourceId:e.LogicalResourceId,Data:{LogGroupName:e.ResourceProperties.LogGroupName}});console.log("Responding",a);let m=require("url").parse(e.ResponseURL),c={hostname:m.hostname,path:m.path,method:"PUT",headers:{"content-type":"","content-length":Buffer.byteLength(a,"utf8")}};return new Promise((g,l)=>{try{let u=require("https").request(c,g);u.on("error",l),u.write(a),u.end()}catch(u){l(u)}})}}function L(e,o=10){if(e!==void 0)return parseInt(e,o)}function N(e,o=1e3,n=6e4){return async t=>{let r=0;do try{return await t()}catch(s){if(f("OperationAbortedException",s)||f("ThrottlingException",s))if(r<e){r++,await D(W(r,o,n));continue}else throw new Error("Out of attempts to change log group");throw s}while(!0)}}function f(e,o){return o.name===e||o.message.includes(e)}function W(e,o,n){return Math.min(Math.round(Math.random()*o*2**e),n)}async function D(e){w&&(e=0),await new Promise(o=>setTimeout(o,e))}

View File

@@ -0,0 +1 @@
"use strict";var c=Object.defineProperty,Z=Object.getOwnPropertyDescriptor,N=Object.getOwnPropertyNames,P=Object.prototype.hasOwnProperty,h=(o,e)=>{for(var n in e)c(o,n,{get:e[n],enumerable:!0})},E=(o,e,n,t)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of N(e))!P.call(o,s)&&s!==n&&c(o,s,{get:()=>e[s],enumerable:!(t=Z(e,s))||t.enumerable});return o},A=o=>E(c({},"__esModule",{value:!0}),o),T={};h(T,{handler:()=>w}),module.exports=A(T);var m=require("@aws-sdk/client-route-53"),d=require("@aws-sdk/credential-providers");async function w(o){let e=o.ResourceProperties;switch(o.RequestType){case"Create":return r(e,!1);case"Update":return D(e,o.OldResourceProperties);case"Delete":return r(e,!0)}}async function D(o,e){e&&o.DelegatedZoneName!==e.DelegatedZoneName&&await r(e,!0),await r(o,!1)}async function r(o,e){let{AssumeRoleArn:n,ParentZoneId:t,ParentZoneName:s,DelegatedZoneName:a,DelegatedZoneNameServers:i,TTL:g,AssumeRoleRegion:R}=o;if(!t&&!s)throw Error("One of ParentZoneId or ParentZoneName must be specified");let l=new Date().getTime(),u=new m.Route53({credentials:(0,d.fromTemporaryCredentials)({clientConfig:{region:R??S(process.env.AWS_REGION??process.env.AWS_DEFAULT_REGION??"")},params:{RoleArn:n,RoleSessionName:`cross-account-zone-delegation-${l}`}})}),f=t??await v(s,u);await u.changeResourceRecordSets({HostedZoneId:f,ChangeBatch:{Changes:[{Action:e?"DELETE":"UPSERT",ResourceRecordSet:{Name:a,Type:"NS",TTL:g,ResourceRecords:i.map(p=>({Value:p}))}}]}})}async function v(o,e){let t=(await e.listHostedZonesByName({DNSName:o})).HostedZones?.filter(s=>{let a=s.Name===`${o}.`,i=s.Config?.PrivateZone!==!0;return a&&i})??[];if(t&&t.length!==1)throw Error(`Expected one hosted zone to match the given name but found ${t.length}`);return t[0].Id}function S(o){let e={cn:"cn-northwest-1","us-gov":"us-gov-west-1","us-iso":"us-iso-east-1","us-isob":"us-isob-east-1","eu-isoe":"eu-isoe-west-1","us-isof":"us-isof-south-1","eusc-de":"eusc-de-east-1"};for(let[n,t]of Object.entries(e))if(o.startsWith(`${n}-`))return t;return"us-east-1"}

View File

@@ -0,0 +1,13 @@
import { Construct } from "constructs";
import { CustomResourceProviderBase, CustomResourceProviderOptions } from "../../../core";
export declare class CrossAccountZoneDelegationProvider extends CustomResourceProviderBase {
/**
* Returns a stack-level singleton ARN (service token) for the custom resource provider.
*/
static getOrCreate(scope: Construct, uniqueid: string, props?: CustomResourceProviderOptions): string;
/**
* Returns a stack-level singleton for the custom resource provider.
*/
static getOrCreateProvider(scope: Construct, uniqueid: string, props?: CustomResourceProviderOptions): CrossAccountZoneDelegationProvider;
private constructor();
}

View File

@@ -0,0 +1 @@
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.CrossAccountZoneDelegationProvider=void 0;const path=require("path"),core_1=require("../../../core");class CrossAccountZoneDelegationProvider extends core_1.CustomResourceProviderBase{static getOrCreate(scope,uniqueid,props){return this.getOrCreateProvider(scope,uniqueid,props).serviceToken}static getOrCreateProvider(scope,uniqueid,props){const id=`${uniqueid}CustomResourceProvider`,stack=core_1.Stack.of(scope);return stack.node.tryFindChild(id)??new CrossAccountZoneDelegationProvider(stack,id,props)}constructor(scope,id,props){super(scope,id,{...props,codeDirectory:path.join(__dirname,"cross-account-zone-delegation-handler"),runtimeName:(0,core_1.determineLatestNodeRuntimeName)(scope)}),this.node.addMetadata("aws:cdk:is-custom-resource-handler-customResourceProvider",!0)}}exports.CrossAccountZoneDelegationProvider=CrossAccountZoneDelegationProvider;

View File

@@ -0,0 +1 @@
"use strict";var c=Object.defineProperty,R=Object.getOwnPropertyDescriptor,i=Object.getOwnPropertyNames,u=Object.prototype.hasOwnProperty,T=(r,e)=>{for(var t in e)c(r,t,{get:e[t],enumerable:!0})},p=(r,e,t,s)=>{if(e&&typeof e=="object"||typeof e=="function")for(let o of i(e))!u.call(r,o)&&o!==t&&c(r,o,{get:()=>e[o],enumerable:!(s=R(e,o))||s.enumerable});return r},y=r=>p(c({},"__esModule",{value:!0}),r),f={};T(f,{handler:()=>m}),module.exports=y(f);var n=require("@aws-sdk/client-route-53");async function m(r){let e=r.ResourceProperties;if(r.RequestType!=="Create")return;let t=new n.Route53,o=(await t.listResourceRecordSets({HostedZoneId:e.HostedZoneId,StartRecordName:e.RecordName,StartRecordType:e.RecordType})).ResourceRecordSets?.find(a=>a.Name===e.RecordName&&a.Type===e.RecordType);if(!o)return;let d=await t.changeResourceRecordSets({HostedZoneId:e.HostedZoneId,ChangeBatch:{Changes:[{Action:"DELETE",ResourceRecordSet:g({Name:o.Name,Type:o.Type,TTL:o.TTL,AliasTarget:o.AliasTarget,ResourceRecords:o.ResourceRecords})}]}});return await(0,n.waitUntilResourceRecordSetsChanged)({client:t,maxWaitTime:890},{Id:d?.ChangeInfo?.Id}),{PhysicalResourceId:`${o.Name}-${o.Type}`}}function g(r){let e={};for(let[t,s]of Object.entries(r))s&&(!Array.isArray(s)||s.length!==0)&&(e[t]=s);return e}

View File

@@ -0,0 +1,13 @@
import { Construct } from "constructs";
import { CustomResourceProviderBase, CustomResourceProviderOptions } from "../../../core";
export declare class DeleteExistingRecordSetProvider extends CustomResourceProviderBase {
/**
* Returns a stack-level singleton ARN (service token) for the custom resource provider.
*/
static getOrCreate(scope: Construct, uniqueid: string, props?: CustomResourceProviderOptions): string;
/**
* Returns a stack-level singleton for the custom resource provider.
*/
static getOrCreateProvider(scope: Construct, uniqueid: string, props?: CustomResourceProviderOptions): DeleteExistingRecordSetProvider;
private constructor();
}

View File

@@ -0,0 +1 @@
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.DeleteExistingRecordSetProvider=void 0;const path=require("path"),core_1=require("../../../core");class DeleteExistingRecordSetProvider extends core_1.CustomResourceProviderBase{static getOrCreate(scope,uniqueid,props){return this.getOrCreateProvider(scope,uniqueid,props).serviceToken}static getOrCreateProvider(scope,uniqueid,props){const id=`${uniqueid}CustomResourceProvider`,stack=core_1.Stack.of(scope);return stack.node.tryFindChild(id)??new DeleteExistingRecordSetProvider(stack,id,props)}constructor(scope,id,props){super(scope,id,{...props,codeDirectory:path.join(__dirname,"delete-existing-record-set-handler"),runtimeName:(0,core_1.determineLatestNodeRuntimeName)(scope)}),this.node.addMetadata("aws:cdk:is-custom-resource-handler-customResourceProvider",!0)}}exports.DeleteExistingRecordSetProvider=DeleteExistingRecordSetProvider;

View File

@@ -0,0 +1,406 @@
import contextlib
import json
import logging
import os
import shutil
import subprocess
import tempfile
import urllib.parse
from urllib.request import Request, urlopen
from uuid import uuid4
from zipfile import ZipFile
import boto3
from botocore.config import Config
from botocore.exceptions import WaiterError
logger = logging.getLogger()
logger.setLevel(logging.INFO)
cloudfront = boto3.client('cloudfront', config=Config(
retries = {
'max_attempts': 10,
'mode': 'standard',
}
))
s3 = boto3.client('s3')
CFN_SUCCESS = "SUCCESS"
CFN_FAILED = "FAILED"
ENV_KEY_MOUNT_PATH = "MOUNT_PATH"
ENV_KEY_SKIP_CLEANUP = "SKIP_CLEANUP"
AWS_CLI_CONFIG_FILE = "/tmp/aws_cli_config"
CUSTOM_RESOURCE_OWNER_TAG = "aws-cdk:cr-owned"
os.putenv('AWS_CONFIG_FILE', AWS_CLI_CONFIG_FILE)
def handler(event, context):
def cfn_error(message=None):
if message:
logger.error("| cfn_error: %s" % message.encode())
cfn_send(event, context, CFN_FAILED, reason=message, physicalResourceId=event.get('PhysicalResourceId', None))
try:
# We are not logging ResponseURL as this is a pre-signed S3 URL, and could be used to tamper
# with the response CloudFormation sees from this Custom Resource execution.
logger.info({ key:value for (key, value) in event.items() if key != 'ResponseURL'})
# cloudformation request type (create/update/delete)
request_type = event['RequestType']
# extract resource properties
props = event['ResourceProperties']
old_props = event.get('OldResourceProperties', {})
physical_id = event.get('PhysicalResourceId', None)
try:
source_bucket_names = props['SourceBucketNames']
source_object_keys = props['SourceObjectKeys']
source_markers = props.get('SourceMarkers', None)
source_markers_config = props.get('SourceMarkersConfig', None)
dest_bucket_name = props['DestinationBucketName']
dest_bucket_prefix = props.get('DestinationBucketKeyPrefix', '')
extract = props.get('Extract', 'true') == 'true'
retain_on_delete = props.get('RetainOnDelete', "true") == "true"
distribution_id = props.get('DistributionId', '')
wait_for_distribution_invalidation = props.get('WaitForDistributionInvalidation', True)
user_metadata = props.get('UserMetadata', {})
system_metadata = props.get('SystemMetadata', {})
prune = props.get('Prune', 'true').lower() == 'true'
exclude = props.get('Exclude', [])
include = props.get('Include', [])
sign_content = props.get('SignContent', 'false').lower() == 'true'
output_object_keys = props.get('OutputObjectKeys', 'true') == 'true'
# backwards compatibility - if "SourceMarkers" is not specified,
# assume all sources have an empty market map
if source_markers is None:
source_markers = [{} for i in range(len(source_bucket_names))]
if source_markers_config is None:
source_markers_config = [{} for i in range(len(source_bucket_names))]
default_distribution_path = dest_bucket_prefix
if not default_distribution_path.endswith("/"):
default_distribution_path += "/"
if not default_distribution_path.startswith("/"):
default_distribution_path = "/" + default_distribution_path
default_distribution_path += "*"
distribution_paths = props.get('DistributionPaths', [default_distribution_path])
except KeyError as e:
cfn_error("missing request resource property %s. props: %s" % (str(e), props))
return
# configure aws cli options after resetting back to the defaults for each request
if os.path.exists(AWS_CLI_CONFIG_FILE):
os.remove(AWS_CLI_CONFIG_FILE)
if sign_content:
aws_command("configure", "set", "default.s3.payload_signing_enabled", "true")
# treat "/" as if no prefix was specified
if dest_bucket_prefix == "/":
dest_bucket_prefix = ""
s3_source_zips = list(map(lambda name, key: "s3://%s/%s" % (name, key), source_bucket_names, source_object_keys))
s3_dest = "s3://%s/%s" % (dest_bucket_name, dest_bucket_prefix)
old_s3_dest = "s3://%s/%s" % (old_props.get("DestinationBucketName", ""), old_props.get("DestinationBucketKeyPrefix", ""))
# obviously this is not
if old_s3_dest == "s3:///":
old_s3_dest = None
logger.info("| s3_dest: %s" % sanitize_message(s3_dest))
logger.info("| old_s3_dest: %s" % sanitize_message(old_s3_dest))
# if we are creating a new resource, allocate a physical id for it
# otherwise, we expect physical id to be relayed by cloudformation
if request_type == "Create":
physical_id = "aws.cdk.s3deployment.%s" % str(uuid4())
else:
if not physical_id:
cfn_error("invalid request: request type is '%s' but 'PhysicalResourceId' is not defined" % request_type)
return
# delete or create/update (only if "retain_on_delete" is false)
if request_type == "Delete" and not retain_on_delete:
if not bucket_owned(dest_bucket_name, dest_bucket_prefix):
aws_command("s3", "rm", s3_dest, "--recursive")
# if we are updating without retention and the destination changed, delete first
if request_type == "Update" and not retain_on_delete and old_s3_dest != s3_dest:
if not old_s3_dest:
logger.warn("cannot delete old resource without old resource properties")
return
aws_command("s3", "rm", old_s3_dest, "--recursive")
if request_type == "Update" or request_type == "Create":
s3_deploy(s3_source_zips, s3_dest, user_metadata, system_metadata, prune, exclude, include, source_markers, extract, source_markers_config)
if distribution_id:
cloudfront_invalidate(distribution_id, distribution_paths, wait_for_distribution_invalidation)
cfn_send(event, context, CFN_SUCCESS, physicalResourceId=physical_id, responseData={
# Passing through the ARN sequences dependencees on the deployment
'DestinationBucketArn': props.get('DestinationBucketArn'),
**({'SourceObjectKeys': props.get('SourceObjectKeys')} if output_object_keys else {'SourceObjectKeys': []})
})
except KeyError as e:
cfn_error("invalid request. Missing key %s" % str(e))
except Exception as e:
logger.exception(e)
cfn_error(str(e))
#---------------------------------------------------------------------------------------------------
# Sanitize the message to mitigate CWE-117 and CWE-93 vulnerabilities
def sanitize_message(message):
if not message:
return message
# Sanitize the message to prevent log injection and HTTP response splitting
sanitized_message = message.replace('\n', '').replace('\r', '')
# Encode the message to handle special characters
encoded_message = urllib.parse.quote(sanitized_message)
return encoded_message
#---------------------------------------------------------------------------------------------------
# populate all files from s3_source_zips to a destination bucket
def s3_deploy(s3_source_zips, s3_dest, user_metadata, system_metadata, prune, exclude, include, source_markers, extract, source_markers_config):
# list lengths are equal
if len(s3_source_zips) != len(source_markers):
raise Exception("'source_markers' and 's3_source_zips' must be the same length")
# create a temporary working directory in /tmp or if enabled an attached efs volume
if ENV_KEY_MOUNT_PATH in os.environ:
workdir = os.getenv(ENV_KEY_MOUNT_PATH) + "/" + str(uuid4())
os.mkdir(workdir)
else:
workdir = tempfile.mkdtemp()
logger.info("| workdir: %s" % workdir)
# create a directory into which we extract the contents of the zip file
contents_dir=os.path.join(workdir, 'contents')
os.mkdir(contents_dir)
try:
# download the archive from the source and extract to "contents"
for i in range(len(s3_source_zips)):
s3_source_zip = s3_source_zips[i]
markers = source_markers[i]
markers_config = source_markers_config[i]
if extract:
archive=os.path.join(workdir, str(uuid4()))
logger.info("archive: %s" % archive)
aws_command("s3", "cp", s3_source_zip, archive)
logger.info("| extracting archive to: %s\n" % contents_dir)
logger.info("| markers: %s" % markers)
extract_and_replace_markers(archive, contents_dir, markers, markers_config)
else:
logger.info("| copying archive to: %s\n" % contents_dir)
aws_command("s3", "cp", s3_source_zip, contents_dir)
# sync from "contents" to destination
s3_command = ["s3", "sync"]
if prune:
s3_command.append("--delete")
if exclude:
for filter in exclude:
s3_command.extend(["--exclude", filter])
if include:
for filter in include:
s3_command.extend(["--include", filter])
s3_command.extend([contents_dir, s3_dest])
s3_command.extend(create_metadata_args(user_metadata, system_metadata))
aws_command(*s3_command)
finally:
if not os.getenv(ENV_KEY_SKIP_CLEANUP):
shutil.rmtree(workdir)
#---------------------------------------------------------------------------------------------------
# invalidate files in the CloudFront distribution edge caches
def cloudfront_invalidate(distribution_id, distribution_paths, wait_for_invalidation):
invalidation_resp = cloudfront.create_invalidation(
DistributionId=distribution_id,
InvalidationBatch={
'Paths': {
'Quantity': len(distribution_paths),
'Items': distribution_paths
},
'CallerReference': str(uuid4()),
})
if wait_for_invalidation:
try:
# Wait for a maximum of 13 minutes for invalidation to complete.
cloudfront.get_waiter('invalidation_completed').wait(
DistributionId=distribution_id,
Id=invalidation_resp['Invalidation']['Id'],
WaiterConfig={
'Delay': 20,
'MaxAttempts': (13*60)//20,
}
)
except WaiterError as e:
raise RuntimeError(f"Unable to confirm that cache invalidation was successful. This may be a CloudFront regression as reported in https://github.com/aws/aws-cdk/issues/15891") from e
#---------------------------------------------------------------------------------------------------
# set metadata
def create_metadata_args(raw_user_metadata, raw_system_metadata):
if len(raw_user_metadata) == 0 and len(raw_system_metadata) == 0:
return []
format_system_metadata_key = lambda k: k.lower()
format_user_metadata_key = lambda k: k.lower()
system_metadata = { format_system_metadata_key(k): v for k, v in raw_system_metadata.items() }
user_metadata = { format_user_metadata_key(k): v for k, v in raw_user_metadata.items() }
flatten = lambda l: [item for sublist in l for item in sublist]
system_args = flatten([[f"--{k}", v] for k, v in system_metadata.items()])
user_args = ["--metadata", json.dumps(user_metadata, separators=(',', ':'))] if len(user_metadata) > 0 else []
return system_args + user_args + ["--metadata-directive", "REPLACE"]
#---------------------------------------------------------------------------------------------------
# executes an "aws" cli command
def aws_command(*args):
aws="/opt/awscli/aws" # from AwsCliLayer
logger.info("| aws %s" % ' '.join(args))
subprocess.check_call([aws] + list(args))
#---------------------------------------------------------------------------------------------------
# sends a response to cloudformation
def cfn_send(event, context, responseStatus, responseData={}, physicalResourceId=None, noEcho=False, reason=None):
responseUrl = event['ResponseURL']
responseBody = {}
responseBody['Status'] = responseStatus
responseBody['Reason'] = reason or ('See the details in CloudWatch Log Stream: ' + context.log_stream_name)
responseBody['PhysicalResourceId'] = physicalResourceId or context.log_stream_name
responseBody['StackId'] = event['StackId']
responseBody['RequestId'] = event['RequestId']
responseBody['LogicalResourceId'] = event['LogicalResourceId']
responseBody['NoEcho'] = noEcho
responseBody['Data'] = responseData
body = json.dumps(responseBody)
logger.info("| response body:\n" + body)
headers = {
'content-type' : '',
'content-length' : str(len(body))
}
try:
request = Request(responseUrl, method='PUT', data=bytes(body.encode('utf-8')), headers=headers)
with contextlib.closing(urlopen(request)) as response:
logger.info("| status code: " + response.reason)
except Exception as e:
logger.error("| unable to send response to CloudFormation")
logger.exception(e)
#---------------------------------------------------------------------------------------------------
# check if bucket is owned by a custom resource
# if it is then we don't want to delete content
def bucket_owned(bucketName, keyPrefix):
tag = CUSTOM_RESOURCE_OWNER_TAG
if keyPrefix != "":
tag = tag + ':' + keyPrefix
try:
request = s3.get_bucket_tagging(
Bucket=bucketName,
)
return any((x["Key"].startswith(tag)) for x in request["TagSet"])
except Exception as e:
logger.info("| error getting tags from bucket")
logger.exception(e)
return False
# extract archive and replace markers in output files
def extract_and_replace_markers(archive, contents_dir, markers, markers_config):
with ZipFile(archive, "r") as zip:
zip.extractall(contents_dir)
# replace markers for this source
for file in zip.namelist():
file_path = os.path.join(contents_dir, file)
if os.path.isdir(file_path): continue
replace_markers(file_path, markers, markers_config)
def prepare_json_safe_markers(markers):
"""Pre-process markers to ensure JSON-safe values"""
safe_markers = {}
for key, value in markers.items():
# Serialize the value as JSON to handle escaping if the value is a string
serialized = json.dumps(value)
if serialized.startswith('"') and serialized.endswith('"'):
json_safe_value = json.dumps(value)[1:-1] # Remove surrounding quotes
else:
json_safe_value = serialized
safe_markers[key.encode('utf-8')] = json_safe_value.encode('utf-8')
return safe_markers
def replace_markers(filename, markers, markers_config):
"""Replace markers in a file, with special handling for JSON files."""
# if there are no markers, skip
if not markers:
return
outfile = filename + '.new'
json_escape = markers_config.get('jsonEscape', 'false').lower()
if json_escape == 'true':
replace_tokens = prepare_json_safe_markers(markers)
else:
replace_tokens = dict([(k.encode('utf-8'), v.encode('utf-8')) for k, v in markers.items()])
# Handle content with line-by-line binary replacement
with open(filename, 'rb') as fi, open(outfile, 'wb') as fo:
# Process line by line to handle large files
for line in fi:
for token, replacement in replace_tokens.items():
line = line.replace(token, replacement)
fo.write(line)
# Delete the original file and rename the new one to the original
os.remove(filename)
os.rename(outfile, filename)
def replace_markers_in_json(json_object, replace_tokens):
"""Replace markers in JSON content with proper escaping."""
try:
def replace_in_structure(obj):
if isinstance(obj, str):
# Convert string to bytes for consistent replacement
result = obj.encode('utf-8')
for token, replacement in replace_tokens.items():
result = result.replace(token, replacement)
# Convert back to string
return result.decode('utf-8')
elif isinstance(obj, dict):
return {k: replace_in_structure(v) for k, v in obj.items()}
elif isinstance(obj, list):
return [replace_in_structure(item) for item in obj]
return obj
# Process the whole structure
processed = replace_in_structure(json_object)
return json.dumps(processed)
except Exception as e:
logger.error(f'Error processing JSON: {e}')
logger.exception(e)
return json_object

View File

@@ -0,0 +1,27 @@
import { Construct } from "constructs";
import * as lambda from "../../../aws-lambda";
export declare class BucketDeploymentSingletonFunction extends lambda.SingletonFunction {
constructor(scope: Construct, id: string, props: BucketDeploymentSingletonFunctionProps);
}
/**
* Initialization properties for BucketDeploymentSingletonFunction
*/
export interface BucketDeploymentSingletonFunctionProps extends lambda.FunctionOptions {
/**
* A unique identifier to identify this Lambda.
*
* The identifier should be unique across all custom resource providers.
* We recommend generating a UUID per provider.
*/
readonly uuid: string;
/**
* A descriptive name for the purpose of this Lambda.
*
* If the Lambda does not have a physical name, this string will be
* reflected in its generated name. The combination of lambdaPurpose
* and uuid must be unique.
*
* @default SingletonLambda
*/
readonly lambdaPurpose?: string;
}

View File

@@ -0,0 +1 @@
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.BucketDeploymentSingletonFunction=void 0;const path=require("path"),lambda=require("../../../aws-lambda");class BucketDeploymentSingletonFunction extends lambda.SingletonFunction{constructor(scope,id,props){super(scope,id,{...props,code:lambda.Code.fromAsset(path.join(__dirname,"bucket-deployment-handler")),handler:"index.handler",runtime:lambda.Runtime.PYTHON_3_13}),this.addMetadata("aws:cdk:is-custom-resource-handler-singleton",!0),this.addMetadata("aws:cdk:is-custom-resource-handler-runtime-family",this.runtime.family),props?.logGroup&&this.logGroup.node.addMetadata("aws:cdk:is-custom-resource-handler-logGroup",!0),props?.logRetention&&this.lambdaFunction._logRetention?.node.addMetadata("aws:cdk:is-custom-resource-handler-logRetention",!0)}}exports.BucketDeploymentSingletonFunction=BucketDeploymentSingletonFunction;

View File

@@ -0,0 +1 @@
"use strict";var f=Object.create,i=Object.defineProperty,I=Object.getOwnPropertyDescriptor,C=Object.getOwnPropertyNames,w=Object.getPrototypeOf,P=Object.prototype.hasOwnProperty,A=(t,e)=>{for(var o in e)i(t,o,{get:e[o],enumerable:!0})},d=(t,e,o,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of C(e))!P.call(t,s)&&s!==o&&i(t,s,{get:()=>e[s],enumerable:!(r=I(e,s))||r.enumerable});return t},l=(t,e,o)=>(o=t!=null?f(w(t)):{},d(e||!t||!t.__esModule?i(o,"default",{value:t,enumerable:!0}):o,t)),B=t=>d(i({},"__esModule",{value:!0}),t),q={};A(q,{autoDeleteHandler:()=>S,handler:()=>H}),module.exports=B(q);var h=require("@aws-sdk/client-s3"),y=l(require("https")),m=l(require("url")),a={sendHttpRequest:D,log:T,includeStackTraces:!0,userHandlerIndex:"./index"},p="AWSCDK::CustomResourceProviderFramework::CREATE_FAILED",L="AWSCDK::CustomResourceProviderFramework::MISSING_PHYSICAL_ID";function R(t){return async(e,o)=>{let r={...e,ResponseURL:"..."};if(a.log(JSON.stringify(r,void 0,2)),e.RequestType==="Delete"&&e.PhysicalResourceId===p){a.log("ignoring DELETE event caused by a failed CREATE event"),await u("SUCCESS",e);return}try{let s=await t(r,o),n=k(e,s);await u("SUCCESS",n)}catch(s){let n={...e,Reason:a.includeStackTraces?s.stack:s.message};n.PhysicalResourceId||(e.RequestType==="Create"?(a.log("CREATE failed, responding with a marker physical resource id so that the subsequent DELETE will be ignored"),n.PhysicalResourceId=p):a.log(`ERROR: Malformed event. "PhysicalResourceId" is required: ${JSON.stringify(e)}`)),await u("FAILED",n)}}}function k(t,e={}){let o=e.PhysicalResourceId??t.PhysicalResourceId??t.RequestId;if(t.RequestType==="Delete"&&o!==t.PhysicalResourceId)throw new Error(`DELETE: cannot change the physical resource ID from "${t.PhysicalResourceId}" to "${e.PhysicalResourceId}" during deletion`);return{...t,...e,PhysicalResourceId:o}}async function u(t,e){let o={Status:t,Reason:e.Reason??t,StackId:e.StackId,RequestId:e.RequestId,PhysicalResourceId:e.PhysicalResourceId||L,LogicalResourceId:e.LogicalResourceId,NoEcho:e.NoEcho,Data:e.Data},r=m.parse(e.ResponseURL),s=`${r.protocol}//${r.hostname}/${r.pathname}?***`;a.log("submit response to cloudformation",s,o);let n=JSON.stringify(o),E={hostname:r.hostname,path:r.path,method:"PUT",headers:{"content-type":"","content-length":Buffer.byteLength(n,"utf8")}};await O({attempts:5,sleep:1e3},a.sendHttpRequest)(E,n)}async function D(t,e){return new Promise((o,r)=>{try{let s=y.request(t,n=>{n.resume(),!n.statusCode||n.statusCode>=400?r(new Error(`Unsuccessful HTTP response: ${n.statusCode}`)):o()});s.on("error",r),s.write(e),s.end()}catch(s){r(s)}})}function T(t,...e){console.log(t,...e)}function O(t,e){return async(...o)=>{let r=t.attempts,s=t.sleep;for(;;)try{return await e(...o)}catch(n){if(r--<=0)throw n;await b(Math.floor(Math.random()*s)),s*=2}}}async function b(t){return new Promise(e=>setTimeout(e,t))}var g="aws-cdk:auto-delete-objects",x=JSON.stringify({Version:"2012-10-17",Statement:[]}),c=new h.S3({}),H=R(S);async function S(t){switch(t.RequestType){case"Create":return;case"Update":return{PhysicalResourceId:(await F(t)).PhysicalResourceId};case"Delete":return N(t.ResourceProperties?.BucketName)}}async function F(t){let e=t,o=e.OldResourceProperties?.BucketName;return{PhysicalResourceId:e.ResourceProperties?.BucketName??o}}async function _(t){try{let e=(await c.getBucketPolicy({Bucket:t}))?.Policy??x,o=JSON.parse(e);o.Statement.push({Principal:"*",Effect:"Deny",Action:["s3:PutObject"],Resource:[`arn:aws:s3:::${t}/*`]}),await c.putBucketPolicy({Bucket:t,Policy:JSON.stringify(o)})}catch(e){if(e.name==="NoSuchBucket")throw e;console.log(`Could not set new object deny policy on bucket '${t}' prior to deletion.`)}}async function U(t){let e;do{e=await c.listObjectVersions({Bucket:t});let o=[...e.Versions??[],...e.DeleteMarkers??[]];if(o.length===0)return;let r=o.map(s=>({Key:s.Key,VersionId:s.VersionId}));await c.deleteObjects({Bucket:t,Delete:{Objects:r}})}while(e?.IsTruncated)}async function N(t){if(!t)throw new Error("No BucketName was provided.");try{if(!await W(t)){console.log(`Bucket does not have '${g}' tag, skipping cleaning.`);return}await _(t),await U(t)}catch(e){if(e.name==="NoSuchBucket"){console.log(`Bucket '${t}' does not exist.`);return}throw e}}async function W(t){return(await c.getBucketTagging({Bucket:t})).TagSet?.some(o=>o.Key===g&&o.Value==="true")}

View File

@@ -0,0 +1,13 @@
import { Construct } from "constructs";
import { CustomResourceProviderBase, CustomResourceProviderOptions } from "../../../core";
export declare class AutoDeleteObjectsProvider extends CustomResourceProviderBase {
/**
* Returns a stack-level singleton ARN (service token) for the custom resource provider.
*/
static getOrCreate(scope: Construct, uniqueid: string, props?: CustomResourceProviderOptions): string;
/**
* Returns a stack-level singleton for the custom resource provider.
*/
static getOrCreateProvider(scope: Construct, uniqueid: string, props?: CustomResourceProviderOptions): AutoDeleteObjectsProvider;
private constructor();
}

View File

@@ -0,0 +1 @@
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.AutoDeleteObjectsProvider=void 0;const path=require("path"),core_1=require("../../../core");class AutoDeleteObjectsProvider extends core_1.CustomResourceProviderBase{static getOrCreate(scope,uniqueid,props){return this.getOrCreateProvider(scope,uniqueid,props).serviceToken}static getOrCreateProvider(scope,uniqueid,props){const id=`${uniqueid}CustomResourceProvider`,stack=core_1.Stack.of(scope);return stack.node.tryFindChild(id)??new AutoDeleteObjectsProvider(stack,id,props)}constructor(scope,id,props){super(scope,id,{...props,codeDirectory:path.join(__dirname,"auto-delete-objects-handler"),runtimeName:(0,core_1.determineLatestNodeRuntimeName)(scope)}),this.node.addMetadata("aws:cdk:is-custom-resource-handler-customResourceProvider",!0)}}exports.AutoDeleteObjectsProvider=AutoDeleteObjectsProvider;

View File

@@ -0,0 +1,123 @@
import boto3 # type: ignore
import json
import logging
import urllib.request
s3 = boto3.client("s3")
EVENTBRIDGE_CONFIGURATION = 'EventBridgeConfiguration'
CONFIGURATION_TYPES = ["TopicConfigurations", "QueueConfigurations", "LambdaFunctionConfigurations"]
def handler(event: dict, context):
response_status = "SUCCESS"
error_message = ""
try:
props = event["ResourceProperties"]
notification_configuration = props["NotificationConfiguration"]
managed = props.get('Managed', 'true').lower() == 'true'
skipDestinationValidation = props.get('SkipDestinationValidation', 'false').lower() == 'true'
stack_id = event['StackId']
old = event.get("OldResourceProperties", {}).get("NotificationConfiguration", {})
if managed:
config = handle_managed(event["RequestType"], notification_configuration)
else:
config = handle_unmanaged(props["BucketName"], stack_id, event["RequestType"], notification_configuration, old)
s3.put_bucket_notification_configuration(Bucket=props["BucketName"], NotificationConfiguration=config, SkipDestinationValidation=skipDestinationValidation)
except Exception as e:
logging.exception("Failed to put bucket notification configuration")
response_status = "FAILED"
error_message = f"Error: {str(e)}. "
finally:
submit_response(event, context, response_status, error_message)
def handle_managed(request_type, notification_configuration):
if request_type == 'Delete':
return {}
return notification_configuration
def handle_unmanaged(bucket, stack_id, request_type, notification_configuration, old):
def get_id(n):
n['Id'] = ''
sorted_notifications = sort_filter_rules(n)
strToHash=json.dumps(sorted_notifications, sort_keys=True).replace('"Name": "prefix"', '"Name": "Prefix"').replace('"Name": "suffix"', '"Name": "Suffix"')
return f"{stack_id}-{hash(strToHash)}"
def with_id(n):
n['Id'] = get_id(n)
return n
# find external notifications
external_notifications = {}
existing_notifications = s3.get_bucket_notification_configuration(Bucket=bucket)
for t in CONFIGURATION_TYPES:
if request_type == 'Update':
old_incoming_ids = [get_id(n) for n in old.get(t, [])]
# if the notification was created by us, we know what id to expect so we can filter by it.
external_notifications[t] = [n for n in existing_notifications.get(t, []) if not get_id(n) in old_incoming_ids]
elif request_type == 'Delete':
# For 'Delete' request, old parameter is an empty dict so we cannot use this to determine which are external
# notifications. Fall back to rely on the stack naming logic.
external_notifications[t] = [n for n in existing_notifications.get(t, []) if not n['Id'].startswith(f"{stack_id}-")]
elif request_type == 'Create':
# if this is a create event then all existing notifications are external
external_notifications[t] = [n for n in existing_notifications.get(t, [])]
# always treat EventBridge configuration as an external config if it already exists
# as there is no way to determine whether it's managed by us or not
if EVENTBRIDGE_CONFIGURATION in existing_notifications:
external_notifications[EVENTBRIDGE_CONFIGURATION] = existing_notifications[EVENTBRIDGE_CONFIGURATION]
# if delete, that's all we need
if request_type == 'Delete':
return external_notifications
# otherwise, merge external with incoming config and augment with id
notifications = {}
for t in CONFIGURATION_TYPES:
external = external_notifications.get(t, [])
incoming = [with_id(n) for n in notification_configuration.get(t, [])]
notifications[t] = external + incoming
# EventBridge configuration is a special case because it's just an empty object if it exists
if EVENTBRIDGE_CONFIGURATION in notification_configuration:
notifications[EVENTBRIDGE_CONFIGURATION] = notification_configuration[EVENTBRIDGE_CONFIGURATION]
elif EVENTBRIDGE_CONFIGURATION in external_notifications:
notifications[EVENTBRIDGE_CONFIGURATION] = external_notifications[EVENTBRIDGE_CONFIGURATION]
return notifications
def submit_response(event: dict, context, response_status: str, error_message: str):
response_body = json.dumps(
{
"Status": response_status,
"Reason": f"{error_message}See the details in CloudWatch Log Stream: {context.log_stream_name}",
"PhysicalResourceId": event.get("PhysicalResourceId") or event["LogicalResourceId"],
"StackId": event["StackId"],
"RequestId": event["RequestId"],
"LogicalResourceId": event["LogicalResourceId"],
"NoEcho": False,
}
).encode("utf-8")
headers = {"content-type": "", "content-length": str(len(response_body))}
try:
req = urllib.request.Request(url=event["ResponseURL"], headers=headers, data=response_body, method="PUT")
with urllib.request.urlopen(req) as response:
print(response.read().decode("utf-8"))
print("Status code: " + response.reason)
except Exception as e:
print("send(..) failed executing request.urlopen(..): " + str(e))
def sort_filter_rules(json_obj):
# Check if the input is a dictionary
if not isinstance(json_obj, dict):
return json_obj
# Recursively sort the filter rules for nested dictionaries
for key, value in json_obj.items():
if isinstance(value, dict):
json_obj[key] = sort_filter_rules(value)
elif isinstance(value, list):
json_obj[key] = [sort_filter_rules(item) for item in value]
# Sort the FilterRules list if it exists
if "Filter" in json_obj and "Key" in json_obj["Filter"] and "FilterRules" in json_obj["Filter"]["Key"]:
filter_rules = json_obj["Filter"]["Key"]["FilterRules"]
sorted_filter_rules = sorted(filter_rules, key=lambda x: x["Name"])
json_obj["Filter"]["Key"]["FilterRules"] = sorted_filter_rules
return json_obj

View File

@@ -0,0 +1 @@
"use strict";var o=Object.defineProperty,n=Object.getOwnPropertyDescriptor,c=Object.getOwnPropertyNames,a=Object.prototype.hasOwnProperty,p=(i,t)=>{for(var e in t)o(i,e,{get:t[e],enumerable:!0})},l=(i,t,e,r)=>{if(t&&typeof t=="object"||typeof t=="function")for(let s of c(t))!a.call(i,s)&&s!==e&&o(i,s,{get:()=>t[s],enumerable:!(r=n(t,s))||r.enumerable});return i},d=i=>l(o({},"__esModule",{value:!0}),i),S={};p(S,{handler:()=>u}),module.exports=d(S);async function u(i){console.log("Spam filter");let t=i.Records[0].ses;return console.log("SES Notification: %j",t),t.receipt.spfVerdict.status==="FAIL"||t.receipt.dkimVerdict.status==="FAIL"||t.receipt.spamVerdict.status==="FAIL"||t.receipt.virusVerdict.status==="FAIL"?(console.log("Dropping spam"),{disposition:"STOP_RULE_SET"}):null}

View File

@@ -0,0 +1,27 @@
import { Construct } from "constructs";
import * as lambda from "../../../aws-lambda";
export declare class DropSpamSingletonFunction extends lambda.SingletonFunction {
constructor(scope: Construct, id: string, props: DropSpamSingletonFunctionProps);
}
/**
* Initialization properties for DropSpamSingletonFunction
*/
export interface DropSpamSingletonFunctionProps extends lambda.FunctionOptions {
/**
* A unique identifier to identify this Lambda.
*
* The identifier should be unique across all custom resource providers.
* We recommend generating a UUID per provider.
*/
readonly uuid: string;
/**
* A descriptive name for the purpose of this Lambda.
*
* If the Lambda does not have a physical name, this string will be
* reflected in its generated name. The combination of lambdaPurpose
* and uuid must be unique.
*
* @default SingletonLambda
*/
readonly lambdaPurpose?: string;
}

View File

@@ -0,0 +1 @@
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.DropSpamSingletonFunction=void 0;const path=require("path"),lambda=require("../../../aws-lambda");class DropSpamSingletonFunction extends lambda.SingletonFunction{constructor(scope,id,props){super(scope,id,{...props,code:lambda.Code.fromAsset(path.join(__dirname,"drop-spam-handler")),handler:"index.handler",runtime:lambda.determineLatestNodeRuntime(scope)}),this.addMetadata("aws:cdk:is-custom-resource-handler-singleton",!0),this.addMetadata("aws:cdk:is-custom-resource-handler-runtime-family",this.runtime.family),props?.logGroup&&this.logGroup.node.addMetadata("aws:cdk:is-custom-resource-handler-logGroup",!0),props?.logRetention&&this.lambdaFunction._logRetention?.node.addMetadata("aws:cdk:is-custom-resource-handler-logRetention",!0)}}exports.DropSpamSingletonFunction=DropSpamSingletonFunction;

View File

@@ -0,0 +1 @@
"use strict";var i=Object.defineProperty,l=Object.getOwnPropertyDescriptor,m=Object.getOwnPropertyNames,f=Object.prototype.hasOwnProperty,g=(e,n)=>{for(var r in n)i(e,r,{get:n[r],enumerable:!0})},y=(e,n,r,a)=>{if(n&&typeof n=="object"||typeof n=="function")for(let o of m(n))!f.call(e,o)&&o!==r&&i(e,o,{get:()=>n[o],enumerable:!(a=l(n,o))||a.enumerable});return e},p=e=>y(i({},"__esModule",{value:!0}),e),P={};g(P,{handler:()=>u}),module.exports=p(P);function w(e){let n=Object.entries(e).filter(([r])=>r.endsWith("Client")&&r!=="__Client");if(n.length==0)throw new Error("There is no *Client class in the package.");if(n.length>1)throw new Error(`There are more than one *Client classes in the package: ${n.map(r=>r[0]).join(",")}`);return n[0][1]}function h(e){return e.charAt(0).toUpperCase()+e.slice(1)}function C(e,n){let r=`${h(n)}Command`,a=Object.entries(e).find(([o])=>o.toLowerCase()===r.toLowerCase())?.[1];if(!a)throw new Error(`Unable to find command named: ${r} for action: ${n} in service package`);return a}var u=async e=>{console.log("Event: ",e);try{let n=require(`@aws-sdk/client-${e.service}`),r=w(n),a=C(n,e.action),o=new r({region:e.region,endpoint:e.endpoint}),c=new a(e.parameters??{}),t=await o.send(c);if(t.Payload&&(t.Payload instanceof Uint8Array||t.Payload instanceof Buffer))try{let s=new TextDecoder().decode(t.Payload);t.Payload=JSON.parse(s)}catch{t.Payload=new TextDecoder().decode(t.Payload)}let d=JSON.stringify(t);return JSON.parse(d)}catch(n){throw console.error("Error: ",n),n}};

View File

@@ -0,0 +1,27 @@
import { Construct } from "constructs";
import * as lambda from "../../../aws-lambda";
export declare class CrossRegionAwsSdkSingletonFunction extends lambda.SingletonFunction {
constructor(scope: Construct, id: string, props: CrossRegionAwsSdkSingletonFunctionProps);
}
/**
* Initialization properties for CrossRegionAwsSdkSingletonFunction
*/
export interface CrossRegionAwsSdkSingletonFunctionProps extends lambda.FunctionOptions {
/**
* A unique identifier to identify this Lambda.
*
* The identifier should be unique across all custom resource providers.
* We recommend generating a UUID per provider.
*/
readonly uuid: string;
/**
* A descriptive name for the purpose of this Lambda.
*
* If the Lambda does not have a physical name, this string will be
* reflected in its generated name. The combination of lambdaPurpose
* and uuid must be unique.
*
* @default SingletonLambda
*/
readonly lambdaPurpose?: string;
}

View File

@@ -0,0 +1 @@
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.CrossRegionAwsSdkSingletonFunction=void 0;const path=require("path"),lambda=require("../../../aws-lambda");class CrossRegionAwsSdkSingletonFunction extends lambda.SingletonFunction{constructor(scope,id,props){super(scope,id,{...props,code:lambda.Code.fromAsset(path.join(__dirname,"cross-region-aws-sdk-handler")),handler:"index.handler",runtime:lambda.determineLatestNodeRuntime(scope)}),this.addMetadata("aws:cdk:is-custom-resource-handler-singleton",!0),this.addMetadata("aws:cdk:is-custom-resource-handler-runtime-family",this.runtime.family),props?.logGroup&&this.logGroup.node.addMetadata("aws:cdk:is-custom-resource-handler-logGroup",!0),props?.logRetention&&this.lambdaFunction._logRetention?.node.addMetadata("aws:cdk:is-custom-resource-handler-logRetention",!0)}}exports.CrossRegionAwsSdkSingletonFunction=CrossRegionAwsSdkSingletonFunction;

View File

@@ -0,0 +1 @@
"use strict";var o=Object.defineProperty,i=Object.getOwnPropertyDescriptor,a=Object.getOwnPropertyNames,p=Object.prototype.hasOwnProperty,c=(e,n)=>{for(var s in n)o(e,s,{get:n[s],enumerable:!0})},l=(e,n,s,t)=>{if(n&&typeof n=="object"||typeof n=="function")for(let r of a(n))!p.call(e,r)&&r!==s&&o(e,r,{get:()=>n[r],enumerable:!(t=i(n,r))||t.enumerable});return e},g=e=>l(o({},"__esModule",{value:!0}),e),y={};c(y,{handler:()=>u}),module.exports=g(y);function x(e){return e.replace(/[-\/\\^$*+?.()|[\]{}]/g,"\\$&")}async function u(e){console.log("Event: %j",{...e,ResponseURL:"..."});let n=Object.entries(e.expressionAttributeValues).reduce((s,[t,r])=>s.replace(new RegExp(x(t),"g"),JSON.stringify(r)),e.expression);return console.log(`Expression: ${n}`),[eval][0](n)}

View File

@@ -0,0 +1,33 @@
import { Construct } from "constructs";
import * as lambda from "../../../aws-lambda";
export declare class EvalNodejsSingletonFunction extends lambda.SingletonFunction {
constructor(scope: Construct, id: string, props: EvalNodejsSingletonFunctionProps);
}
/**
* Initialization properties for EvalNodejsSingletonFunction
*/
export interface EvalNodejsSingletonFunctionProps extends lambda.FunctionOptions {
/**
* A unique identifier to identify this Lambda.
*
* The identifier should be unique across all custom resource providers.
* We recommend generating a UUID per provider.
*/
readonly uuid: string;
/**
* A descriptive name for the purpose of this Lambda.
*
* If the Lambda does not have a physical name, this string will be
* reflected in its generated name. The combination of lambdaPurpose
* and uuid must be unique.
*
* @default SingletonLambda
*/
readonly lambdaPurpose?: string;
/**
* The runtime that this Lambda will use.
*
* @default - the latest Lambda node runtime available in your region.
*/
readonly runtime?: lambda.Runtime;
}

View File

@@ -0,0 +1 @@
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.EvalNodejsSingletonFunction=void 0;const path=require("path"),lambda=require("../../../aws-lambda");class EvalNodejsSingletonFunction extends lambda.SingletonFunction{constructor(scope,id,props){super(scope,id,{...props,code:lambda.Code.fromAsset(path.join(__dirname,"eval-nodejs-handler")),handler:"index.handler",runtime:props.runtime?props.runtime:lambda.determineLatestNodeRuntime(scope)}),this.addMetadata("aws:cdk:is-custom-resource-handler-singleton",!0),this.addMetadata("aws:cdk:is-custom-resource-handler-runtime-family",this.runtime.family),props?.logGroup&&this.logGroup.node.addMetadata("aws:cdk:is-custom-resource-handler-logGroup",!0),props?.logRetention&&this.lambdaFunction._logRetention?.node.addMetadata("aws:cdk:is-custom-resource-handler-logRetention",!0)}}exports.EvalNodejsSingletonFunction=EvalNodejsSingletonFunction;

View File

@@ -0,0 +1,16 @@
import subprocess as sp
import os
import logging
#https://github.com/aws/aws-cdk/tree/main/packages/%40aws-cdk/aws-stepfunctions#custom-state
def handler(event, context):
logger = logging.getLogger()
logger.setLevel(logging.INFO)
command = ["/opt/awscli/aws", "emr-containers", "update-role-trust-policy", "--cluster-name", f"{event['ResourceProperties']['eksClusterId']}", "--namespace", f"{event['ResourceProperties']['eksNamespace']}", "--role-name", f"{event['ResourceProperties']['roleName']}"]
if event['RequestType'] == 'Create' or event['RequestType'] == 'Update' :
try:
res = sp.check_output(command)
logger.info(f"Successfully ran {command}")
except Exception as e:
logger.info(f"ERROR: {str(e)}")

View File

@@ -0,0 +1,27 @@
import { Construct } from "constructs";
import * as lambda from "../../../aws-lambda";
export declare class RolePolicySingletonFunction extends lambda.SingletonFunction {
constructor(scope: Construct, id: string, props: RolePolicySingletonFunctionProps);
}
/**
* Initialization properties for RolePolicySingletonFunction
*/
export interface RolePolicySingletonFunctionProps extends lambda.FunctionOptions {
/**
* A unique identifier to identify this Lambda.
*
* The identifier should be unique across all custom resource providers.
* We recommend generating a UUID per provider.
*/
readonly uuid: string;
/**
* A descriptive name for the purpose of this Lambda.
*
* If the Lambda does not have a physical name, this string will be
* reflected in its generated name. The combination of lambdaPurpose
* and uuid must be unique.
*
* @default SingletonLambda
*/
readonly lambdaPurpose?: string;
}

View File

@@ -0,0 +1 @@
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.RolePolicySingletonFunction=void 0;const path=require("path"),lambda=require("../../../aws-lambda");class RolePolicySingletonFunction extends lambda.SingletonFunction{constructor(scope,id,props){super(scope,id,{...props,code:lambda.Code.fromAsset(path.join(__dirname,"role-policy-handler")),handler:"index.handler",runtime:lambda.Runtime.PYTHON_3_13}),this.addMetadata("aws:cdk:is-custom-resource-handler-singleton",!0),this.addMetadata("aws:cdk:is-custom-resource-handler-runtime-family",this.runtime.family),props?.logGroup&&this.logGroup.node.addMetadata("aws:cdk:is-custom-resource-handler-logGroup",!0),props?.logRetention&&this.lambdaFunction._logRetention?.node.addMetadata("aws:cdk:is-custom-resource-handler-logRetention",!0)}}exports.RolePolicySingletonFunction=RolePolicySingletonFunction;

View File

@@ -0,0 +1 @@
"use strict";var I=Object.create,i=Object.defineProperty,g=Object.getOwnPropertyDescriptor,S=Object.getOwnPropertyNames,w=Object.getPrototypeOf,A=Object.prototype.hasOwnProperty,P=(o,e)=>{for(var t in e)i(o,t,{get:e[t],enumerable:!0})},l=(o,e,t,n)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of S(e))!A.call(o,s)&&s!==t&&i(o,s,{get:()=>e[s],enumerable:!(n=g(e,s))||n.enumerable});return o},m=(o,e,t)=>(t=o!=null?I(w(o)):{},l(e||!o||!o.__esModule?i(t,"default",{value:o,enumerable:!0}):t,o)),L=o=>l(i({},"__esModule",{value:!0}),o),W={};P(W,{autoDeleteHandler:()=>E,handler:()=>_}),module.exports=L(W);var c=require("@aws-sdk/client-lambda"),u=require("@aws-sdk/client-synthetics"),y=m(require("https")),R=m(require("url")),a={sendHttpRequest:T,log:F,includeStackTraces:!0,userHandlerIndex:"./index"},p="AWSCDK::CustomResourceProviderFramework::CREATE_FAILED",D="AWSCDK::CustomResourceProviderFramework::MISSING_PHYSICAL_ID";function C(o){return async(e,t)=>{let n={...e,ResponseURL:"..."};if(a.log(JSON.stringify(n,void 0,2)),e.RequestType==="Delete"&&e.PhysicalResourceId===p){a.log("ignoring DELETE event caused by a failed CREATE event"),await d("SUCCESS",e);return}try{let s=await o(n,t),r=b(e,s);await d("SUCCESS",r)}catch(s){let r={...e,Reason:a.includeStackTraces?s.stack:s.message};r.PhysicalResourceId||(e.RequestType==="Create"?(a.log("CREATE failed, responding with a marker physical resource id so that the subsequent DELETE will be ignored"),r.PhysicalResourceId=p):a.log(`ERROR: Malformed event. "PhysicalResourceId" is required: ${JSON.stringify(e)}`)),await d("FAILED",r)}}}function b(o,e={}){let t=e.PhysicalResourceId??o.PhysicalResourceId??o.RequestId;if(o.RequestType==="Delete"&&t!==o.PhysicalResourceId)throw new Error(`DELETE: cannot change the physical resource ID from "${o.PhysicalResourceId}" to "${e.PhysicalResourceId}" during deletion`);return{...o,...e,PhysicalResourceId:t}}async function d(o,e){let t={Status:o,Reason:e.Reason??o,StackId:e.StackId,RequestId:e.RequestId,PhysicalResourceId:e.PhysicalResourceId||D,LogicalResourceId:e.LogicalResourceId,NoEcho:e.NoEcho,Data:e.Data},n=R.parse(e.ResponseURL),s=`${n.protocol}//${n.hostname}/${n.pathname}?***`;a.log("submit response to cloudformation",s,t);let r=JSON.stringify(t),f={hostname:n.hostname,path:n.path,method:"PUT",headers:{"content-type":"","content-length":Buffer.byteLength(r,"utf8")}};await x({attempts:5,sleep:1e3},a.sendHttpRequest)(f,r)}async function T(o,e){return new Promise((t,n)=>{try{let s=y.request(o,r=>{r.resume(),!r.statusCode||r.statusCode>=400?n(new Error(`Unsuccessful HTTP response: ${r.statusCode}`)):t()});s.on("error",n),s.write(e),s.end()}catch(s){n(s)}})}function F(o,...e){console.log(o,...e)}function x(o,e){return async(...t)=>{let n=o.attempts,s=o.sleep;for(;;)try{return await e(...t)}catch(r){if(n--<=0)throw r;await N(Math.floor(Math.random()*s)),s*=2}}}async function N(o){return new Promise(e=>setTimeout(e,o))}var h="aws-cdk:auto-delete-underlying-resources",H=new c.LambdaClient({}),U=new u.SyntheticsClient({}),_=C(E);async function E(o){switch(o.RequestType){case"Create":return{PhyscialResourceId:o.ResourceProperties?.CanaryName};case"Update":return{PhysicalResourceId:(await k(o)).PhysicalResourceId};case"Delete":return q(o.ResourceProperties?.CanaryName)}}async function k(o){return{PhysicalResourceId:o.ResourceProperties?.CanaryName}}async function q(o){if(console.log(`Deleting lambda function associated with ${o}`),!o)throw new Error("No CanaryName was provided.");try{let e=await U.send(new u.GetCanaryCommand({Name:o}));if(e.Canary===void 0||e.Canary.Id===void 0||e.Canary.EngineArn===void 0)return;if(!O(e.Canary.Tags)){console.log(`Canary does not have '${h}' tag, skipping deletion.`);return}let t=e.Canary.EngineArn.split(":");t.at(-1)?.includes(e.Canary.Id)||t.pop();let n=t.join(":");console.log(`Deleting lambda ${n}`),await H.send(new c.DeleteFunctionCommand({FunctionName:n}))}catch(e){if(e.name!=="ResourceNotFoundException")throw e}}function O(o){return o?Object.keys(o).some(e=>e===h&&o[e]==="true"):!1}

View File

@@ -0,0 +1,13 @@
import { Construct } from "constructs";
import { CustomResourceProviderBase, CustomResourceProviderOptions } from "../../../core";
export declare class AutoDeleteUnderlyingResourcesProvider extends CustomResourceProviderBase {
/**
* Returns a stack-level singleton ARN (service token) for the custom resource provider.
*/
static getOrCreate(scope: Construct, uniqueid: string, props?: CustomResourceProviderOptions): string;
/**
* Returns a stack-level singleton for the custom resource provider.
*/
static getOrCreateProvider(scope: Construct, uniqueid: string, props?: CustomResourceProviderOptions): AutoDeleteUnderlyingResourcesProvider;
private constructor();
}

View File

@@ -0,0 +1 @@
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.AutoDeleteUnderlyingResourcesProvider=void 0;const path=require("path"),core_1=require("../../../core");class AutoDeleteUnderlyingResourcesProvider extends core_1.CustomResourceProviderBase{static getOrCreate(scope,uniqueid,props){return this.getOrCreateProvider(scope,uniqueid,props).serviceToken}static getOrCreateProvider(scope,uniqueid,props){const id=`${uniqueid}CustomResourceProvider`,stack=core_1.Stack.of(scope);return stack.node.tryFindChild(id)??new AutoDeleteUnderlyingResourcesProvider(stack,id,props)}constructor(scope,id,props){super(scope,id,{...props,codeDirectory:path.join(__dirname,"auto-delete-underlying-resources-handler"),runtimeName:(0,core_1.determineLatestNodeRuntimeName)(scope)}),this.node.addMetadata("aws:cdk:is-custom-resource-handler-customResourceProvider",!0)}}exports.AutoDeleteUnderlyingResourcesProvider=AutoDeleteUnderlyingResourcesProvider;

View File

@@ -0,0 +1 @@
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.external=void 0,exports.handler=handler,exports.withRetries=withRetries;const https=require("https"),url=require("url");exports.external={sendHttpRequest:defaultSendHttpRequest,log:defaultLog,includeStackTraces:!0,userHandlerIndex:"./index"};const CREATE_FAILED_PHYSICAL_ID_MARKER="AWSCDK::CustomResourceProviderFramework::CREATE_FAILED",MISSING_PHYSICAL_ID_MARKER="AWSCDK::CustomResourceProviderFramework::MISSING_PHYSICAL_ID";async function handler(event,context){const sanitizedEvent={...event,ResponseURL:"..."};if(exports.external.log(JSON.stringify(sanitizedEvent,void 0,2)),event.RequestType==="Delete"&&event.PhysicalResourceId===CREATE_FAILED_PHYSICAL_ID_MARKER){exports.external.log("ignoring DELETE event caused by a failed CREATE event"),await submitResponse("SUCCESS",event);return}try{const userHandler=require(exports.external.userHandlerIndex).handler,result=await userHandler(sanitizedEvent,context),responseEvent=renderResponse(event,result);await submitResponse("SUCCESS",responseEvent)}catch(e){const resp={...event,Reason:exports.external.includeStackTraces?e.stack:e.message};resp.PhysicalResourceId||(event.RequestType==="Create"?(exports.external.log("CREATE failed, responding with a marker physical resource id so that the subsequent DELETE will be ignored"),resp.PhysicalResourceId=CREATE_FAILED_PHYSICAL_ID_MARKER):exports.external.log(`ERROR: Malformed event. "PhysicalResourceId" is required: ${JSON.stringify(event)}`)),await submitResponse("FAILED",resp)}}function renderResponse(cfnRequest,handlerResponse={}){const physicalResourceId=handlerResponse.PhysicalResourceId??cfnRequest.PhysicalResourceId??cfnRequest.RequestId;if(cfnRequest.RequestType==="Delete"&&physicalResourceId!==cfnRequest.PhysicalResourceId)throw new Error(`DELETE: cannot change the physical resource ID from "${cfnRequest.PhysicalResourceId}" to "${handlerResponse.PhysicalResourceId}" during deletion`);return{...cfnRequest,...handlerResponse,PhysicalResourceId:physicalResourceId}}async function submitResponse(status,event){const json={Status:status,Reason:event.Reason??status,StackId:event.StackId,RequestId:event.RequestId,PhysicalResourceId:event.PhysicalResourceId||MISSING_PHYSICAL_ID_MARKER,LogicalResourceId:event.LogicalResourceId,NoEcho:event.NoEcho,Data:event.Data},parsedUrl=url.parse(event.ResponseURL),loggingSafeUrl=`${parsedUrl.protocol}//${parsedUrl.hostname}/${parsedUrl.pathname}?***`;exports.external.log("submit response to cloudformation",loggingSafeUrl,json);const responseBody=JSON.stringify(json),req={hostname:parsedUrl.hostname,path:parsedUrl.path,method:"PUT",headers:{"content-type":"","content-length":Buffer.byteLength(responseBody,"utf8")}};await withRetries({attempts:5,sleep:1e3},exports.external.sendHttpRequest)(req,responseBody)}async function defaultSendHttpRequest(options,requestBody){return new Promise((resolve,reject)=>{try{const request=https.request(options,response=>{response.resume(),!response.statusCode||response.statusCode>=400?reject(new Error(`Unsuccessful HTTP response: ${response.statusCode}`)):resolve()});request.on("error",reject),request.write(requestBody),request.end()}catch(e){reject(e)}})}function defaultLog(fmt,...params){console.log(fmt,...params)}function withRetries(options,fn){return async(...xs)=>{let attempts=options.attempts,ms=options.sleep;for(;;)try{return await fn(...xs)}catch(e){if(attempts--<=0)throw e;await sleep(Math.floor(Math.random()*ms)),ms*=2}}}async function sleep(ms){return new Promise(ok=>setTimeout(ok,ms))}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,27 @@
import { Construct } from "constructs";
import * as lambda from "../../../aws-lambda";
export declare class AwsCustomResourceSingletonFunction extends lambda.SingletonFunction {
constructor(scope: Construct, id: string, props: AwsCustomResourceSingletonFunctionProps);
}
/**
* Initialization properties for AwsCustomResourceSingletonFunction
*/
export interface AwsCustomResourceSingletonFunctionProps extends lambda.FunctionOptions {
/**
* A unique identifier to identify this Lambda.
*
* The identifier should be unique across all custom resource providers.
* We recommend generating a UUID per provider.
*/
readonly uuid: string;
/**
* A descriptive name for the purpose of this Lambda.
*
* If the Lambda does not have a physical name, this string will be
* reflected in its generated name. The combination of lambdaPurpose
* and uuid must be unique.
*
* @default SingletonLambda
*/
readonly lambdaPurpose?: string;
}

View File

@@ -0,0 +1 @@
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.AwsCustomResourceSingletonFunction=void 0;const path=require("path"),lambda=require("../../../aws-lambda");class AwsCustomResourceSingletonFunction extends lambda.SingletonFunction{constructor(scope,id,props){super(scope,id,{...props,code:lambda.Code.fromAsset(path.join(__dirname,"aws-custom-resource-handler")),handler:"index.handler",runtime:lambda.determineLatestNodeRuntime(scope)}),this.addMetadata("aws:cdk:is-custom-resource-handler-singleton",!0),this.addMetadata("aws:cdk:is-custom-resource-handler-runtime-family",this.runtime.family),props?.logGroup&&this.logGroup.node.addMetadata("aws:cdk:is-custom-resource-handler-logGroup",!0),props?.logRetention&&this.lambdaFunction._logRetention?.node.addMetadata("aws:cdk:is-custom-resource-handler-logRetention",!0)}}exports.AwsCustomResourceSingletonFunction=AwsCustomResourceSingletonFunction;

View File

@@ -0,0 +1,5 @@
import { Construct } from "constructs";
import * as lambda from "../../../aws-lambda";
export declare class ApproveLambdaFunction extends lambda.Function {
constructor(scope: Construct, id: string, props?: lambda.FunctionOptions);
}

View File

@@ -0,0 +1 @@
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.ApproveLambdaFunction=void 0;const path=require("path"),lambda=require("../../../aws-lambda");class ApproveLambdaFunction extends lambda.Function{constructor(scope,id,props){super(scope,id,{...props,code:lambda.Code.fromAsset(path.join(__dirname,"approve-lambda")),handler:"index.handler",runtime:lambda.determineLatestNodeRuntime(scope)}),this.node.addMetadata("aws:cdk:is-custom-resource-handler-runtime-family",this.runtime.family)}}exports.ApproveLambdaFunction=ApproveLambdaFunction;

View File

@@ -0,0 +1 @@
"use strict";var l=Object.defineProperty,y=Object.getOwnPropertyDescriptor,f=Object.getOwnPropertyNames,w=Object.prototype.hasOwnProperty,S=(t,e)=>{for(var n in e)l(t,n,{get:e[n],enumerable:!0})},v=(t,e,n,s)=>{if(e&&typeof e=="object"||typeof e=="function")for(let a of f(e))!w.call(t,a)&&a!==n&&l(t,a,{get:()=>e[a],enumerable:!(s=y(e,a))||s.enumerable});return t},h=t=>v(l({},"__esModule",{value:!0}),t),b={};S(b,{handler:()=>T}),module.exports=h(b);var d=require("@aws-sdk/client-codepipeline"),u=new d.CodePipeline,A=5,P=t=>new Promise(e=>setTimeout(e,t*1e3));async function T(t,e){let{PipelineName:n,StageName:s,ActionName:a}=t;function g(o){let m=o.stageStates?.filter(r=>r.stageName===s),c=m.length&&m[0].actionStates.filter(r=>r.actionName===a),p=c&&c.length&&c[0].latestExecution;return p?p.token:void 0}let N=Date.now()+A*6e4;for(;Date.now()<N;){let o=await u.getPipelineState({name:n}),i=g(o);if(i){await u.putApprovalResult({pipelineName:n,actionName:a,stageName:s,result:{summary:"No security changes detected. Automatically approved by Lambda.",status:"Approved"},token:i});return}await P(5)}}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,13 @@
import { Construct } from "constructs";
import { CustomResourceProviderBase, CustomResourceProviderOptions } from "../../../core";
export declare class TriggerProvider extends CustomResourceProviderBase {
/**
* Returns a stack-level singleton ARN (service token) for the custom resource provider.
*/
static getOrCreate(scope: Construct, uniqueid: string, props?: CustomResourceProviderOptions): string;
/**
* Returns a stack-level singleton for the custom resource provider.
*/
static getOrCreateProvider(scope: Construct, uniqueid: string, props?: CustomResourceProviderOptions): TriggerProvider;
private constructor();
}

View File

@@ -0,0 +1 @@
"use strict";Object.defineProperty(exports,"__esModule",{value:!0}),exports.TriggerProvider=void 0;const path=require("path"),core_1=require("../../../core");class TriggerProvider extends core_1.CustomResourceProviderBase{static getOrCreate(scope,uniqueid,props){return this.getOrCreateProvider(scope,uniqueid,props).serviceToken}static getOrCreateProvider(scope,uniqueid,props){const id=`${uniqueid}CustomResourceProvider`,stack=core_1.Stack.of(scope);return stack.node.tryFindChild(id)??new TriggerProvider(stack,id,props)}constructor(scope,id,props){super(scope,id,{...props,codeDirectory:path.join(__dirname,"lambda"),runtimeName:(0,core_1.determineLatestNodeRuntimeName)(scope)}),this.node.addMetadata("aws:cdk:is-custom-resource-handler-customResourceProvider",!0)}}exports.TriggerProvider=TriggerProvider;