Skip to content

Commit

Permalink
Sanitizes AWS resource names in deploylib and related IAC files
Browse files Browse the repository at this point in the history
  • Loading branch information
DavidSeptimus committed Jan 18, 2023
1 parent 40eba0c commit 26a5641
Show file tree
Hide file tree
Showing 29 changed files with 1,080 additions and 107 deletions.
253 changes: 172 additions & 81 deletions pkg/infra/pulumi_aws/deploylib.ts

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion pkg/infra/pulumi_aws/files.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import (
"github.com/klothoplatform/klotho/pkg/templateutils"
)

//go:embed *.tmpl *.ts *.json iac/*.ts iac/k8s/*
//go:embed *.tmpl *.ts *.json iac/*.ts iac/k8s/* iac/sanitization/*
var files embed.FS

var index = templateutils.MustTemplate(files, "index.ts.tmpl")
Expand Down
10 changes: 7 additions & 3 deletions pkg/infra/pulumi_aws/iac/elasticache.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import * as aws from '@pulumi/aws'
import * as pulumi from '@pulumi/pulumi'
import { PulumiFn } from '@pulumi/pulumi/automation'
import { Resource, CloudCCLib } from '../deploylib'
import { Resource } from '../deploylib'
import * as validators from './sanitization/aws/elasticache'
import { sanitized } from './sanitization/sanitizer'

const ELASTICACHE_ENGINE = 'redis'

Expand Down Expand Up @@ -46,7 +47,10 @@ export const setupElasticacheCluster = (
retentionInDays: 0,
})

