diff --git a/apply/apply.go b/apply/apply.go index 5fc93232b..2560bc264 100644 --- a/apply/apply.go +++ b/apply/apply.go @@ -483,7 +483,7 @@ func applyEnvs( if err != nil { return nil, errs.WrapUser(err, "unable to apply module invocation") } - } else if kind == v2.ComponentKindCDKTF { + } else if kind == v2.ComponentKindCDKTF || kind == v2.ComponentKindEnvtio { logrus.Warn("module invocations not templated for kind CDKTF") err := writeStructToTS(fs, foggTS, fmt.Sprintf("%s/src/helpers/fogg-types.generated.ts", path)) if err != nil { diff --git a/apply/golden_file_test.go b/apply/golden_file_test.go index f7369c4a4..09bd614ec 100644 --- a/apply/golden_file_test.go +++ b/apply/golden_file_test.go @@ -47,6 +47,7 @@ func TestIntegration(t *testing.T) { {"v2_github_actions_with_pre_commit"}, {"v2_atlantis_depends_on"}, {"v2_cdktf_components"}, + {"v2_envtio_components"}, {"generic_providers_yaml"}, } diff --git a/config/v2/config.go b/config/v2/config.go index 551261ff1..ca0d88be0 100644 --- a/config/v2/config.go +++ b/config/v2/config.go @@ -666,6 +666,8 @@ const ( ComponentKindTerraform = DefaultComponentKind // ComponentKindCDKTF is a CDKTF component ComponentKindCDKTF ComponentKind = "cdktf" + // ComponentKindEnvtio is a CDKTF component using the envtio framework + ComponentKindEnvtio ComponentKind = "envtio" // DefaultComponentKind defaults to terraform component DefaultModuleKind ModuleKind = "terraform" // ModuleKindTerraform is a terraform Module diff --git a/plan/ci.go b/plan/ci.go index 87030b0fd..6848fee11 100644 --- a/plan/ci.go +++ b/plan/ci.go @@ -435,7 +435,7 @@ func (p *Plan) buildTurboRootConfig(c *v2.Config) *TurboConfig { for env, envPlan := range p.Envs { for component, componentPlan := range envPlan.Components { kind := componentPlan.Kind.GetOrDefault() - if kind == v2.ComponentKindCDKTF { + if kind == v2.ComponentKindCDKTF || kind == v2.ComponentKindEnvtio { // applyEnvs implementation detail pkgs = append(pkgs, fmt.Sprintf("%s/envs/%s/%s", util.RootPath, env, component)) } diff --git a/plan/plan.go b/plan/plan.go index 88ab4dc54..c395bcd5a 100644 --- a/plan/plan.go +++ b/plan/plan.go @@ -721,6 +721,9 @@ func (p *Plan) buildEnvs(conf *v2.Config) (map[string]Env, error) { "prettier": "^3.3.3", "typescript": "^5.4.0", } + if componentConf.Kind.GetOrDefault() == v2.ComponentKindEnvtio { + componentPlan.CdktfDependencies["@envtio/base"] = "0.0.7" + } for _, dep := range componentConf.CdktfDependencies { componentPlan.CdktfDependencies[dep.Name] = dep.Version @@ -740,9 +743,8 @@ func (p *Plan) buildEnvs(conf *v2.Config) (map[string]Env, error) { componentBackends := make(map[string]Backend) for componentName, component := range envPlan.Components { - // FIXME (el): get rid of non-terraform component kinds kind := component.Kind.GetOrDefault() - if !(kind == v2.ComponentKindTerraform || kind == v2.ComponentKindCDKTF) { + if !(kind == v2.ComponentKindTerraform || kind == v2.ComponentKindCDKTF || kind == v2.ComponentKindEnvtio) { continue } diff --git a/templates/templates.go b/templates/templates.go index 15786ff3b..882d316aa 100644 --- a/templates/templates.go +++ b/templates/templates.go @@ -78,6 +78,7 @@ var Templates = &T{ Components: map[v2.ComponentKind]fs.FS{ v2.ComponentKindTerraform: mustFSSub("templates/component/terraform"), v2.ComponentKindCDKTF: mustFSSub("templates/component/cdktf"), + v2.ComponentKindEnvtio: mustFSSub("templates/component/envtio"), }, Env: mustFSSub("templates/env"), Module: map[v2.ModuleKind]fs.FS{ diff --git a/templates/templates/component/envtio/.eslintrc.json.tmpl b/templates/templates/component/envtio/.eslintrc.json.tmpl new file mode 100644 index 000000000..c3db6104f --- /dev/null +++ b/templates/templates/component/envtio/.eslintrc.json.tmpl @@ -0,0 +1,78 @@ +{ + "env": { + "node": true + }, + "root": true, + "plugins": ["@typescript-eslint", "import"], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": 2018, + "sourceType": "commonjs", + "project": "./tsconfig.dev.json" + }, + "extends": ["plugin:import/typescript", "plugin:prettier/recommended"], + "settings": { + "import/parsers": { + "@typescript-eslint/parser": [".ts", ".tsx"] + }, + "import/resolver": { + "node": {}, + "typescript": { + "project": "./tsconfig.dev.json", + "alwaysTryTypes": true + } + } + }, + "ignorePatterns": ["*.js", "*.d.ts", "node_modules/", "*.generated.ts"], + "rules": { + "@typescript-eslint/no-require-imports": ["error"], + "import/no-extraneous-dependencies": [ + "error", + { + "devDependencies": ["**/test/**"], + "optionalDependencies": false, + "peerDependencies": true + } + ], + "import/no-unresolved": ["error"], + "import/order": [ + "warn", + { + "groups": ["builtin", "external"], + "alphabetize": { + "order": "asc", + "caseInsensitive": true + } + } + ], + "no-duplicate-imports": ["error"], + "no-shadow": ["off"], + "@typescript-eslint/no-shadow": ["error"], + "key-spacing": ["error"], + "no-multiple-empty-lines": ["error"], + "@typescript-eslint/no-floating-promises": ["error"], + "no-return-await": ["off"], + "@typescript-eslint/return-await": ["error"], + "no-trailing-spaces": ["error"], + "dot-notation": ["error"], + "no-bitwise": ["error"], + "@typescript-eslint/member-ordering": [ + "error", + { + "default": [ + "public-static-field", + "public-static-method", + "protected-static-field", + "protected-static-method", + "private-static-field", + "private-static-method", + "field", + "constructor", + "method" + ] + } + ], + "eol-last": ["error", "always"], + "space-in-parens": ["error", "never"] + } +} diff --git a/templates/templates/component/envtio/.gitattributes.create b/templates/templates/component/envtio/.gitattributes.create new file mode 100644 index 000000000..1383c5949 --- /dev/null +++ b/templates/templates/component/envtio/.gitattributes.create @@ -0,0 +1,4 @@ +/.eslintrc.json linguist-generated +/.prettierrc.json linguist-generated +/.gitattributes linguist-generated +/.fogg-component.yaml linguist-generated diff --git a/templates/templates/component/envtio/.gitignore.create b/templates/templates/component/envtio/.gitignore.create new file mode 100644 index 000000000..37380abf1 --- /dev/null +++ b/templates/templates/component/envtio/.gitignore.create @@ -0,0 +1,17 @@ +*.d.ts +*.js +node_modules +cdktf.out +cdktf.log +*terraform.*.tfstate* +.gen +.terraform +.terraform.lock.hcl +tsconfig.tsbuildinfo +!jest.config.js +!setup.js + +// build outputs +cdk.tf.json +assets +dist diff --git a/templates/templates/component/envtio/.prettierrc.json.tmpl b/templates/templates/component/envtio/.prettierrc.json.tmpl new file mode 100644 index 000000000..84c85a388 --- /dev/null +++ b/templates/templates/component/envtio/.prettierrc.json.tmpl @@ -0,0 +1,3 @@ +{ + "overrides": [] +} diff --git a/templates/templates/component/envtio/Makefile.tmpl b/templates/templates/component/envtio/Makefile.tmpl new file mode 100644 index 000000000..4de276f40 --- /dev/null +++ b/templates/templates/component/envtio/Makefile.tmpl @@ -0,0 +1,32 @@ +{{ template "fogg_header" }} +lint: + pnpm run eslint +.PHONY: lint + +fmt: + pnpm run prettier +.PHONY: fmt + +check-plan: cdk.tf.json + terraform init + terraform plan +.PHONY: check-plan + +docs: +.PHONY: docs + +clean: + rm -rf cdk.tf.json + rm -rf assets + rm -rf node_modules +.PHONY: clean + +cdk.tf.json: + pnpm i + pnpm turbo synth +.PHONY: cdk.tf.json + +check-updates: + pnpx npm-check-updates@latest --target=minor + @echo "WARNING: update fogg.yml for target package versions" +.PHONY: check-updates diff --git a/templates/templates/component/envtio/README.md.create b/templates/templates/component/envtio/README.md.create new file mode 100644 index 000000000..d81ffebb4 --- /dev/null +++ b/templates/templates/component/envtio/README.md.create @@ -0,0 +1,5 @@ +# Envtio Component + +```console +make check-plan +``` diff --git a/templates/templates/component/envtio/package.json.tmpl b/templates/templates/component/envtio/package.json.tmpl new file mode 100644 index 000000000..a0c330195 --- /dev/null +++ b/templates/templates/component/envtio/package.json.tmpl @@ -0,0 +1,14 @@ +{ + "name": "{{ list .Env .Name | join "-" }}", + "author": "{{ .Owner }}", + "version": "0.0.0", + "private": true, + "scripts": { + "eslint": "eslint . --ext .ts", + "prettier": "prettier --write .", + "synth": "npx ts-node --swc -P ./tsconfig.dev.json src/index.ts" + }, + "dependencies": {{ .CdktfDependencies | toPrettyJson | indent 2 }}, + "devDependencies": {{ .CdktfDevDependencies | toPrettyJson | indent 2 }}, + "//" : "Auto-generated by fogg. Do not edit" +} diff --git a/templates/templates/component/envtio/src/helpers/fogg-stack.ts.tmpl b/templates/templates/component/envtio/src/helpers/fogg-stack.ts.tmpl new file mode 100644 index 000000000..9c6501763 --- /dev/null +++ b/templates/templates/component/envtio/src/helpers/fogg-stack.ts.tmpl @@ -0,0 +1,332 @@ +{{ template "fogg_header_js" }} +import * as fs from "fs"; +import { provider as awsProvider } from "@cdktf/provider-aws"; +import { provider as cloudflareProvider } from "@cdktf/provider-cloudflare"; +import { provider as dataDogProvider } from "@cdktf/provider-datadog"; +import { AwsSpec, AwsSpecProps } from "@envtio/base/lib/aws"; +import { + S3Backend, + S3BackendConfig, + DataTerraformRemoteState, + DataTerraformRemoteStateS3, + DataTerraformRemoteStateS3Config, + TerraformHclModule, + TerraformLocal, +} from "cdktf"; +import { Construct } from "constructs"; +import * as yaml from "js-yaml"; + +import { + Component, + Backend, + AWSProvider, + DatadogProvider, + GenericProvider, +} from "./fogg-types.generated"; + +export interface FoggStackProps extends Omit { + /** + * Force remote state configuration + * @default false - only configure remote states if `component_backends_filtered` is true + */ + forceRemoteStates?: boolean; +} + +export function loadComponentConfig(): Component { + const file = fs.readFileSync(`.fogg-component.yaml`, "utf8"); + const componentConfig = yaml.load(file) as Component; + return replaceNullWithUndefined(componentConfig); +} + +/** + * Helper stack to wrap Fogg component configuration and set up configured providers and backends. + */ +export class FoggStack extends AwsSpec { + public readonly foggComp: Component; + public readonly modules: Record = {}; + public readonly locals: Record = {}; + private readonly _remoteStates: Record = {}; + + constructor(scope: Construct, id: string, props: FoggStackProps) { + const foggComp = loadComponentConfig(); + const awsProviderConfig = parseAwsProviderConfig(foggComp); + super(scope, id, { + ...props, + providerConfig: awsProviderConfig, + }); + this.foggComp = foggComp; + + this.parseBackendConfig(); + this.parseBundledProviderConfig(); + for (const p of Object.values(this.foggComp.required_providers)) { + if (p.enabled) this.parseGenericProviderConfig(p); + } + // parse remote backends + const forceRemoteBackend = props.forceRemoteStates ?? false; + if (forceRemoteBackend || this.foggComp.component_backends_filtered) { + for (const [name, remoteStateConfig] of Object.entries( + this.foggComp.component_backends, + )) { + this.parseRemoteState(name, remoteStateConfig); + } + } + this.parseLocalsBlock(); + this.parseModules(); + } + + /** + * Set variables for the main module defined in the fogg component configuration. + * + * @param variables - The variables to set for the module + */ + public setMainModuleVariables(variables: Record): void { + const id = (this.foggComp.module_name = + this.foggComp.module_name ?? "main"); + this.setModuleVariables(id, variables); + } + + /** + * Return a remote state defined in the fogg component configuration. + * @param name - The name of the remote state to get + * @returns the DataTerraformRemoteState object + * @throws if the remote state is not found + */ + public remoteState(name: string): DataTerraformRemoteState { + if (!this._remoteStates[name]) { + throw new Error(`Remote state ${name} not found`); + } + return this._remoteStates[name]; + } + + /** + * Set variables for a module included in the fogg component modules[] configuration. + * + * @param name - The module name as defined in the fogg component configuration + * @param variables - The variables to set for the module + */ + public setModuleVariables( + name: string, + variables: Record, + ): void { + if (!this.modules[name]) { + throw new Error(`Module ${name} not found`); + } + for (const [key, value] of Object.entries(variables)) { + this.modules[name].set(key, value); + } + } + + /** + * Get a local defined in the fogg component configuration. + * + * @param name the name of the local to get + * @returns the TerraformLocal object + */ + public getLocal(name: string): TerraformLocal { + if (!this.locals[name]) { + throw new Error(`Local ${name} not found`); + } + return this.locals[name]; + } + + private parseBackendConfig(): void { + if (this.foggComp.backend.kind === "s3" && this.foggComp.backend.s3) { + const s3Config = this.foggComp.backend.s3; + let s3BackendConfig: Mutable = { + bucket: s3Config.bucket, + dynamodbTable: s3Config.dynamo_table, + key: s3Config.key_path, + region: s3Config.region, + encrypt: true, + }; + if (s3Config.profile) { + s3BackendConfig.profile = s3Config.profile; + } else if (s3Config.role_arn) { + s3BackendConfig.assumeRole = { + roleArn: s3Config.role_arn, + }; + } + // console.log( + // `Setting S3 backend Config ${JSON.stringify(s3BackendConfig, null, 2)}` + // ); + new S3Backend(this, s3BackendConfig); + } else { + throw new Error( + `Unsupported backend configuration ${this.foggComp.backend.kind}`, + ); + } + } + + private parseRemoteState(id: string, remoteConfig: Backend): void { + if (remoteConfig.kind === "s3" && remoteConfig.s3) { + const s3Config = remoteConfig.s3; + let remoteStateConfig: Mutable = { + bucket: s3Config.bucket, + dynamodbTable: s3Config.dynamo_table, + key: s3Config.key_path, + region: s3Config.region, + encrypt: true, + }; + if (s3Config.profile) { + remoteStateConfig.profile = s3Config.profile; + } else if (s3Config.role_arn) { + remoteStateConfig.assumeRole = { + roleArn: s3Config.role_arn, + }; + } + this._remoteStates[id] = new DataTerraformRemoteStateS3( + this, + id, + remoteStateConfig, + ); + } else { + throw new Error(`Unsupported backend configuration ${remoteConfig.kind}`); + } + } + + private parseBundledProviderConfig(): void { + const providers = this.foggComp.providers_configuration; + if (providers.datadog) { + this.parseDataDogProviderConfig(providers.datadog); + } + } + + private parseGenericProviderConfig(config: GenericProvider): void { + switch (config.source) { + case "cloudflare/cloudflare": + this.parseCloudflareProviderConfig(config); + break; + default: + throw new Error(`Unsupported provider ${config.source}`); + } + } + + private parseLocalsBlock() { + if (this.foggComp.locals_block) { + for (const [key, value] of Object.entries(this.foggComp.locals_block)) { + this.locals[key] = new TerraformLocal(this, key, `\${${value}}`); + } + } + } + + private parseModules() { + if (this.foggComp.module_source) { + const id = (this.foggComp.module_name = + this.foggComp.module_name ?? "main"); + this.modules[id] = new TerraformHclModule(this, id, { + source: this.foggComp.module_source, + }); + } + + for (let i = 0; i < this.foggComp.modules.length; i++) { + const moduleConfig = this.foggComp.modules[i]; + const id = moduleConfig.name ?? `module_${i}`; + if (!moduleConfig.source) { + console.warn(`Module ${id} does not have a source, skipping`); + continue; + } + if (this.modules[id]) { + throw new Error(`Module ${id} already exists`); + } + if (!moduleConfig.name) { + console.log( + `Module ${moduleConfig.source} does not have a name, using ${id}`, + ); + } + this.modules[id] = new TerraformHclModule(this, id, { + source: moduleConfig.source, + version: moduleConfig.version, + }); + // TODO: Add validation for module variables + // TODO: Export module outputs + } + } + + private parseDataDogProviderConfig(_config: DatadogProvider): void { + new dataDogProvider.DatadogProvider(this, "datadog", {}); + } + + private parseCloudflareProviderConfig(_config: GenericProvider): void { + new cloudflareProvider.CloudflareProvider(this, "cloudflare", {}); + } +} + +// Parse AWS Provider config from Fogg component configuration +function parseAwsProviderConfig( + foggComp: Component, +): awsProvider.AwsProviderConfig { + const providers = foggComp.providers_configuration; + if ( + providers.aws_regional_providers && + providers.aws_regional_providers.length > 0 + ) { + throw new Error( + "AWS regional providers are not supported by envtio components", + ); + } + if (providers.aws) { + return getAwsProviderConfig(foggComp, providers.aws); + } + throw new Error("AWS provider configuration not found"); +} + +function getAwsProviderConfig( + foggComp: Component, + config: AWSProvider, +): awsProvider.AwsProviderConfig { + const c: Mutable = { + region: config.region, + alias: config.alias, + }; + if (config.default_tags && config.default_tags.enabled) { + c.defaultTags = [ + { + tags: { + env: foggComp.env, + owner: foggComp.owner, + project: foggComp.project, + managedBy: "terraform", + service: foggComp.name, + ...(foggComp.backend.s3?.key_path && { + tfstateKey: foggComp.backend.s3?.key_path, + }), + ...(foggComp.providers_configuration?.aws?.default_tags?.enabled && + (foggComp.providers_configuration?.aws?.default_tags?.tags ?? {})), + }, + }, + ]; + } + if (config.profile) { + c.profile = config.profile; + } else if (config.role_arn) { + c.assumeRole = [ + { + roleArn: config.role_arn, + }, + ]; + } + return c; +} + +// helper type to make readonly interface properties mutable +type Mutable = { + -readonly [P in keyof T]: T[P]; +}; + +// helper function to replace fogg "null" values with "undefined" +function replaceNullWithUndefined(obj: any): any { + if (obj === null) { + return undefined; + } + if (Array.isArray(obj)) { + return obj.map(replaceNullWithUndefined); + } + if (typeof obj === "object" && obj !== null) { + const newObj: any = {}; + for (const key in obj) { + newObj[key] = replaceNullWithUndefined(obj[key]); + } + return newObj; + } + return obj; +} diff --git a/templates/templates/component/envtio/src/index.ts.tmpl b/templates/templates/component/envtio/src/index.ts.tmpl new file mode 100644 index 000000000..e8e5798fa --- /dev/null +++ b/templates/templates/component/envtio/src/index.ts.tmpl @@ -0,0 +1,24 @@ +{{ template "fogg_header_js" }} +import * as fs from "fs"; +import * as path from "path"; +import { App } from "cdktf"; +import { ComponentStack } from "./stack"; + +const outdir = "cdktf.out"; +const app = new App({ + outdir, +}); +const stack = new ComponentStack(app, "Default"); +app.synth(); + +// Copy the generated Terraform code to the current directory to keep relative directory references +const stackSynthDir = path.join( + outdir, + app.manifest.forStack(stack).workingDirectory, +); + +fs.cpSync(stackSynthDir, ".", { recursive: true }); +fs.rmSync(outdir, { recursive: true }); +if (process.env.CI) { + fs.renameSync("cdk.tf.json", "ci-cdk.tf.json"); +} diff --git a/templates/templates/component/envtio/src/stack.ts.create b/templates/templates/component/envtio/src/stack.ts.create new file mode 100644 index 000000000..727666096 --- /dev/null +++ b/templates/templates/component/envtio/src/stack.ts.create @@ -0,0 +1,42 @@ +// // Use provider-aws constructs directly +// import { dataAwsAvailabilityZones } from "@cdktf/provider-aws"; +// // Or use CDKTF constructs directly from npm +// import { MyConstruct } from "@handshakes/my-construct"; +import { Construct } from "constructs"; +import { FoggStack } from "./helpers/fogg-stack"; + +export class ComponentStack extends FoggStack { + constructor(scope: Construct, id: string) { + super(scope, id, { + forceRemoteStates: false, + // Prefix for resource UUID, should never change for component lifecycle + gridUUID: "123-456", + // This is used as a tag and may change over time + environmentName: "development", + }); + + // // Configure fogg module variables here + // this.setModuleVariables("main", { + // foo: "bar", + // baz: "qux", + // tags: { + // // Use fogg component configuration values + // Project: this.foggComp.project, + // Owner: this.foggComp.owner, + // Environment: this.foggComp.env, + // }, + // }); + + // // Or: create AWS Resources + // const azs = new dataAwsAvailabilityZones.DataAwsAvailabilityZones( + // this, + // "azs", + // {} + // ); + + // // Or: use custom constructs + // new MyConstruct(this, "my-construct", { + // foo: "bar", + // }); + } +} diff --git a/templates/templates/component/envtio/tsconfig.dev.json.create b/templates/templates/component/envtio/tsconfig.dev.json.create new file mode 100644 index 000000000..5fd187593 --- /dev/null +++ b/templates/templates/component/envtio/tsconfig.dev.json.create @@ -0,0 +1,31 @@ +{ + "compilerOptions": { + "alwaysStrict": true, + + "experimentalDecorators": true, + "inlineSourceMap": true, + "inlineSources": true, + "lib": ["es2018"], + "module": "CommonJS", + "noFallthroughCasesInSwitch": true, + "noImplicitAny": true, + "noImplicitReturns": true, + "noImplicitThis": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "resolveJsonModule": true, + "strict": true, + "strictNullChecks": true, + "strictPropertyInitialization": true, + "stripInternal": true, + "incremental": true, + "skipLibCheck": true, + "declaration": false, + "esModuleInterop": true, + "noEmitOnError": false, + "target": "ES2019", + "outDir": "dist" + }, + "include": ["src/**/*.ts", "test/**/*.ts"], + "exclude": ["node_modules", "cdktf.out"] +} diff --git a/testdata/v2_envtio_components/.fogg-version b/testdata/v2_envtio_components/.fogg-version new file mode 100644 index 000000000..64e78fd82 --- /dev/null +++ b/testdata/v2_envtio_components/.fogg-version @@ -0,0 +1 @@ +undefined-pre+undefined.dirty \ No newline at end of file diff --git a/testdata/v2_envtio_components/.gitattributes b/testdata/v2_envtio_components/.gitattributes new file mode 100644 index 000000000..a69b7f0d2 --- /dev/null +++ b/testdata/v2_envtio_components/.gitattributes @@ -0,0 +1,15 @@ +fogg.tf linguist-generated +remote-states.tf linguist-generated +Makefile linguist-generated +atlantis.yaml linguist-generated +.travis.yml linguist-generated +.circleci/config.yml linguist-generated +.terraformignore linguist-generated +.github/workflows/fogg_ci.yml linguist-generated +.github/workflows/actions/setup-pre-commit/action.yml linguist-generated +.pre-commit-config linguist-generated +requirements.txt linguist-generated +.vscode/settings.json linguist-generated +pnpm-workspace.yaml linguist-generated +package.json linguist-generated +turbo.json linguist-generated diff --git a/testdata/v2_envtio_components/.gitignore b/testdata/v2_envtio_components/.gitignore new file mode 100644 index 000000000..c298677ff --- /dev/null +++ b/testdata/v2_envtio_components/.gitignore @@ -0,0 +1,39 @@ +# Auto-generated by fogg. Do not edit +# Make improvements in fogg, so that everyone can benefit. + +# Compiled files +*.tfstate +*.tfstate.*.backup +*.tfstate.backup +*tfvars +.terraform.lock.hcl +# JS files +node_modules +.turbo +# Module directory +.terraform/ + +# Pycharm folder +.idea + +# Editor Swap Files +*.swp +*.swo +*.swn +*.swm +*.swl +*.swk + +.fogg +/terraform.d/plugins +/terraform.d/modules + +.DS_Store + +# Scala language server +.metals + +buildevents.plan +check-plan.output + +venv diff --git a/testdata/v2_envtio_components/.terraform.d/plugin-cache/.gitignore b/testdata/v2_envtio_components/.terraform.d/plugin-cache/.gitignore new file mode 100644 index 000000000..504d4d91d --- /dev/null +++ b/testdata/v2_envtio_components/.terraform.d/plugin-cache/.gitignore @@ -0,0 +1,5 @@ +# Auto-generated by fogg. Do not edit +# Make improvements in fogg, so that everyone can benefit. + +* +!.gitignore diff --git a/testdata/v2_envtio_components/.terraformignore b/testdata/v2_envtio_components/.terraformignore new file mode 100644 index 000000000..e472f3751 --- /dev/null +++ b/testdata/v2_envtio_components/.terraformignore @@ -0,0 +1,10 @@ +# Auto-generated by fogg. Do not edit +# Make improvements in fogg, so that everyone can benefit. + + +.fogg/ +terraform.d/** +**/terraform.d +**/.terraform.d/** +**/.terraform/** +**/.git/** diff --git a/testdata/v2_envtio_components/.vscode/settings.json b/testdata/v2_envtio_components/.vscode/settings.json new file mode 100644 index 000000000..a45e3566a --- /dev/null +++ b/testdata/v2_envtio_components/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "eslint.workingDirectories": [ + "terraform/envs/test/lambda", + "terraform/modules/my-envtio-module" + ] +} diff --git a/testdata/v2_envtio_components/Makefile b/testdata/v2_envtio_components/Makefile new file mode 100644 index 000000000..391cd0730 --- /dev/null +++ b/testdata/v2_envtio_components/Makefile @@ -0,0 +1,152 @@ +# Auto-generated by fogg. Do not edit +# Make improvements in fogg, so that everyone can benefit. + +include scripts/common.mk + +ENVS=test +MODULES=my-envtio-module +ACCOUNTS= + +all: check + +setup: ## set up working directory by installing dependencies + curl -s https://raw.githubusercontent.com/vincenthsh/fogg/vundefined-pre+undefined.dirty/download.sh | bash -s -- -b .fogg/bin vundefined-pre+undefined.dirty + .fogg/bin/fogg setup +.PHONY: setup + +lint: lint-terraform ## lint the code in this repo +.PHONY: lint + +lint-terraform: lint-accounts lint-envs lint-modules ## lint the terraform code in this repo +.PHONY: lint-terraform + +lint-accounts: ## lint the terrarform code in terraform/accounts + @for account in $(ACCOUNTS); do \ + echo "terraform/accounts/$$account"; \ + $(MAKE) -C terraform/accounts/$$account lint || exit $$?; \ + done +.PHONY: lint-accounts + +lint-envs: ## lint the terraform code in terraform/envs + @for env in $(ENVS); do \ + echo "terraform/envs/$$env"; \ + $(MAKE) -C terraform/envs/$$env lint || exit $$?; \ + done; \ + $(MAKE) -C terraform/global lint || exit $$? +.PHONY: lint-envs + +lint-modules: ## lint the terraform code in terraform/modules + @for module in $(MODULES); do \ + echo "terraform/modules/$$module"; \ + $(MAKE) -C terraform/modules/$$module lint || exit $$?; \ + done +.PHONY: lint-modules + +fmt: fmt-fogg fmt-accounts fmt-envs fmt-global fmt-modules ## format code in this repo +.PHONY: fmt + +fmt-fogg: + .fogg/bin/fogg fmt +.PHONY: fmt-fogg + +fmt-accounts: ## format code in terraform/accounts + @for account in $(ACCOUNTS); do \ + $(MAKE) -C terraform/accounts/$$account fmt || exit $$?; \ + done +.PHONY: fmt-accounts + +fmt-envs: ## format the code in teraform/envs + @for env in $(ENVS); do \ + $(MAKE) -C terraform/envs/$$env fmt || exit $$?; \ + done +.PHONY: fmt-envs + +fmt-global: ## format the code in terraform/global + $(MAKE) -C terraform/global fmt || exit $$? +.PHONY: fmt-global + +fmt-modules: ## format the code in terraform/modules + @for module in $(MODULES); do \ + $(MAKE) -C terraform/modules/$$module fmt || exit $$?; \ + done +.PHONY: fmt-modules + +docs: docs-envs docs-global docs-modules ## update docs +.PHONY: docs + +docs-accounts: ## update docs in terraform/accounts + @for account in $(ACCOUNTS); do \ + $(MAKE) -C terraform/accounts/$$account docs || exit $$?; \ + done +.PHONY: docs-accounts + +docs-envs: ## update the docs in terraform/envs + @for env in $(ENVS); do \ + $(MAKE) -C terraform/envs/$$env docs || exit $$?; \ + done +.PHONY: docs-envs + +docs-global: ## update the docs in terraform/global + $(MAKE) -C terraform/global docs || exit $$?; +.PHONY: docs-global + +docs-modules: ## update the docs in terraform/modules + @for module in $(MODULES); do \ + $(MAKE) -C terraform/modules/$$module docs || exit $$?; \ + done +.PHONY: docs-modules + +test: +.PHONY: test + +check: check-plan check-docs ## check all code in the repo +.PHONY: check + +check-plan: check-plan-accounts check-plan-envs check-plan-global ## check terraform plans across all components +.PHONY: check-plan + +check-plan-accounts: ## check terraform plans in terraform/accounts + @for account in $(ACCOUNTS); do \ + $(MAKE) -C terraform/accounts/$$account check-plan || exit $$?; \ + done +.PHONY: check-plan-accounts + +check-plan-envs: ## check terraform plans in terraform/envs + @for env in $(ENVS); do \ + $(MAKE) -C terraform/envs/$$env check-plan || exit $$?; \ + done +.PHONY: check-plan-envs + +check-plan-global: ## check terraform plans in terraform/global + $(MAKE) -C terraform/global check-plan || exit $$? +.PHONY: check-plan-global + +check-docs: ## check terraform docs + @for mod in $(MODULES); do \ + $(MAKE) -C terraform/modules/$$module check-docs || exit $$?; \ + done +.PHONY: check-docs + +clean: clean-accounts clean-envs clean-global ## clean the repo +.PHONY: clean + +clean-accounts: ## clean in terraform/accounts + @for account in $(ACCOUNTS); do \ + $(MAKE) -C terraform/accounts/$$account clean || exit $$?; \ + done +.PHONY: clean-accounts + +clean-envs: ## clean in terraform/envs + @for env in $(ENVS); do \ + $(MAKE) -C terraform/envs/$$env clean || exit $$?; \ + done +.PHONY: clean-envs + +clean-global: ## clean in terraform/global + $(MAKE) -C terraform/global clean || exit $$? +.PHONY: clean-global + +help: ## display help for this makefile + @fgrep -h "##" $(MAKEFILE_LIST) | fgrep -v fgrep | sed -e 's/\\$$//' | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' +.PHONY: help + diff --git a/testdata/v2_envtio_components/README.md b/testdata/v2_envtio_components/README.md new file mode 100644 index 000000000..e69de29bb diff --git a/testdata/v2_envtio_components/atlantis.yaml b/testdata/v2_envtio_components/atlantis.yaml new file mode 100644 index 000000000..6fac70ea9 --- /dev/null +++ b/testdata/v2_envtio_components/atlantis.yaml @@ -0,0 +1,22 @@ +# Auto-generated by fogg. Do not edit +# Make improvements in fogg, so that everyone can benefit. + +version: 3 +projects: + - name: test_lambda + dir: terraform/envs/test/lambda + workspace: default + terraform_version: 1.7.5 + autoplan: + when_modified: + - '!remote-states.tf' + - '**/*.tf' + - '**/*.tf.json' + - '**/*.tfvars' + - '**/*.tfvars.json' + enabled: true + apply_requirements: + - approved +automerge: true +parallel_apply: true +parallel_plan: true diff --git a/testdata/v2_envtio_components/fogg.yml b/testdata/v2_envtio_components/fogg.yml new file mode 100644 index 000000000..261f26b37 --- /dev/null +++ b/testdata/v2_envtio_components/fogg.yml @@ -0,0 +1,53 @@ +version: 2 +defaults: + backend: + bucket: buck + role: role + account_id: 123456789012 + region: us-west-2 + owner: foo@example.com + project: proj + tools: + atlantis: + enabled: true + version: 3 + automerge: true + parallel_plan: true + parallel_apply: true + providers: + aws: + account_id: 123456789012 + role: role + region: us-west-2 + version: 0.12.0 + additional_providers: + shared_services: + region: ap-southeast-1 + account_id: 210987654321 + terraform_version: 1.7.5 +turbo: + enabled: true +envs: + test: + components: + lambda: + providers: + aws: + default_tags: + enabled: true + tags: + team: TIES + kind: envtio # required to ensure AwsSpec is used + cdktf_dependencies: + - name: "@types/aws-lambda" + version: "^8.10.76" + +modules: + my-envtio-module: + kind: cdktf # this is still a CDKTF module + package_name: "my-envtio-module-foo" + cdktf_dependencies: + - name: "@cdktf/provider-aws" + version: "^19.34.0" + - name: "@envtio/base" + version: "0.0.7" diff --git a/testdata/v2_envtio_components/package.json b/testdata/v2_envtio_components/package.json new file mode 100644 index 000000000..95e38f6fc --- /dev/null +++ b/testdata/v2_envtio_components/package.json @@ -0,0 +1,20 @@ +{ + "name": "fogg-monorepo", + "version": "0.0.0", + "type": "module", + "scripts": { + "synth": "TURBO_SCM_BASE='main' turbo run synth --affected" + }, + "engines": { + "node": "20" + }, + "devDependencies": { + "@types/node": "^20.6.0", + "cdktf-vitest": "^0.1.2", + "turbo": "^2.1.1", + "typescript": "^5.4.0", + "vitest": "^2.0.5" + }, + "packageManager": "pnpm@9.7.0", + "//" : "Auto-generated by fogg. Do not edit" +} diff --git a/testdata/v2_envtio_components/pnpm-workspace.yaml b/testdata/v2_envtio_components/pnpm-workspace.yaml new file mode 100644 index 000000000..3dff3611f --- /dev/null +++ b/testdata/v2_envtio_components/pnpm-workspace.yaml @@ -0,0 +1,6 @@ +# Auto-generated by fogg. Do not edit +# Make improvements in fogg, so that everyone can benefit. + +packages: + - terraform/envs/test/lambda + - terraform/modules/my-envtio-module diff --git a/testdata/v2_envtio_components/scripts/common.mk b/testdata/v2_envtio_components/scripts/common.mk new file mode 100644 index 000000000..24d2fb08a --- /dev/null +++ b/testdata/v2_envtio_components/scripts/common.mk @@ -0,0 +1,32 @@ +# Auto-generated by fogg. Do not edit +# Make improvements in fogg, so that everyone can benefit. + +SHELL=/bin/bash -o pipefail +TF_VARS := $(patsubst %,-e%,$(filter TF_VAR_%,$(.VARIABLES))) +REPO_ROOT := $(shell git rev-parse --show-toplevel) +REPO_RELATIVE_PATH := $(shell git rev-parse --show-prefix) +AUTO_APPROVE := false +export AWS_SDK_LOAD_CONFIG := 1 + +TFENV_DIR ?= $(REPO_ROOT)/.fogg/tfenv +export PATH :=$(TFENV_DIR)/libexec:$(TFENV_DIR)/versions/$(TERRAFORM_VERSION)/:$(REPO_ROOT)/.fogg/bin:$(PATH) +export TF_PLUGIN_CACHE_DIR=$(REPO_ROOT)/.terraform.d/plugin-cache +export TF_IN_AUTOMATION=1 +terraform_command ?= $(TFENV_DIR)/versions/$(TERRAFORM_VERSION)/terraform + +ifeq ($(TF_BACKEND_KIND),remote) + REFRESH := true +else + REFRESH := false +endif + + +tfenv: ## install the tfenv tool + @if [ ! -d ${TFENV_DIR} ]; then \ + git clone -q https://github.com/tfutils/tfenv.git $(TFENV_DIR); \ + fi +.PHONY: tfenv + +terraform: tfenv ## ensure that the proper version of terraform is installed + @${TFENV_DIR}/bin/tfenv install $(TERRAFORM_VERSION) +.PHONY: terraform diff --git a/testdata/v2_envtio_components/scripts/component.mk b/testdata/v2_envtio_components/scripts/component.mk new file mode 100644 index 000000000..234cac54b --- /dev/null +++ b/testdata/v2_envtio_components/scripts/component.mk @@ -0,0 +1,127 @@ +# Auto-generated by fogg. Do not edit +# Make improvements in fogg, so that everyone can benefit. + +SELF_DIR:=$(shell dirname $(realpath $(lastword $(MAKEFILE_LIST)))) +CHECK_PLANFILE_PATH ?= check-plan.output + +include $(SELF_DIR)/common.mk + +all: +.PHONY: all + +setup: ## set up local dependencies for this repo + $(MAKE) -C $(REPO_ROOT) setup +.PHONY: setup + +check: lint check-plan ## run all checks for this component +.PHONY: check + +fmt: terraform ## format code in this component + $(terraform_command) fmt $(TF_ARGS) +.PHONY: fmt + +lint: lint-terraform-fmt lint-tflint ## run all linters for this component +.PHONY: lint + +lint-tflint: ## run the tflint linter for this component + @printf "tflint: " +ifeq ($(TFLINT_ENABLED),1) + @tflint -c $(REPO_ROOT)/.tflint.hcl || exit $$?; +else + @echo "disabled" +endif +.PHONY: lint-tflint + +lint-terraform-fmt: terraform ## run `terraform fmt` in check mode + $(terraform_command) fmt $(TF_ARGS) --check=true --diff=true +.PHONY: lint-terraform-fmt + +check-auth: check-auth-aws check-auth-heroku ## check that authentication is properly set up for this component +.PHONY: check-auth + +check-auth-aws: + @for p in $(AWS_BACKEND_PROFILE) $(AWS_PROVIDER_PROFILE); do \ + aws --profile $$p sts get-caller-identity > /dev/null || (echo "AWS AUTH error. This component is configured to use a profile named '$$p'. Please add one to your ~/.aws/config" && exit -1); \ + done + @for r in $(AWS_BACKEND_ROLE_ARN) $(AWS_PROVIDER_ROLE_ARN); do \ + aws sts assume-role --role-arn $$r --role-session-name fogg-auth-test > /dev/null || (echo "AWS AUTH error. This component is configured to use a role named '$$r'." && exit -1); \ + done +.PHONY: check-auth-aws + +check-auth-heroku: +ifeq ($(HEROKU_PROVIDER),1) + @echo "Checking heroku auth..." + @if command heroku >/dev/null; then \ + heroku auth:whoami || timeout 15 heroku auth:login || (echo "Not authenticated to heroku. For SSO accounts, run 'heroku login', for non-sso accounts set HEROKU_EMAIL and HEROKU_API_KEY" && exit -1); \ + else \ + echo "Heroku CLI not installed, can't check auth."; \ + fi +endif +.PHONY: check-auth-heroku + +refresh: + @if [ "$(TF_BACKEND_KIND)" != "remote" ]; then \ + $(terraform_command) refresh $(TF_ARGS); \ + date +%s > .terraform/refreshed_at; \ + else \ + echo "remote backend does not support the refresh command, skipping"; \ + fi +.PHONY: refresh + +refresh-cached: + @last_refresh=`cat .terraform/refreshed_at 2>/dev/null || echo '0'`; \ + current_time=`date +%s`; \ + if (( current_time - last_refresh > 600 )); then \ + echo "It has been awhile since the last refresh. It is time."; \ + $(MAKE) refresh; \ + else \ + echo "Not time to refresh yet."; \ + fi; +.PHONY: refresh-cached + +plan: check-auth init fmt refresh-cached ## run a terraform plan + $(terraform_command) plan $(TF_ARGS) -refresh=$(REFRESH) -input=false +.PHONY: plan + +apply: check-auth init refresh ## run a terraform apply + @$(terraform_command) apply $(TF_ARGS) -refresh=$(REFRESH) -auto-approve=$(AUTO_APPROVE) +.PHONY: apply + +docs: + echo +.PHONY: docs + +clean: ## clean modules and plugins for this component + -rm -rfv .terraform/modules + -rm -rfv .terraform/plugins +.PHONY: clean + +test: +.PHONY: test + +init: terraform check-auth ## run terraform init for this component + @$(terraform_command) init -input=false +.PHONY: init + +check-plan: check-auth init refresh-cached ## run a terraform plan and check that it does not fail + @if [ "$(TF_BACKEND_KIND)" != "remote" ]; then \ + $(terraform_command) plan $(TF_ARGS) -detailed-exitcode -lock=false -refresh=$(REFRESH) -out=$(CHECK_PLANFILE_PATH) ; \ + ERR=$$?; \ + rm $(CHECK_PLANFILE_PATH) 2>/dev/null; \ + else \ + $(terraform_command) plan $(TF_ARGS) -detailed-exitcode -lock=false; \ + ERR=$$?; \ + fi; \ + if [ $$ERR -eq 0 ] ; then \ + echo "Success"; \ + elif [ $$ERR -eq 1 ] ; then \ + echo "Error in plan execution."; \ + exit 1; \ + elif [ $$ERR -eq 2 ] ; then \ + echo "Diff"; \ + fi; +.PHONY: check-plan + +run: check-auth ## run an arbitrary terraform command, CMD. ex `make run CMD='show'` + @$(terraform_command) $(CMD) +.PHONY: run diff --git a/testdata/v2_envtio_components/scripts/failed_output_only b/testdata/v2_envtio_components/scripts/failed_output_only new file mode 100644 index 000000000..a21d3dab3 --- /dev/null +++ b/testdata/v2_envtio_components/scripts/failed_output_only @@ -0,0 +1,13 @@ +#!/bin/sh + +t=`mktemp` && \ +exec $@ 2>&1 > $t || \ +( + a=$?; + echo $a; + cat $t; + rm $t; + exit $a +) + + diff --git a/testdata/v2_envtio_components/scripts/module.mk b/testdata/v2_envtio_components/scripts/module.mk new file mode 100644 index 000000000..3de233977 --- /dev/null +++ b/testdata/v2_envtio_components/scripts/module.mk @@ -0,0 +1,44 @@ +# Auto-generated by fogg. Do not edit +# Make improvements in fogg, so that everyone can benefit. + +SELF_DIR:=$(shell dirname $(realpath $(lastword $(MAKEFILE_LIST)))) + +include $(SELF_DIR)/common.mk + +all: fmt lint doc +.PHONY: all + +fmt: terraform ## run terraform fmt on this module + @$(terraform_command) fmt $(TF_ARGS) +.PHONY: fmt + +check: lint check-docs ## run all checks on this module +.PHONY: check + +lint: lint-tf check-docs ## run all linters on this module +.PHONY: lint + +lint-tf: terraform ## run terraform linters on this module + $(terraform_command) fmt $(TF_ARGS) --check=true --diff=true +.PHONY: lint-tf + +readme: ## update this module's README.md + bash $(REPO_ROOT)/scripts/update-readme.sh update +.PHONY: readme + +docs: readme ## update all docs for this module +.PHONY: docs + +check-docs: ## check that this module's docs are up-to-date + @bash $(REPO_ROOT)/scripts/update-readme.sh check; \ + if [ ! $$? -eq 0 ]; then \ + echo "Docs are out of date, run \`make docs\`"; \ + exit 1 ; \ + fi +.PHONY: check-docs + +clean: +.PHONY: clean + +test: +.PHONY: test diff --git a/testdata/v2_envtio_components/scripts/update-readme.sh b/testdata/v2_envtio_components/scripts/update-readme.sh new file mode 100644 index 000000000..17ec2e294 --- /dev/null +++ b/testdata/v2_envtio_components/scripts/update-readme.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +# Auto-generated by fogg. Do not edit +# Make improvements in fogg, so that everyone can benefit. + +# I would have written this directly in the Makefile, but that was difficult. + +CMD="$1" + +TMP=$(mktemp) +TMP2=$(mktemp) +terraform-docs md . >"$TMP" +sed '/^$/,//{//!d;}' README.md | sed "/^$/r $TMP" >$TMP2 + +case "$CMD" in +update) + mv $TMP2 README.md + ;; +check) + diff $TMP2 README.md >/dev/null + ;; +esac + +exit $? diff --git a/testdata/v2_envtio_components/terraform.d/.keep b/testdata/v2_envtio_components/terraform.d/.keep new file mode 100644 index 000000000..e69de29bb diff --git a/testdata/v2_envtio_components/terraform/envs/test/Makefile b/testdata/v2_envtio_components/terraform/envs/test/Makefile new file mode 100644 index 000000000..88a602741 --- /dev/null +++ b/testdata/v2_envtio_components/terraform/envs/test/Makefile @@ -0,0 +1,51 @@ +# Auto-generated by fogg. Do not edit +# Make improvements in fogg, so that everyone can benefit. + +COMPONENTS=lambda + +all: +.PHONY: all + +lint: ## lint all components in the env + @for c in $(COMPONENTS); do \ + echo $$c; \ + $(MAKE) -C $$c lint || exit $$? ; \ + done +.PHONY: lint + +fmt: ## format terraform code in all components in this env + @for c in $(COMPONENTS); do \ + echo $$c; \ + $(MAKE) -C $$c fmt || exit $$? ; \ + done +.PHONY: fmt + +check-plan: ## check all plans in this environment + @for c in $(COMPONENTS); do \ + echo $$c; \ + $(MAKE) -C $$c check-plan || exit $$? ; \ + done +.PHONY: check-plan + +docs: ## generate docs for all components in this env + @for c in $(COMPONENTS); do \ + echo $$c; \ + $(MAKE) -C $$c docs || exit $$? ; \ + done +.PHONY: docs + +clean: ## clean all components in this env + @for c in $(COMPONENTS); do \ + echo $$c; \ + $(MAKE) -C $$c clean || exit $$? ; \ + done +.PHONY: clean + +test: +.PHONY: test + + +help: ## display help for this makefile + @fgrep -h "##" $(MAKEFILE_LIST) | fgrep -v fgrep | sed -e 's/\\$$//' | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' +.PHONY: help + diff --git a/testdata/v2_envtio_components/terraform/envs/test/README.md b/testdata/v2_envtio_components/terraform/envs/test/README.md new file mode 100644 index 000000000..e69de29bb diff --git a/testdata/v2_envtio_components/terraform/envs/test/lambda/.fogg-component.yaml b/testdata/v2_envtio_components/terraform/envs/test/lambda/.fogg-component.yaml new file mode 100644 index 000000000..f0bb5e3d3 --- /dev/null +++ b/testdata/v2_envtio_components/terraform/envs/test/lambda/.fogg-component.yaml @@ -0,0 +1,293 @@ +path_to_repo_root: ../../../../ +terraform_version: 1.7.5 +account_backends: {} +all_accounts: {} +backend: + kind: s3 + s3: + account_id: "123456789012" + account_name: "" + bucket: buck + dynamo_table: null + key_path: terraform/proj/envs/test/components/lambda.tfstate + profile: null + region: us-west-2 + role_arn: arn:aws:iam::123456789012:role/role +component_backends: + lambda: + kind: s3 + s3: + account_id: "123456789012" + account_name: "" + bucket: buck + dynamo_table: null + key_path: terraform/proj/envs/test/components/lambda.tfstate + profile: null + region: us-west-2 + role_arn: arn:aws:iam::123456789012:role/role +autoplan_relative_globs: [] +autoplan_files: [] +locals_block: {} +component_backends_filtered: false +env: test +extra_vars: {} +name: lambda +owner: foo@example.com +project: proj +providers_configuration: + assert: + enabled: true + version: 0.0.1 + auth0: null + aws: + account_id: "123456789012" + alias: null + profile: null + region: us-west-2 + role_arn: arn:aws:iam::123456789012:role/role + default_tags: + enabled: true + tags: + team: TIES + ignore_tags: + enabled: false + aws_regional_providers: + - account_id: "210987654321" + alias: shared_services + profile: null + region: ap-southeast-1 + role_arn: arn:aws:iam::210987654321:role/role + default_tags: + enabled: true + tags: + team: TIES + ignore_tags: + enabled: false + bless: null + datadog: null + github: null + grafana: null + heroku: null + kubernetes: null + okta: null + sentry: null + snowflake: null + tfe: null + sops: null +required_providers: {} +provider_versions: + archive: + source: hashicorp/archive + version: ~> 2.0 + assert: + source: bwoznicki/assert + version: 0.0.1 + aws: + source: hashicorp/aws + version: 0.12.0 + local: + source: hashicorp/local + version: ~> 2.0 + "null": + source: hashicorp/null + version: ~> 3.0 + okta-head: + source: okta/okta + version: ~> 3.30 + random: + source: hashicorp/random + version: ~> 3.4 + tls: + source: hashicorp/tls + version: ~> 3.0 +integration_registry: null +cdktf_dependencies: + '@cdktf/provider-aws': ^19.29.0 + '@cdktf/provider-cloudflare': ^11.16.0 + '@cdktf/provider-datadog': ^11.8.0 + '@envtio/base': 0.0.7 + '@types/aws-lambda': ^8.10.76 + cdktf: ^0.20.8 + constructs: ^10.3.0 + js-yaml: ^4.1.0 +cdktf_dev_dependencies: + '@swc/core': ^1.7.6 + '@types/js-yaml': ^4.0.9 + '@types/node': ^20.6.0 + '@typescript-eslint/eslint-plugin': ^8 + '@typescript-eslint/parser': ^8 + eslint: ^8 + eslint-config-prettier: ^9.1.0 + eslint-import-resolver-typescript: ^3.6.1 + eslint-plugin-import: ^2.29.1 + eslint-plugin-prettier: ^5.2.1 + prettier: ^3.3.3 + ts-node: ^10.9.2 + typescript: ^5.4.0 +tf_lint: + enabled: false +travisci: + cicomponent: + enabled: false + buildevents: false + awsprofilename: "" + awsrolename: "" + awsregion: "" + awsaccountid: "" + command: "" +circleci: + cicomponent: + enabled: false + buildevents: false + awsprofilename: "" + awsrolename: "" + awsregion: "" + awsaccountid: "" + command: "" + sshfingerprints: [] +githubactionsci: + cicomponent: + enabled: false + buildevents: false + awsprofilename: "" + awsrolename: "" + awsregion: "" + awsaccountid: "" + command: "" +kind: envtio +module_source: null +module_name: null +module_for_each: null +providers: {} +variables: [] +outputs: [] +modules: [] +global: + path_to_repo_root: ../../ + terraform_version: 1.7.5 + account_backends: {} + all_accounts: {} + backend: + kind: s3 + s3: + account_id: "123456789012" + account_name: "" + bucket: buck + dynamo_table: null + key_path: terraform/proj/global.tfstate + profile: null + region: us-west-2 + role_arn: arn:aws:iam::123456789012:role/role + component_backends: {} + autoplan_relative_globs: [] + autoplan_files: [] + locals_block: {} + component_backends_filtered: false + env: "" + extra_vars: {} + name: global + owner: foo@example.com + project: proj + providers_configuration: + assert: + enabled: true + version: 0.0.1 + auth0: null + aws: + account_id: "123456789012" + alias: null + profile: null + region: us-west-2 + role_arn: arn:aws:iam::123456789012:role/role + default_tags: + enabled: false + ignore_tags: + enabled: false + aws_regional_providers: + - account_id: "210987654321" + alias: shared_services + profile: null + region: ap-southeast-1 + role_arn: arn:aws:iam::210987654321:role/role + default_tags: + enabled: false + ignore_tags: + enabled: false + bless: null + datadog: null + github: null + grafana: null + heroku: null + kubernetes: null + okta: null + sentry: null + snowflake: null + tfe: null + sops: null + required_providers: {} + provider_versions: + archive: + source: hashicorp/archive + version: ~> 2.0 + assert: + source: bwoznicki/assert + version: 0.0.1 + aws: + source: hashicorp/aws + version: 0.12.0 + local: + source: hashicorp/local + version: ~> 2.0 + "null": + source: hashicorp/null + version: ~> 3.0 + okta-head: + source: okta/okta + version: ~> 3.30 + random: + source: hashicorp/random + version: ~> 3.4 + tls: + source: hashicorp/tls + version: ~> 3.0 + integration_registry: null + cdktf_dependencies: {} + cdktf_dev_dependencies: {} + tf_lint: + enabled: false + travisci: + cicomponent: + enabled: false + buildevents: false + awsprofilename: "" + awsrolename: "" + awsregion: "" + awsaccountid: "" + command: "" + circleci: + cicomponent: + enabled: false + buildevents: false + awsprofilename: "" + awsrolename: "" + awsregion: "" + awsaccountid: "" + command: "" + sshfingerprints: [] + githubactionsci: + cicomponent: + enabled: false + buildevents: false + awsprofilename: "" + awsrolename: "" + awsregion: "" + awsaccountid: "" + command: "" + module_source: null + module_name: null + module_for_each: null + providers: {} + variables: [] + outputs: [] + modules: [] + global: null diff --git a/testdata/v2_envtio_components/terraform/envs/test/lambda/Makefile b/testdata/v2_envtio_components/terraform/envs/test/lambda/Makefile new file mode 100644 index 000000000..3cab7dadb --- /dev/null +++ b/testdata/v2_envtio_components/terraform/envs/test/lambda/Makefile @@ -0,0 +1,34 @@ +# Auto-generated by fogg. Do not edit +# Make improvements in fogg, so that everyone can benefit. + +lint: + pnpm run eslint +.PHONY: lint + +fmt: + pnpm run prettier +.PHONY: fmt + +check-plan: cdk.tf.json + terraform init + terraform plan +.PHONY: check-plan + +docs: +.PHONY: docs + +clean: + rm -rf cdk.tf.json + rm -rf assets + rm -rf node_modules +.PHONY: clean + +cdk.tf.json: + pnpm i + pnpm turbo synth +.PHONY: cdk.tf.json + +check-updates: + pnpx npm-check-updates@latest --target=minor + @echo "WARNING: update fogg.yml for target package versions" +.PHONY: check-updates diff --git a/testdata/v2_envtio_components/terraform/envs/test/lambda/README.md b/testdata/v2_envtio_components/terraform/envs/test/lambda/README.md new file mode 100644 index 000000000..d81ffebb4 --- /dev/null +++ b/testdata/v2_envtio_components/terraform/envs/test/lambda/README.md @@ -0,0 +1,5 @@ +# Envtio Component + +```console +make check-plan +``` diff --git a/testdata/v2_envtio_components/terraform/envs/test/lambda/package.json b/testdata/v2_envtio_components/terraform/envs/test/lambda/package.json new file mode 100644 index 000000000..3134b55f2 --- /dev/null +++ b/testdata/v2_envtio_components/terraform/envs/test/lambda/package.json @@ -0,0 +1,37 @@ +{ + "name": "test-lambda", + "author": "foo@example.com", + "version": "0.0.0", + "private": true, + "scripts": { + "eslint": "eslint . --ext .ts", + "prettier": "prettier --write .", + "synth": "npx ts-node --swc -P ./tsconfig.dev.json src/index.ts" + }, + "dependencies": { + "@cdktf/provider-aws": "^19.29.0", + "@cdktf/provider-cloudflare": "^11.16.0", + "@cdktf/provider-datadog": "^11.8.0", + "@envtio/base": "0.0.7", + "@types/aws-lambda": "^8.10.76", + "cdktf": "^0.20.8", + "constructs": "^10.3.0", + "js-yaml": "^4.1.0" + }, + "devDependencies": { + "@swc/core": "^1.7.6", + "@types/js-yaml": "^4.0.9", + "@types/node": "^20.6.0", + "@typescript-eslint/eslint-plugin": "^8", + "@typescript-eslint/parser": "^8", + "eslint": "^8", + "eslint-config-prettier": "^9.1.0", + "eslint-import-resolver-typescript": "^3.6.1", + "eslint-plugin-import": "^2.29.1", + "eslint-plugin-prettier": "^5.2.1", + "prettier": "^3.3.3", + "ts-node": "^10.9.2", + "typescript": "^5.4.0" + }, + "//" : "Auto-generated by fogg. Do not edit" +} diff --git a/testdata/v2_envtio_components/terraform/envs/test/lambda/src/helpers/fogg-stack.ts b/testdata/v2_envtio_components/terraform/envs/test/lambda/src/helpers/fogg-stack.ts new file mode 100644 index 000000000..83316c31e --- /dev/null +++ b/testdata/v2_envtio_components/terraform/envs/test/lambda/src/helpers/fogg-stack.ts @@ -0,0 +1,334 @@ +// Auto-generated by fogg. Do not edit +// Make improvements in fogg, so that everyone can benefit. + +import * as fs from "fs"; +import { provider as awsProvider } from "@cdktf/provider-aws"; +import { provider as cloudflareProvider } from "@cdktf/provider-cloudflare"; +import { provider as dataDogProvider } from "@cdktf/provider-datadog"; +import { AwsSpec, AwsSpecProps } from "@envtio/base/lib/aws"; +import { + S3Backend, + S3BackendConfig, + DataTerraformRemoteState, + DataTerraformRemoteStateS3, + DataTerraformRemoteStateS3Config, + TerraformHclModule, + TerraformLocal, +} from "cdktf"; +import { Construct } from "constructs"; +import * as yaml from "js-yaml"; + +import { + Component, + Backend, + AWSProvider, + DatadogProvider, + GenericProvider, +} from "./fogg-types.generated"; + +export interface FoggStackProps extends Omit { + /** + * Force remote state configuration + * @default false - only configure remote states if `component_backends_filtered` is true + */ + forceRemoteStates?: boolean; +} + +export function loadComponentConfig(): Component { + const file = fs.readFileSync(`.fogg-component.yaml`, "utf8"); + const componentConfig = yaml.load(file) as Component; + return replaceNullWithUndefined(componentConfig); +} + +/** + * Helper stack to wrap Fogg component configuration and set up configured providers and backends. + */ +export class FoggStack extends AwsSpec { + public readonly foggComp: Component; + public readonly modules: Record = {}; + public readonly locals: Record = {}; + private readonly _remoteStates: Record = {}; + + constructor(scope: Construct, id: string, props: FoggStackProps) { + const foggComp = loadComponentConfig(); + const awsProviderConfig = parseAwsProviderConfig(foggComp); + super(scope, id, { + ...props, + providerConfig: awsProviderConfig, + }); + this.foggComp = foggComp; + + this.parseBackendConfig(); + this.parseBundledProviderConfig(); + for (const p of Object.values(this.foggComp.required_providers)) { + if (p.enabled) this.parseGenericProviderConfig(p); + } + // parse remote backends + const forceRemoteBackend = props.forceRemoteStates ?? false; + if (forceRemoteBackend || this.foggComp.component_backends_filtered) { + for (const [name, remoteStateConfig] of Object.entries( + this.foggComp.component_backends, + )) { + this.parseRemoteState(name, remoteStateConfig); + } + } + this.parseLocalsBlock(); + this.parseModules(); + } + + /** + * Set variables for the main module defined in the fogg component configuration. + * + * @param variables - The variables to set for the module + */ + public setMainModuleVariables(variables: Record): void { + const id = (this.foggComp.module_name = + this.foggComp.module_name ?? "main"); + this.setModuleVariables(id, variables); + } + + /** + * Return a remote state defined in the fogg component configuration. + * @param name - The name of the remote state to get + * @returns the DataTerraformRemoteState object + * @throws if the remote state is not found + */ + public remoteState(name: string): DataTerraformRemoteState { + if (!this._remoteStates[name]) { + throw new Error(`Remote state ${name} not found`); + } + return this._remoteStates[name]; + } + + /** + * Set variables for a module included in the fogg component modules[] configuration. + * + * @param name - The module name as defined in the fogg component configuration + * @param variables - The variables to set for the module + */ + public setModuleVariables( + name: string, + variables: Record, + ): void { + if (!this.modules[name]) { + throw new Error(`Module ${name} not found`); + } + for (const [key, value] of Object.entries(variables)) { + this.modules[name].set(key, value); + } + } + + /** + * Get a local defined in the fogg component configuration. + * + * @param name the name of the local to get + * @returns the TerraformLocal object + */ + public getLocal(name: string): TerraformLocal { + if (!this.locals[name]) { + throw new Error(`Local ${name} not found`); + } + return this.locals[name]; + } + + private parseBackendConfig(): void { + if (this.foggComp.backend.kind === "s3" && this.foggComp.backend.s3) { + const s3Config = this.foggComp.backend.s3; + let s3BackendConfig: Mutable = { + bucket: s3Config.bucket, + dynamodbTable: s3Config.dynamo_table, + key: s3Config.key_path, + region: s3Config.region, + encrypt: true, + }; + if (s3Config.profile) { + s3BackendConfig.profile = s3Config.profile; + } else if (s3Config.role_arn) { + s3BackendConfig.assumeRole = { + roleArn: s3Config.role_arn, + }; + } + // console.log( + // `Setting S3 backend Config ${JSON.stringify(s3BackendConfig, null, 2)}` + // ); + new S3Backend(this, s3BackendConfig); + } else { + throw new Error( + `Unsupported backend configuration ${this.foggComp.backend.kind}`, + ); + } + } + + private parseRemoteState(id: string, remoteConfig: Backend): void { + if (remoteConfig.kind === "s3" && remoteConfig.s3) { + const s3Config = remoteConfig.s3; + let remoteStateConfig: Mutable = { + bucket: s3Config.bucket, + dynamodbTable: s3Config.dynamo_table, + key: s3Config.key_path, + region: s3Config.region, + encrypt: true, + }; + if (s3Config.profile) { + remoteStateConfig.profile = s3Config.profile; + } else if (s3Config.role_arn) { + remoteStateConfig.assumeRole = { + roleArn: s3Config.role_arn, + }; + } + this._remoteStates[id] = new DataTerraformRemoteStateS3( + this, + id, + remoteStateConfig, + ); + } else { + throw new Error(`Unsupported backend configuration ${remoteConfig.kind}`); + } + } + + private parseBundledProviderConfig(): void { + const providers = this.foggComp.providers_configuration; + if (providers.datadog) { + this.parseDataDogProviderConfig(providers.datadog); + } + } + + private parseGenericProviderConfig(config: GenericProvider): void { + switch (config.source) { + case "cloudflare/cloudflare": + this.parseCloudflareProviderConfig(config); + break; + default: + throw new Error(`Unsupported provider ${config.source}`); + } + } + + private parseLocalsBlock() { + if (this.foggComp.locals_block) { + for (const [key, value] of Object.entries(this.foggComp.locals_block)) { + this.locals[key] = new TerraformLocal(this, key, `\${${value}}`); + } + } + } + + private parseModules() { + if (this.foggComp.module_source) { + const id = (this.foggComp.module_name = + this.foggComp.module_name ?? "main"); + this.modules[id] = new TerraformHclModule(this, id, { + source: this.foggComp.module_source, + }); + } + + for (let i = 0; i < this.foggComp.modules.length; i++) { + const moduleConfig = this.foggComp.modules[i]; + const id = moduleConfig.name ?? `module_${i}`; + if (!moduleConfig.source) { + console.warn(`Module ${id} does not have a source, skipping`); + continue; + } + if (this.modules[id]) { + throw new Error(`Module ${id} already exists`); + } + if (!moduleConfig.name) { + console.log( + `Module ${moduleConfig.source} does not have a name, using ${id}`, + ); + } + this.modules[id] = new TerraformHclModule(this, id, { + source: moduleConfig.source, + version: moduleConfig.version, + }); + // TODO: Add validation for module variables + // TODO: Export module outputs + } + } + + private parseDataDogProviderConfig(_config: DatadogProvider): void { + new dataDogProvider.DatadogProvider(this, "datadog", {}); + } + + private parseCloudflareProviderConfig(_config: GenericProvider): void { + new cloudflareProvider.CloudflareProvider(this, "cloudflare", {}); + } +} + +// Parse AWS Provider config from Fogg component configuration +function parseAwsProviderConfig( + foggComp: Component, +): awsProvider.AwsProviderConfig { + const providers = foggComp.providers_configuration; + if ( + providers.aws_regional_providers && + providers.aws_regional_providers.length > 0 + ) { + throw new Error( + "AWS regional providers are not supported by envtio components", + ); + } + if (providers.aws) { + return getAwsProviderConfig(foggComp, providers.aws); + } + throw new Error("AWS provider configuration not found"); +} + +function getAwsProviderConfig( + foggComp: Component, + config: AWSProvider, +): awsProvider.AwsProviderConfig { + const c: Mutable = { + region: config.region, + alias: config.alias, + }; + if (config.default_tags && config.default_tags.enabled) { + c.defaultTags = [ + { + tags: { + env: foggComp.env, + owner: foggComp.owner, + project: foggComp.project, + managedBy: "terraform", + service: foggComp.name, + ...(foggComp.backend.s3?.key_path && { + tfstateKey: foggComp.backend.s3?.key_path, + }), + ...(foggComp.providers_configuration?.aws?.default_tags?.enabled && + (foggComp.providers_configuration?.aws?.default_tags?.tags ?? {})), + }, + }, + ]; + } + if (config.profile) { + c.profile = config.profile; + } else if (config.role_arn) { + c.assumeRole = [ + { + roleArn: config.role_arn, + }, + ]; + } + return c; +} + +// helper type to make readonly interface properties mutable +type Mutable = { + -readonly [P in keyof T]: T[P]; +}; + +// helper function to replace fogg "null" values with "undefined" +function replaceNullWithUndefined(obj: any): any { + if (obj === null) { + return undefined; + } + if (Array.isArray(obj)) { + return obj.map(replaceNullWithUndefined); + } + if (typeof obj === "object" && obj !== null) { + const newObj: any = {}; + for (const key in obj) { + newObj[key] = replaceNullWithUndefined(obj[key]); + } + return newObj; + } + return obj; +} diff --git a/testdata/v2_envtio_components/terraform/envs/test/lambda/src/helpers/fogg-types.generated.ts b/testdata/v2_envtio_components/terraform/envs/test/lambda/src/helpers/fogg-types.generated.ts new file mode 100644 index 000000000..b936f2b96 --- /dev/null +++ b/testdata/v2_envtio_components/terraform/envs/test/lambda/src/helpers/fogg-types.generated.ts @@ -0,0 +1,250 @@ +/* Do not change, this code is generated from Golang structs */ + + +export interface IntegrationRegistryMap { + format?: string; + drop_component?: boolean; + path?: string; + for_each?: boolean; + path_for_each?: string; +} +export interface ModuleIntegrationConfig { + mode?: string; + format?: string; + drop_prefix?: boolean; + drop_component?: boolean; + path_infix?: string; + providers?: string[]; + outputs_map?: {[key: string]: IntegrationRegistryMap}; +} +export interface ComponentModule { + source?: string; + version?: string; + name?: string; + prefix?: string; + variables?: string[]; + outputs?: string[]; + integration?: ModuleIntegrationConfig; + providers?: {[key: string]: string}; + for_each?: string; + depends_on?: string[]; +} +export interface EKSConfig { + cluster_name: string; +} +export interface GitHubActionsComponent { + Enabled: boolean; + Buildevents: boolean; + AWSProfileName: string; + AWSRoleName: string; + AWSRegion: string; + AWSAccountID: string; + Command: string; +} +export interface CircleCIComponent { + Enabled: boolean; + Buildevents: boolean; + AWSProfileName: string; + AWSRoleName: string; + AWSRegion: string; + AWSAccountID: string; + Command: string; + SSHFingerprints: string[]; +} +export interface TravisCIComponent { + Enabled: boolean; + Buildevents: boolean; + AWSProfileName: string; + AWSRoleName: string; + AWSRegion: string; + AWSAccountID: string; + Command: string; +} +export interface TfLint { + enabled: boolean; +} +export interface ProviderVersion { + source: string; + version?: string; +} +export interface GenericProvider { + custom_provider?: boolean; + enabled?: boolean; + version?: string; + source: string; + config: {[key: string]: any}; +} +export interface SopsProvider { + custom_provider?: boolean; + enabled?: boolean; + version?: string; +} +export interface TfeProvider { + custom_provider?: boolean; + enabled?: boolean; + version?: string; + hostname?: string; +} +export interface SnowflakeProvider { + custom_provider?: boolean; + enabled?: boolean; + version?: string; + account?: string; + role?: string; + region?: string; +} +export interface SentryProvider { + custom_provider?: boolean; + enabled?: boolean; + version?: string; + base_url?: string; +} +export interface OktaProvider { + custom_provider?: boolean; + enabled?: boolean; + version?: string; + org_name?: string; + base_url?: string; +} +export interface KubernetesProvider { + custom_provider?: boolean; + enabled?: boolean; + version?: string; +} +export interface HerokuProvider { + custom_provider?: boolean; + enabled?: boolean; + version?: string; +} +export interface GrafanaProvider { + custom_provider?: boolean; + enabled?: boolean; + version?: string; +} +export interface GithubProvider { + custom_provider?: boolean; + enabled?: boolean; + version?: string; + organization: string; + base_url?: string; +} +export interface DatadogProvider { + custom_provider?: boolean; + enabled?: boolean; + version?: string; +} +export interface BlessProvider { + custom_provider?: boolean; + enabled?: boolean; + version?: string; + additional_regions?: string[]; + aws_profile?: string; + aws_region?: string; + role_arn?: string; +} +export interface AWSProviderIgnoreTags { + enabled?: boolean; + keys?: string[]; + key_prefixes?: string[]; +} +export interface AWSProviderDefaultTags { + enabled?: boolean; + tags?: {[key: string]: string}; +} +export interface AWSProvider { + account_id: string; + alias?: string; + profile?: string; + region: string; + role_arn?: string; + default_tags?: AWSProviderDefaultTags; + ignore_tags?: AWSProviderIgnoreTags; +} +export interface Auth0Provider { + custom_provider?: boolean; + enabled?: boolean; + version?: string; + domain?: string; +} +export interface AssertProvider { + custom_provider?: boolean; + enabled?: boolean; + version?: string; +} +export interface ProviderConfiguration { + assert?: AssertProvider; + auth0?: Auth0Provider; + aws?: AWSProvider; + aws_regional_providers: AWSProvider[]; + bless?: BlessProvider; + datadog?: DatadogProvider; + github?: GithubProvider; + grafana?: GrafanaProvider; + heroku?: HerokuProvider; + kubernetes?: KubernetesProvider; + okta?: OktaProvider; + sentry?: SentryProvider; + snowflake?: SnowflakeProvider; + tfe?: TfeProvider; + sops?: SopsProvider; +} +export interface Number { + +} +export interface RemoteBackend { + host_name: string; + organization: string; + workspace: string; +} +export interface S3Backend { + account_id?: string; + account_name: string; + bucket: string; + dynamo_table?: string; + key_path: string; + profile?: string; + region: string; + role_arn?: string; +} +export interface Backend { + kind: string; + s3?: S3Backend; + remote?: RemoteBackend; +} +export interface Component { + path_to_repo_root: string; + terraform_version: string; + account_backends: {[key: string]: Backend}; + all_accounts: {[key: string]: Number}; + backend: Backend; + component_backends: {[key: string]: Backend}; + autoplan_relative_globs: string[]; + autoplan_files: string[]; + locals_block: {[key: string]: any}; + component_backends_filtered: boolean; + env: string; + extra_vars: {[key: string]: string}; + name: string; + owner: string; + project: string; + providers_configuration: ProviderConfiguration; + required_providers: {[key: string]: GenericProvider}; + provider_versions: {[key: string]: ProviderVersion}; + integration_registry?: string; + cdktf_dependencies: {[key: string]: string}; + cdktf_dev_dependencies: {[key: string]: string}; + tf_lint: TfLint; + TravisCI: TravisCIComponent; + CircleCI: CircleCIComponent; + GitHubActionsCI: GitHubActionsComponent; + eks?: EKSConfig; + kind?: string; + module_source?: string; + module_name?: string; + module_for_each?: string; + providers: {[key: string]: string}; + variables: string[]; + outputs: string[]; + modules: ComponentModule[]; + global?: Component; +} \ No newline at end of file diff --git a/testdata/v2_envtio_components/terraform/envs/test/lambda/src/index.ts b/testdata/v2_envtio_components/terraform/envs/test/lambda/src/index.ts new file mode 100644 index 000000000..8e8980fba --- /dev/null +++ b/testdata/v2_envtio_components/terraform/envs/test/lambda/src/index.ts @@ -0,0 +1,26 @@ +// Auto-generated by fogg. Do not edit +// Make improvements in fogg, so that everyone can benefit. + +import * as fs from "fs"; +import * as path from "path"; +import { App } from "cdktf"; +import { ComponentStack } from "./stack"; + +const outdir = "cdktf.out"; +const app = new App({ + outdir, +}); +const stack = new ComponentStack(app, "Default"); +app.synth(); + +// Copy the generated Terraform code to the current directory to keep relative directory references +const stackSynthDir = path.join( + outdir, + app.manifest.forStack(stack).workingDirectory, +); + +fs.cpSync(stackSynthDir, ".", { recursive: true }); +fs.rmSync(outdir, { recursive: true }); +if (process.env.CI) { + fs.renameSync("cdk.tf.json", "ci-cdk.tf.json"); +} diff --git a/testdata/v2_envtio_components/terraform/envs/test/lambda/src/stack.ts b/testdata/v2_envtio_components/terraform/envs/test/lambda/src/stack.ts new file mode 100644 index 000000000..727666096 --- /dev/null +++ b/testdata/v2_envtio_components/terraform/envs/test/lambda/src/stack.ts @@ -0,0 +1,42 @@ +// // Use provider-aws constructs directly +// import { dataAwsAvailabilityZones } from "@cdktf/provider-aws"; +// // Or use CDKTF constructs directly from npm +// import { MyConstruct } from "@handshakes/my-construct"; +import { Construct } from "constructs"; +import { FoggStack } from "./helpers/fogg-stack"; + +export class ComponentStack extends FoggStack { + constructor(scope: Construct, id: string) { + super(scope, id, { + forceRemoteStates: false, + // Prefix for resource UUID, should never change for component lifecycle + gridUUID: "123-456", + // This is used as a tag and may change over time + environmentName: "development", + }); + + // // Configure fogg module variables here + // this.setModuleVariables("main", { + // foo: "bar", + // baz: "qux", + // tags: { + // // Use fogg component configuration values + // Project: this.foggComp.project, + // Owner: this.foggComp.owner, + // Environment: this.foggComp.env, + // }, + // }); + + // // Or: create AWS Resources + // const azs = new dataAwsAvailabilityZones.DataAwsAvailabilityZones( + // this, + // "azs", + // {} + // ); + + // // Or: use custom constructs + // new MyConstruct(this, "my-construct", { + // foo: "bar", + // }); + } +} diff --git a/testdata/v2_envtio_components/terraform/envs/test/lambda/tsconfig.dev.json b/testdata/v2_envtio_components/terraform/envs/test/lambda/tsconfig.dev.json new file mode 100644 index 000000000..5fd187593 --- /dev/null +++ b/testdata/v2_envtio_components/terraform/envs/test/lambda/tsconfig.dev.json @@ -0,0 +1,31 @@ +{ + "compilerOptions": { + "alwaysStrict": true, + + "experimentalDecorators": true, + "inlineSourceMap": true, + "inlineSources": true, + "lib": ["es2018"], + "module": "CommonJS", + "noFallthroughCasesInSwitch": true, + "noImplicitAny": true, + "noImplicitReturns": true, + "noImplicitThis": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "resolveJsonModule": true, + "strict": true, + "strictNullChecks": true, + "strictPropertyInitialization": true, + "stripInternal": true, + "incremental": true, + "skipLibCheck": true, + "declaration": false, + "esModuleInterop": true, + "noEmitOnError": false, + "target": "ES2019", + "outDir": "dist" + }, + "include": ["src/**/*.ts", "test/**/*.ts"], + "exclude": ["node_modules", "cdktf.out"] +} diff --git a/testdata/v2_envtio_components/terraform/foo-fooFoo.yaml b/testdata/v2_envtio_components/terraform/foo-fooFoo.yaml new file mode 100644 index 000000000..e69de29bb diff --git a/testdata/v2_envtio_components/terraform/global/Makefile b/testdata/v2_envtio_components/terraform/global/Makefile new file mode 100644 index 000000000..82b45328c --- /dev/null +++ b/testdata/v2_envtio_components/terraform/global/Makefile @@ -0,0 +1,24 @@ +# Auto-generated by fogg. Do not edit +# Make improvements in fogg, so that everyone can benefit. + + +export TERRAFORM_VERSION := 1.7.5 +export TFLINT_ENABLED := 0 +export TF_PLUGIN_CACHE_DIR := ../..//.terraform.d/plugin-cache +export TF_BACKEND_KIND := s3 + + +export AWS_BACKEND_ROLE_ARN := arn:aws:iam::123456789012:role/role + + +export AWS_PROVIDER_ROLE_ARN := arn:aws:iam::123456789012:role/role + + + +include ../..//scripts/component.mk + + +help: ## display help for this makefile + @fgrep -h "##" $(MAKEFILE_LIST) | fgrep -v fgrep | sed -e 's/\\$$//' | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' +.PHONY: help + diff --git a/testdata/v2_envtio_components/terraform/global/README.md b/testdata/v2_envtio_components/terraform/global/README.md new file mode 100644 index 000000000..e69de29bb diff --git a/testdata/v2_envtio_components/terraform/global/fogg.tf b/testdata/v2_envtio_components/terraform/global/fogg.tf new file mode 100644 index 000000000..8624e2742 --- /dev/null +++ b/testdata/v2_envtio_components/terraform/global/fogg.tf @@ -0,0 +1,126 @@ +# Auto-generated by fogg. Do not edit +# Make improvements in fogg, so that everyone can benefit. +provider "aws" { + + region = "us-west-2" + + + assume_role { + role_arn = "arn:aws:iam::123456789012:role/role" + } + + allowed_account_ids = ["123456789012"] +} +# Aliased Providers (for doing things in every region). + + +provider "aws" { + alias = "shared_services" + region = "ap-southeast-1" + + + assume_role { + role_arn = "arn:aws:iam::210987654321:role/role" + } + + allowed_account_ids = ["210987654321"] +} + + +provider "assert" {} +terraform { + required_version = "=1.7.5" + + backend "s3" { + + bucket = "buck" + + key = "terraform/proj/global.tfstate" + encrypt = true + region = "us-west-2" + + assume_role = { + role_arn = "arn:aws:iam::123456789012:role/role" + } + + } + required_providers { + archive = { + source = "hashicorp/archive" + version = "~> 2.0" + } + assert = { + source = "bwoznicki/assert" + version = "0.0.1" + } + aws = { + source = "hashicorp/aws" + version = "0.12.0" + } + local = { + source = "hashicorp/local" + version = "~> 2.0" + } + null = { + source = "hashicorp/null" + version = "~> 3.0" + } + okta-head = { + source = "okta/okta" + version = "~> 3.30" + } + random = { + source = "hashicorp/random" + version = "~> 3.4" + } + tls = { + source = "hashicorp/tls" + version = "~> 3.0" + } + } +} +# tflint-ignore: terraform_unused_declarations +variable "env" { + type = string + default = "" +} +# tflint-ignore: terraform_unused_declarations +variable "project" { + type = string + default = "proj" +} +# tflint-ignore: terraform_unused_declarations +variable "region" { + type = string + default = "us-west-2" +} +# tflint-ignore: terraform_unused_declarations +variable "component" { + type = string + default = "global" +} +# tflint-ignore: terraform_unused_declarations +variable "owner" { + type = string + default = "foo@example.com" +} +# tflint-ignore: terraform_unused_declarations +variable "tags" { + type = object({ project : string, env : string, service : string, owner : string, managedBy : string, tfstateKey : string }) + default = { + project = "proj" + env = "" + service = "global" + owner = "foo@example.com" + tfstateKey = "terraform/proj/global.tfstate" + + managedBy = "terraform" + } +} +# tflint-ignore: terraform_unused_declarations +variable "aws_accounts" { + type = map(string) + default = { + + } +} diff --git a/testdata/v2_envtio_components/terraform/global/main.tf b/testdata/v2_envtio_components/terraform/global/main.tf new file mode 100644 index 000000000..e69de29bb diff --git a/testdata/v2_envtio_components/terraform/global/outputs.tf b/testdata/v2_envtio_components/terraform/global/outputs.tf new file mode 100644 index 000000000..e69de29bb diff --git a/testdata/v2_envtio_components/terraform/global/remote-states.tf b/testdata/v2_envtio_components/terraform/global/remote-states.tf new file mode 100644 index 000000000..8c7ad42aa --- /dev/null +++ b/testdata/v2_envtio_components/terraform/global/remote-states.tf @@ -0,0 +1,2 @@ +# Auto-generated by fogg. Do not edit +# Make improvements in fogg, so that everyone can benefit. diff --git a/testdata/v2_envtio_components/terraform/global/terraform.d b/testdata/v2_envtio_components/terraform/global/terraform.d new file mode 120000 index 000000000..a1f7d0d3e --- /dev/null +++ b/testdata/v2_envtio_components/terraform/global/terraform.d @@ -0,0 +1 @@ +../../terraform.d \ No newline at end of file diff --git a/testdata/v2_envtio_components/terraform/global/variables.tf b/testdata/v2_envtio_components/terraform/global/variables.tf new file mode 100644 index 000000000..e69de29bb diff --git a/testdata/v2_envtio_components/terraform/modules/my-envtio-module/.eslintrc.json b/testdata/v2_envtio_components/terraform/modules/my-envtio-module/.eslintrc.json new file mode 100644 index 000000000..ef31c15c6 --- /dev/null +++ b/testdata/v2_envtio_components/terraform/modules/my-envtio-module/.eslintrc.json @@ -0,0 +1,79 @@ +{ + "env": { + "jest": true, + "node": true + }, + "root": true, + "plugins": ["@typescript-eslint", "import"], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": 2018, + "sourceType": "commonjs", + "project": "./tsconfig.dev.json" + }, + "extends": ["plugin:import/typescript", "plugin:prettier/recommended"], + "settings": { + "import/parsers": { + "@typescript-eslint/parser": [".ts", ".tsx"] + }, + "import/resolver": { + "node": {}, + "typescript": { + "project": "./tsconfig.dev.json", + "alwaysTryTypes": true + } + } + }, + "ignorePatterns": ["*.js", "*.d.ts", "node_modules/", "*.generated.ts"], + "rules": { + "@typescript-eslint/no-require-imports": ["error"], + "import/no-extraneous-dependencies": [ + "error", + { + "devDependencies": ["**/test/**"], + "optionalDependencies": false, + "peerDependencies": true + } + ], + "import/no-unresolved": ["error"], + "import/order": [ + "warn", + { + "groups": ["builtin", "external"], + "alphabetize": { + "order": "asc", + "caseInsensitive": true + } + } + ], + "no-duplicate-imports": ["error"], + "no-shadow": ["off"], + "@typescript-eslint/no-shadow": ["error"], + "key-spacing": ["error"], + "no-multiple-empty-lines": ["error"], + "@typescript-eslint/no-floating-promises": ["error"], + "no-return-await": ["off"], + "@typescript-eslint/return-await": ["error"], + "no-trailing-spaces": ["error"], + "dot-notation": ["error"], + "no-bitwise": ["error"], + "@typescript-eslint/member-ordering": [ + "error", + { + "default": [ + "public-static-field", + "public-static-method", + "protected-static-field", + "protected-static-method", + "private-static-field", + "private-static-method", + "field", + "constructor", + "method" + ] + } + ], + "eol-last": ["error", "always"], + "space-in-parens": ["error", "never"] + } +} diff --git a/testdata/v2_envtio_components/terraform/modules/my-envtio-module/.gitattributes b/testdata/v2_envtio_components/terraform/modules/my-envtio-module/.gitattributes new file mode 100644 index 000000000..0b506f19a --- /dev/null +++ b/testdata/v2_envtio_components/terraform/modules/my-envtio-module/.gitattributes @@ -0,0 +1,3 @@ +/.eslintrc.json linguist-generated +/.prettierrc.json linguist-generated +/.gitattributes linguist-generated diff --git a/testdata/v2_envtio_components/terraform/modules/my-envtio-module/.gitignore b/testdata/v2_envtio_components/terraform/modules/my-envtio-module/.gitignore new file mode 100644 index 000000000..630ac23b8 --- /dev/null +++ b/testdata/v2_envtio_components/terraform/modules/my-envtio-module/.gitignore @@ -0,0 +1,20 @@ +*.d.ts +*.js +node_modules +cdktf.out +cdktf.log +*terraform.*.tfstate* +.terraform +.terraform.lock.hcl +tsconfig.tsbuildinfo +!vitest.config.ts +!vitest.setup.ts + +// build outputs +dist + +// test outputs +coverage +/test-reports/ +junit.xml +/coverage/ diff --git a/testdata/v2_envtio_components/terraform/modules/my-envtio-module/.prettierrc.json b/testdata/v2_envtio_components/terraform/modules/my-envtio-module/.prettierrc.json new file mode 100644 index 000000000..84c85a388 --- /dev/null +++ b/testdata/v2_envtio_components/terraform/modules/my-envtio-module/.prettierrc.json @@ -0,0 +1,3 @@ +{ + "overrides": [] +} diff --git a/testdata/v2_envtio_components/terraform/modules/my-envtio-module/Makefile b/testdata/v2_envtio_components/terraform/modules/my-envtio-module/Makefile new file mode 100644 index 000000000..f30b74b0b --- /dev/null +++ b/testdata/v2_envtio_components/terraform/modules/my-envtio-module/Makefile @@ -0,0 +1,33 @@ +# Auto-generated by fogg. Do not edit +# Make improvements in fogg, so that everyone can benefit. + +lint: + pnpm run eslint +.PHONY: lint + +fmt: + pnpm run prettier +.PHONY: fmt + +docs: +.PHONY: docs + +test: + pnpm run test +.PHONY: test + +clean: + rm -rf cdk.tf.json + rm -rf assets + rm -rf node_modules +.PHONY: clean + +cdk.tf.json: + pnpm i + pnpm turbo synth +.PHONY: cdk.tf.json + +check-updates: + pnpx npm-check-updates@latest --target=minor + @echo "WARNING: update fogg.yml for target package versions" +.PHONY: check-updates diff --git a/testdata/v2_envtio_components/terraform/modules/my-envtio-module/README.md b/testdata/v2_envtio_components/terraform/modules/my-envtio-module/README.md new file mode 100644 index 000000000..644ade658 --- /dev/null +++ b/testdata/v2_envtio_components/terraform/modules/my-envtio-module/README.md @@ -0,0 +1,10 @@ +# CDKTF Module + +```console +make synth +``` + +## Usage + + + diff --git a/testdata/v2_envtio_components/terraform/modules/my-envtio-module/cdktf.json b/testdata/v2_envtio_components/terraform/modules/my-envtio-module/cdktf.json new file mode 100644 index 000000000..bbba6edf2 --- /dev/null +++ b/testdata/v2_envtio_components/terraform/modules/my-envtio-module/cdktf.json @@ -0,0 +1,7 @@ +{ + "language": "typescript", + "codeMakerOutput": "src/.gen", + "app": "npx ts-node --swc -P ./tsconfig.dev.json src/index.ts", + "terraformProviders": [], + "terraformModules": [] +} diff --git a/testdata/v2_envtio_components/terraform/modules/my-envtio-module/package.json b/testdata/v2_envtio_components/terraform/modules/my-envtio-module/package.json new file mode 100644 index 000000000..576ebad3b --- /dev/null +++ b/testdata/v2_envtio_components/terraform/modules/my-envtio-module/package.json @@ -0,0 +1,40 @@ +{ + "name": "my-envtio-module-foo", + "version": "0.0.0", + "private": true, + "exports": { + ".": "./src/index.ts" + }, + "scripts": { + "test": "vitest run --passWithNoTests --update", + "eslint": "eslint . --ext .ts", + "prettier": "prettier --write .", + "synth": "npx ts-node --swc -P ./tsconfig.dev.json src/index.ts" + }, + "dependencies": { + "@cdktf/provider-aws": "^19.34.0", + "@envtio/base": "0.0.7" + }, + "devDependencies": { + "@swc/core": "^1.7.6", + "@types/node": "^20.6.0", + "@typescript-eslint/eslint-plugin": "^8", + "@typescript-eslint/parser": "^8", + "cdktf": "^0.20.8", + "constructs": "^10.3.0", + "eslint": "^8", + "eslint-config-prettier": "^9.1.0", + "eslint-import-resolver-typescript": "^3.6.1", + "eslint-plugin-import": "^2.29.1", + "eslint-plugin-prettier": "^5.2.1", + "prettier": "^3.3.3", + "ts-node": "^10.9.2", + "typescript": "^5.4.0", + "vitest": "^2.0.5" + }, + "peerDependencies": { + "cdktf": "^0.20.8", + "constructs": "^10.3.0" + }, + "//": "Auto-generated by fogg. Do not edit" +} diff --git a/testdata/v2_envtio_components/terraform/modules/my-envtio-module/src/index.ts b/testdata/v2_envtio_components/terraform/modules/my-envtio-module/src/index.ts new file mode 100644 index 000000000..767de6009 --- /dev/null +++ b/testdata/v2_envtio_components/terraform/modules/my-envtio-module/src/index.ts @@ -0,0 +1,38 @@ +// // Or publish cdktf module as HCL module for non CDKTF usage +// import { TFModuleStack, TFModuleVariable, ProviderRequirement } from "@cdktf/tf-module-stack"; +import { + TerraformElement, + // TerraformHclModule, +} from "cdktf"; +import { Construct } from "constructs"; + +export interface MyConstructProps { + foo?: string; +} + +/** + * This is an example construct you may change as required. + */ +export class MyConstruct extends TerraformElement { + private readonly foo: string; + constructor(scope: Construct, id: string, props?: MyConstructProps) { + super(scope, id); + + this.foo = props?.foo ?? "Default"; + + // // create AWS Resources + // const azs = new dataAwsAvailabilityZones.DataAwsAvailabilityZones( + // this, + // "azs", + // {} + // ); + + // // Or: use custom constructs + // new MyConstruct(this, "my-construct", { + // foo: "bar", + // }); + + // // Or use community TF modules + // https://developer.hashicorp.com/terraform/cdktf/create-and-deploy/configuration-file#declare-providers-and-modules + } +} diff --git a/testdata/v2_envtio_components/terraform/modules/my-envtio-module/tsconfig.dev.json b/testdata/v2_envtio_components/terraform/modules/my-envtio-module/tsconfig.dev.json new file mode 100644 index 000000000..1785694b8 --- /dev/null +++ b/testdata/v2_envtio_components/terraform/modules/my-envtio-module/tsconfig.dev.json @@ -0,0 +1,32 @@ +{ + "compilerOptions": { + "alwaysStrict": true, + + "experimentalDecorators": true, + "inlineSourceMap": true, + "inlineSources": true, + "lib": ["es2018"], + "module": "CommonJS", + "noFallthroughCasesInSwitch": true, + "noImplicitAny": true, + "noImplicitReturns": true, + "noImplicitThis": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "resolveJsonModule": true, + "strict": true, + "strictNullChecks": true, + "strictPropertyInitialization": true, + "stripInternal": true, + "incremental": true, + "skipLibCheck": true, + "declaration": false, + "esModuleInterop": true, + "noEmitOnError": false, + "target": "ES2019", + "outDir": "dist", + "types": ["cdktf-vitest"] + }, + "include": ["src/**/*.ts", "test/**/*.ts"], + "exclude": ["node_modules", "cdktf.out"] +} diff --git a/testdata/v2_envtio_components/turbo.json b/testdata/v2_envtio_components/turbo.json new file mode 100644 index 000000000..135ead91c --- /dev/null +++ b/testdata/v2_envtio_components/turbo.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://turbo.build/schema.json", + "tasks": { + "synth": { + "inputs": ["./src/**/*", "./.fogg-component.yaml"], + "outputs": ["cdk.tf.json", "ci-cdk.tf.json", "assets/**"] + } + } +} diff --git a/testdata/v2_envtio_components/vitest.config.ts b/testdata/v2_envtio_components/vitest.config.ts new file mode 100644 index 000000000..2a42025b8 --- /dev/null +++ b/testdata/v2_envtio_components/vitest.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from "vitest/config"; + +// refer to: https://vitest.dev/config/ +export default defineConfig({ + test: { + // clearMocks: true, + // globals: true, + isolate: false, + setupFiles: [`${__dirname}/vitest.setup.ts`], + }, +}); diff --git a/testdata/v2_envtio_components/vitest.setup.ts b/testdata/v2_envtio_components/vitest.setup.ts new file mode 100644 index 000000000..be3fd7141 --- /dev/null +++ b/testdata/v2_envtio_components/vitest.setup.ts @@ -0,0 +1,4 @@ +// refer to: https://github.com/duniul/cdktf-vitest +import { setupCdktfVitest } from "cdktf-vitest"; + +setupCdktfVitest();