Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Resource name sanitization #108

Merged
merged 11 commits into from
Feb 2, 2023
260 changes: 173 additions & 87 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(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here, need to remove clustername as the actual name for this and memdb but lets maybe chat about that tomorrow

validators.cacheCluster.cacheClusterIdValidation()
)`${sanitizeClusterName(appName, dbName)}`
// create the db resources
const redis = new aws.elasticache.Cluster(
clusterName,
Expand Down
34 changes: 16 additions & 18 deletions pkg/infra/pulumi_aws/iac/load_balancing.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,14 @@
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 aws from '@pulumi/aws'
import * as validators from './sanitization/aws/elb'
import {
ListenerArgs,
TargetGroupArgs,
LoadBalancerArgs,
TargetGroupArgs,
TargetGroupAttachmentArgs,
} from '@pulumi/aws/lb'
import { ListenerRuleArgs } from '@pulumi/aws/alb'
import { hash as h, sanitized } from './sanitization/sanitizer'
import { CloudCCLib } from '../deploylib'

export interface Route {
Expand Down Expand Up @@ -176,9 +168,12 @@ 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}`, {
lb = new aws.lb.LoadBalancer(lbName, {
internal: params.internal || false,
loadBalancerType: 'application',
securityGroups: params.securityGroups,
Expand All @@ -188,7 +183,7 @@ export class LoadBalancerPlugin {
})
break
case 'network':
lb = new aws.lb.LoadBalancer(`${appName}-${resourceId}`, {
lb = new aws.lb.LoadBalancer(lbName, {
internal: params.internal || true,
loadBalancerType: 'network',
subnets: params.subnets,
Expand Down Expand Up @@ -237,12 +232,15 @@ export class LoadBalancerPlugin {
params: TargetGroupArgs
): aws.lb.TargetGroup => {
let targetGroup: aws.lb.TargetGroup
let tgName = sanitized(validators.targetGroup.nameValidation())`${h(appName)}-${h(
execUnitName
)}`
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}-${execUnitName}`, {
targetGroup = new aws.lb.TargetGroup(tgName, {
port: params.port,
protocol: params.protocol,
targetType: 'ip',
Expand All @@ -251,15 +249,15 @@ export class LoadBalancerPlugin {
})
break
case 'instance':
targetGroup = new aws.lb.TargetGroup(`${appName}-${execUnitName}`, {
targetGroup = new aws.lb.TargetGroup(tgName, {
port: params.port,
protocol: params.protocol,
vpcId: params.vpcId,
tags: params.tags,
})
break
case 'alb':
targetGroup = new aws.lb.TargetGroup(`${appName}-${execUnitName}`, {
targetGroup = new aws.lb.TargetGroup(tgName, {
targetType: 'alb',
port: params.port,
protocol: params.protocol,
Expand All @@ -269,7 +267,7 @@ export class LoadBalancerPlugin {
})
break
case 'lambda':
targetGroup = new aws.lb.TargetGroup(`${appName}-${execUnitName}`, {
targetGroup = new aws.lb.TargetGroup(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