const clusterName = sanitizeClusterName(appName, dbName)
// TODO: look into removing sanitizeClusterName when making other breaking changes to resource names
const clusterName = sanitized(
validators.cacheCluster.cacheClusterIdValidation()
)`${sanitizeClusterName(appName, dbName)}`
// create the db resources
const redis = new aws.elasticache.Cluster(
clusterName,
Expand Down
33 changes: 15 additions & 18 deletions pkg/infra/pulumi_aws/iac/load_balancing.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,13 @@
import { Region } from '@pulumi/aws'
import * as aws from '@pulumi/aws'
import * as awsx from '@pulumi/awsx'
import * as k8s from '@pulumi/kubernetes'

import * as pulumi from '@pulumi/pulumi'
import * as sha256 from 'simple-sha256'
import * as fs from 'fs'
import * as requestRetry from 'requestretry'
import * as crypto from 'crypto'

import * as eks from '@pulumi/eks'
import * as validators from './sanitization/aws/elb'
import {
ListenerArgs,
TargetGroupArgs,
LoadBalancerArgs,
TargetGroupArgs,
TargetGroupAttachmentArgs,
} from '@pulumi/aws/lb'
import { ListenerRuleArgs } from '@pulumi/aws/alb'
import { h, sanitized } from './sanitization/sanitizer'

export class LoadBalancerPlugin {
// A map of all resources which are going to be fronted by a load balancer
Expand All @@ -32,10 +23,13 @@ export class LoadBalancerPlugin {
params: LoadBalancerArgs
): aws.lb.LoadBalancer => {
let lb: aws.lb.LoadBalancer
let lbName = sanitized(validators.loadBalancer.nameValidation())`${h(appName)}-${h(
resourceId
)}`
switch (params.loadBalancerType) {
case 'application':
lb = new aws.lb.LoadBalancer(`${appName}-${resourceId}-alb`, {
name: `${appName}-${resourceId}`,
name: lbName,
internal: params.internal || false,
loadBalancerType: 'application',
securityGroups: params.securityGroups,
Expand All @@ -46,7 +40,7 @@ export class LoadBalancerPlugin {
break
case 'network':
lb = new aws.lb.LoadBalancer(`${appName}-${resourceId}-nlb`, {
name: `${appName}-${resourceId}`,
name: lbName,
internal: params.internal || true,
loadBalancerType: 'network',
subnets: params.subnets,
Expand Down Expand Up @@ -93,13 +87,16 @@ export class LoadBalancerPlugin {
params: TargetGroupArgs
): aws.lb.TargetGroup => {
let targetGroup: aws.lb.TargetGroup
let tgName = sanitized(validators.targetGroup.nameValidation())`${h(appName)}-${h(
resourceId
)}`
if (params.targetType != 'lambda' && !(params.port && params.protocol)) {
throw new Error('Port and Protocol must be specified for non lambda target types')
}
switch (params.targetType) {
case 'ip':
targetGroup = new aws.lb.TargetGroup(`${appName}-${resourceId}-targetGroup`, {
name: `${appName}-${resourceId}`,
name: tgName,
port: params.port,
protocol: params.protocol,
targetType: 'ip',
Expand All @@ -109,7 +106,7 @@ export class LoadBalancerPlugin {
break
case 'instance':
targetGroup = new aws.lb.TargetGroup(`${appName}-${resourceId}-targetGroup`, {
name: `${appName}-${resourceId}`,
name: tgName,
port: params.port,
protocol: params.protocol,
vpcId: params.vpcId,
Expand All @@ -118,7 +115,7 @@ export class LoadBalancerPlugin {
break
case 'alb':
targetGroup = new aws.lb.TargetGroup(`${appName}-${resourceId}-targetGroup`, {
name: `${appName}-${resourceId}`,
name: tgName,
targetType: 'alb',
port: params.port,
protocol: params.protocol,
Expand All @@ -129,7 +126,7 @@ export class LoadBalancerPlugin {
break
case 'lambda':
targetGroup = new aws.lb.TargetGroup(`${appName}-${resourceId}-targetGroup`, {
name: `${appName}-${resourceId}`,
name: tgName,
targetType: 'lambda',
tags: params.tags,
})
Expand Down
8 changes: 7 additions & 1 deletion pkg/infra/pulumi_aws/iac/memorydb.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import * as aws from '@pulumi/aws'
import * as pulumi from '@pulumi/pulumi'
import { Resource } from '../deploylib'
import { sanitized } from './sanitization/sanitizer'
import { cacheCluster } from './sanitization/aws/memorydb'

const sanitizeClusterName = (appName: string, dbName: string): string => {
let cluster = `${appName}-${dbName}`
Expand Down Expand Up @@ -37,7 +39,11 @@ export const setupMemoryDbCluster = (
securityGroupIds: pulumi.Output<string>[],
appName: string
) => {
const clusterName = sanitizeClusterName(appName, dbName)
// TODO: look into removing sanitizeClusterName when making other breaking changes to resource names
const clusterName = sanitized(cacheCluster.clusterNameValidation())`${sanitizeClusterName(
appName,
dbName
)}`
const memdbCluster = new aws.memorydb.Cluster(
clusterName,
{
Expand Down
11 changes: 11 additions & 0 deletions pkg/infra/pulumi_aws/iac/sanitization/aws/app_runner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { regexpMatch } from '../sanitizer'

export const service = {
nameValidation() {
return {
minLength: 4,
maxLength: 40,
rules: [regexpMatch('', /^[\w-]+$/, (n) => n.replace(/[^\w-]/g, '-'))],
}
},
}
17 changes: 17 additions & 0 deletions pkg/infra/pulumi_aws/iac/sanitization/aws/cloud_watch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { regexpMatch } from '../sanitizer'

export const logGroup = {
nameValidation() {
return {
minLength: 1,
maxLength: 512,
rules: [
regexpMatch(
"Log group names consist of the following characters: a-z, A-Z, 0-9, '_' (underscore), '-' (hyphen), '/' (forward slash), '.' (period), and '#' (number sign).",
/^[-._/#A-Za-z\d]+$/,
(n) => n.replace(/[^-._/#A-Za-z\d]/g, '_')
),
],
}
},
}
33 changes: 33 additions & 0 deletions pkg/infra/pulumi_aws/iac/sanitization/aws/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { regexpMatch } from '../sanitizer'

export const tag = {
keyValidation() {
return {
minLength: 1,
maxLength: 128,
rules: [
regexpMatch('', /^[\p{L}\p{Z}\p{N}_.:/=+\-@]+$/u, (n) =>
n.replace(/[\p{L}\p{Z}\p{N}_.:/=+\-@]/gu, '_')
),
],
}
},

valueValidation() {
return {
minLength: 0,
maxLength: 256,
rules: [
regexpMatch('', /^[\p{L}\p{Z}\p{N}_.:/=+\-@]+$/u, (n) =>
n.replace(/[\p{L}\p{Z}\p{N}_.:/=+\-@]/gu, '_')
),
{
description:
"The aws: prefix is prohibited for tags; it's reserved for AWS use.",
apply: (v) => !v.startsWith('aws:'),
fix: (v) => v.replace(/^aws:/, ''),
},
],
}
},
}
13 changes: 13 additions & 0 deletions pkg/infra/pulumi_aws/iac/sanitization/aws/dynamodb.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { regexpMatch } from '../sanitizer'

export const table = {
nameValidation() {
return {
minLength: 3,
maxLength: 255,
rules: [
regexpMatch('', /^[a-zA-Z0-9_.-]+$/, (n) => n.replace(/[^a-zA-Z0-9_.-]/g, '_')),
],
}
},
}
40 changes: 40 additions & 0 deletions pkg/infra/pulumi_aws/iac/sanitization/aws/ec2.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { regexpMatch } from '../sanitizer'

export const vpc = {
securityGroup: {
nameValidation() {
return {
minLength: 1,
maxLength: 255,
rules: [
regexpMatch(
'a-z, A-Z, 0-9, spaces, and ._-:/()#,@[]+=&;{}!$*',
/^[\w -.:/()#,@\[\]+=&;{}!$*]+$/,
(s) => s.replace(/[^\w -.:/()#,@\[\]+=&;{}!$*]/g, '_')
),
],
}
},
},
}

export const classic = {
securityGroup: {
nameValidation() {
return {
minLength: 1,
maxLength: 255,
rules: [
regexpMatch('Must only containASCII characters', /^[[:ascii:]]+$/, (s) =>
s.replace(/[^[:ascii:]]/g, '_')
),
{
description: "Cannot start with 'sg-'",
validate: (s) => !s.startsWith('sg-'),
fix: (s) => s.substring(3),
},
],
}
},
},
}
31 changes: 31 additions & 0 deletions pkg/infra/pulumi_aws/iac/sanitization/aws/ecs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { regexpMatch } from '../sanitizer'

export const cluster = {
nameValidation() {
return {
minLength: 1,
maxLength: 255,
rules: [regexpMatch('', /^[\w-]+$/, (s) => s.replace(/[^\w-]/g, '_'))],
}
},
}

export const taskDefinition = {
familyValidation() {
return {
minLength: 1,
maxLength: 255,
rules: [regexpMatch('', /^[\w-]+$/, (s) => s.replace(/[^\w-]/g, '_'))],
}
},
}

export const service = {
nameValidation() {
return {
minLength: 1,
maxLength: 255,
rules: [regexpMatch('', /^[\w-]+$/, (s) => s.replace(/[^\w-]/g, '_'))],
}
},
}
20 changes: 20 additions & 0 deletions pkg/infra/pulumi_aws/iac/sanitization/aws/eks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { regexpMatch } from '../sanitizer'

export const cluster = {
nameValidation() {
return {
minLength: 1,
maxLength: 100,
rules: [
regexpMatch(
'The name can contain only alphanumeric characters (case-sensitive) and hyphens.',
/^[a-zA-Z\d-]+$/,
(s) => s.replace(/[^a-zA-Z\d-]/g, '_')
),
regexpMatch('The name must start with an alphabetic character', /^[a-zA-Z]/, (s) =>
s.replace(/^[^a-zA-Z]+/, '')
),
],
}
},
}
50 changes: 50 additions & 0 deletions pkg/infra/pulumi_aws/iac/sanitization/aws/elasticache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { regexpMatch, regexpNotMatch } from '../sanitizer'

export const cacheCluster = {
cacheClusterIdValidation() {
return {
minLength: 1,
maxLength: 50,
rules: [
regexpMatch('', /[a-zA-Z0-9-]/, (s) => s.replace(/[^a-zA-Z0-9-]/g, '-')),
{
description: 'Identifier must not end with a hyphen',
validate: (s) => !s.endsWith('-'),
fix: (s) => s.replace(/-+$/, ''),
},
regexpNotMatch('Identifier must not contain consecutive hyphens', /--/, (s) =>
s.replace(/--+/g, '-')
),
regexpMatch('Identifier must start with a letter', /^[a-zA-Z]/, (s) =>
s.replace(/^[^a-zA-Z]+/, '')
),
],
}
},
}
export const cacheSubnetGroup = {
cacheSubnetGroupNameValidation() {
return {
minLength: 1,
maxLength: 255,
rules: [
regexpMatch(
'',
/^[a-z\d-]+$/, // uppercase is technically valid, but AWS will convert the value to lowercase
(s) => s.toLocaleLowerCase().replace(/[^a-z\d-]/g, '-')
),
{
description: 'Identifier must not end with a hyphen',
validate: (s) => !s.endsWith('-'),
fix: (s) => s.replace(/-+$/, ''),
},
regexpNotMatch('Identifier must not contain consecutive hyphens', /--/, (s) =>
s.replace(/--+/g, '-')
),
regexpMatch('Identifier must start with a letter', /^[a-zA-Z]/, (s) =>
s.replace(/^[^a-zA-Z]+/, '')
),
],
}
},
}
Loading

0 comments on commit 26a5641

Please sign in to comment.