From abbc8015879115ab2f2fb5f3002a8cafd8e8f56d Mon Sep 17 00:00:00 2001 From: oscar-hernandez-nttd <335457@nttdata.com> Date: Mon, 20 Jul 2026 16:17:16 -0600 Subject: [PATCH 1/2] feat!: convert to copier-managed skeleton --- .agents/references/primitive/creation.md | 40 ++ .../references/primitive/release-history.md | 67 +++ .agents/references/primitive/standards.md | 101 +++++ .agents/references/primitive/testing.md | 62 +++ .../reference-architecture/release-history.md | 51 +++ .../reference-architecture/standards.md | 72 +++ .../reference-architecture/testing.md | 43 ++ .../shared/terraform-module-standards.md | 55 +++ .agents/skills/primitive-module/SKILL.md | 39 ++ .../skills/reference-architecture/SKILL.md | 37 ++ .claude/rules/agent-routing.md | 9 + .claude/rules/terraform-files.md | 15 + .copier-answers.yml | 5 + .cursor/rules/launch-agent-routing.mdc | 11 + .cursor/rules/primitive-module-workflow.mdc | 18 + .../rules/reference-architecture-workflow.mdc | 17 + .../agents/primitive-module-creator.agent.md | 24 + .../reference-architecture-creator.agent.md | 23 + .github/dependabot.yml | 44 +- .github/release-drafter.yml | 56 ++- .github/scripts/validate-readonly-test.sh | 87 ++++ .github/workflows/pull-request-automation.yml | 14 + .../pull-request-check-terraform.yml | 20 + .../pull-request-dependabot-automerge.yml | 14 + .github/workflows/pull-request-label.yml | 13 +- .../pull-request-precommit-checks.yml | 78 ++++ .../pull-request-terraform-check-aws.yml | 22 - .github/workflows/rebuild-cache-on-merge.yml | 56 +++ .github/workflows/release-publish.yml | 2 +- .github/workflows/update-from-skeleton.yml | 24 + .gitignore | 17 +- .golangci.yaml | 5 + .lcafenv | 23 - .pre-commit-config.yaml | 44 ++ .tflint.hcl | 13 + .tool-versions | 9 +- AGENTS.md | 36 ++ CLAUDE.md | 9 + CODEOWNERS | 2 +- Makefile | 427 ++++++++++++++---- README.md | 94 ++-- examples/source_from_folder/README.md | 22 +- examples/source_from_zip/README.md | 22 +- 43 files changed, 1573 insertions(+), 269 deletions(-) create mode 100644 .agents/references/primitive/creation.md create mode 100644 .agents/references/primitive/release-history.md create mode 100644 .agents/references/primitive/standards.md create mode 100644 .agents/references/primitive/testing.md create mode 100644 .agents/references/reference-architecture/release-history.md create mode 100644 .agents/references/reference-architecture/standards.md create mode 100644 .agents/references/reference-architecture/testing.md create mode 100644 .agents/references/shared/terraform-module-standards.md create mode 100644 .agents/skills/primitive-module/SKILL.md create mode 100644 .agents/skills/reference-architecture/SKILL.md create mode 100644 .claude/rules/agent-routing.md create mode 100644 .claude/rules/terraform-files.md create mode 100644 .copier-answers.yml create mode 100644 .cursor/rules/launch-agent-routing.mdc create mode 100644 .cursor/rules/primitive-module-workflow.mdc create mode 100644 .cursor/rules/reference-architecture-workflow.mdc create mode 100644 .github/agents/primitive-module-creator.agent.md create mode 100644 .github/agents/reference-architecture-creator.agent.md create mode 100755 .github/scripts/validate-readonly-test.sh create mode 100644 .github/workflows/pull-request-automation.yml create mode 100644 .github/workflows/pull-request-check-terraform.yml create mode 100644 .github/workflows/pull-request-dependabot-automerge.yml create mode 100644 .github/workflows/pull-request-precommit-checks.yml delete mode 100644 .github/workflows/pull-request-terraform-check-aws.yml create mode 100644 .github/workflows/rebuild-cache-on-merge.yml create mode 100644 .github/workflows/update-from-skeleton.yml create mode 100644 .golangci.yaml delete mode 100644 .lcafenv create mode 100644 .pre-commit-config.yaml create mode 100644 .tflint.hcl create mode 100644 AGENTS.md create mode 100644 CLAUDE.md diff --git a/.agents/references/primitive/creation.md b/.agents/references/primitive/creation.md new file mode 100644 index 0000000..8ca20ed --- /dev/null +++ b/.agents/references/primitive/creation.md @@ -0,0 +1,40 @@ +# Primitive Module Creation and Cleanup + +Use this reference when converting the skeleton into a primitive module or reviewing a conversion. + +## Creation Flow + +1. Confirm the provider and one target resource type. Review the provider documentation and a similar primitive for that provider. +2. For AWS, consult the official service API reference when practical. Translate documented ranges, enums, formats, and cross-field constraints into variable validation and descriptions. If no suitable public API reference is found after a reasonable search, proceed using provider documentation rather than retrying indefinitely. +3. Implement `versions.tf`, `variables.tf`, `main.tf`, and `outputs.tf` using the primitive standards. +4. Build `examples/complete/` with its Terraform files, an accurate README, resource naming, and any deployable prerequisite resources. +5. Before writing cloud-backed tests, run the available example validation flow: formatting and linting, init, validate, plan, apply, and destroy. Resolve failures before continuing when credentials and environment access permit. +6. Add Terratest coverage, then run the Go quality checks in the testing reference before cloud-backed test execution. +7. Build root `README.md` from `TEMPLATED_README.md`, replace the module-specific title and overview, add usage, retain the development boilerplate, and populate terraform-docs. +8. Run the cleanup and completion checks below. + +## Root and Example Documentation + +- Do not write root `README.md` from scratch. Start from `TEMPLATED_README.md` and preserve Module Development, Pre-Requisites, Pre-Commit Hooks, Local Validation, Review and Merge Process, and Automatic Updates. +- Do not remove `TEMPLATED_README.md` until all of its required sections have been incorporated into the root README. +- The generated terraform-docs block in the root README must not be empty. Generate it when available; otherwise supply accurate inputs and outputs tables. +- The handwritten usage block in `examples/complete/README.md` must match `examples/complete/main.tf`, including resources, policies, dependencies, variables, inputs, and outputs. Update it whenever the example changes. + +## Skeleton Cleanup + +Before completion: + +- Delete `examples/with_cake/` when it is part of the skeleton. +- Update the Go module path and every test import from `launch-terraform-template` to the new module repository. +- Replace template comments in `tests/testimpl/`, including empty template settings comments. +- Search all Markdown and Go files for `TODO:` and template references. Remove or replace every placeholder, including the hooks documentation placeholder. +- Search for stale resource names, package names, copied snippets, imports, and generated provider files that should not be committed. +- Run `go mod tidy` after renaming imports or adding SDK dependencies. + +## Completion Checklist + +- Root variables have explicit types, descriptions, required validation, and coherent optional objects. +- Root outputs exist in the provider schema, have descriptions, and match the intended composition interface. +- The complete example passes every root variable through, exposes test-consumed outputs, and uses the secure configuration. +- Root and example documentation are synchronized and complete. +- No skeleton resources, TODOs, template names, or stale imports remain. diff --git a/.agents/references/primitive/release-history.md b/.agents/references/primitive/release-history.md new file mode 100644 index 0000000..3f23436 --- /dev/null +++ b/.agents/references/primitive/release-history.md @@ -0,0 +1,67 @@ +# Primitive Module Guidance Release History + +This file preserves the learnings from the previous monolithic primitive module guide. + +## 2.1 + +- Skeleton updates now add the `.agents/`, `.claude/`, and `.cursor/` guidance trees. Reviewers of automatic update PRs should expect this additive template change. + +## 2.0 + +- Split the legacy GitHub agent guide into an agent-agnostic skill and focused reference files. The legacy file is a compatibility stub; operational standards, creation and cleanup guidance, test guidance, and historical rationale now live separately. + +## 1.13 + +- AWS primitive modules should consult official AWS API references where practical and translate documented ranges, enums, formats, and cross-field constraints into Terraform validations and variable descriptions. + +## 1.12 + +- Root `README.md` should be built from `TEMPLATED_README.md`; preserve skeleton boilerplate and only replace module-specific sections. + +## 1.11 + +- Optional object variables need cross-field validation for paired fields, conditional requirements, and contradictory sentinel values. + +## 1.10 + +- Add mechanical checks for loose test assertions and README/example drift. When `main.tf` changes, revisit the example README and tests. + +## 1.9 + +- Regula/OPA references were removed from the skeleton. Use the current unified Terraform check workflow and current Makefile targets. + +## 1.8 + +- Avoid Terraform reserved variable names. Verify output attributes against provider schema. Keep example outputs aligned with tests. Use account-scoped unique names where cloud resources require uniqueness. + +## 1.7 + +- Treat specific-value assertions, differentiated functional/readonly tests, security verification, README accuracy, and template cleanup as high-severity requirements. + +## 1.6 + +- Remove skeleton placeholders, generate terraform-docs output, require output descriptions, validate bounded inputs, and keep examples complete. + +## 1.5 + +- Examples should use security-first defaults and cleanup should search broadly for skeleton/template references. + +## 1.4 + +- Legacy GitHub agent files required frontmatter first to be recognized. + +## 1.3 + +- Agent guidance moved into `.github/agents/` in the older layout and gained a skeleton cleanup checklist. + +## 1.2 + +- Resource naming module usage was corrected: use `for_each = var.resource_names_map`, `class_env`, required `cloud_resource_type` and `maximum_length`, and output format references such as `module.resource_names["key"].format`. + +## 1.1 + +- Terratest should verify real resource state through provider APIs, not only Terraform outputs. + +## 1.0 + +- Initial primitive module guidance. diff --git a/.agents/references/primitive/standards.md b/.agents/references/primitive/standards.md new file mode 100644 index 0000000..df4f773 --- /dev/null +++ b/.agents/references/primitive/standards.md @@ -0,0 +1,101 @@ +# Primitive Module Standards + +Primitive modules wrap one cloud resource type and keep the interface reusable. They are comprehensive production wrappers, not minimal examples or opinionated architectures. + +## Architecture + +- Include one primary cloud resource type. +- Do not add business logic or multi-resource architecture behavior. +- Use a descriptive Terraform resource name based on the resource type; do not use `this`. +- Expose every non-deprecated, non-computed argument that a typical production deployment would configure. Optional attributes normally default to `null` so Terraform omits them unless chosen. +- Export useful resource attributes individually. Do not output the complete resource object. +- Keep secure defaults in the example, especially encryption and private access patterns where the provider supports them. + +## Required Structure + +```text +examples/complete/ + main.tf + variables.tf + outputs.tf + versions.tf + README.md +tests/ + post_deploy_functional/ + post_deploy_functional_readonly/ + testimpl/ +main.tf +variables.tf +outputs.tf +versions.tf +README.md +TEMPLATED_README.md +Makefile +go.mod +go.sum +``` + +## Provider Notes + +- Azure resources commonly use explicit `location`, `resource_group_name`, and nested configuration blocks. +- AWS resources commonly use `data.aws_region.current`, tags for grouping, and separate resources for versioning, policies, encryption, or logging when the provider models them separately. +- GCP resources commonly distinguish project, location, labels, and IAM binding patterns. + +## Variables + +- Give every variable an explicit type and useful description. +- Use `snake_case`, provider argument names where practical, and concise group headings for related inputs. +- Do not use Terraform-reserved names such as `source`, `target`, `version`, `count`, `for`, or `provider`. +- Required infrastructure inputs have no default. Optional flags should default to `false` or the safer option. Tags or labels should default to an empty map. +- Use `object()` with `optional()` attributes for structured optional configuration. +- Add validation for provider-enforced numeric bounds, enums, formats, mutually exclusive inputs, and cross-field requirements. +- Nullable validation expressions must avoid evaluating null values, for example with a conditional expression. Use `try()` for nested optional object attributes. +- Optional object descriptions must explain conditional field requirements and prohibited combinations. + +For every optional object, validate all of the following where applicable: + +- Individual enum, range, and format constraints. +- Fields that must be provided together. +- Fields required when another field is set or active. +- Fields prohibited by an off or disabled sentinel value. + +## Resources and Outputs + +- Map variables directly to resource arguments. Use dynamic blocks for optional nested blocks when the provider supports them. +- Avoid lifecycle blocks and data sources unless they are necessary for the resource contract. +- AWS often models configuration as separate resources rather than nested blocks; follow the provider schema. +- Place tags or labels at the end of resource blocks where the local provider convention supports it. +- Every output needs a short description and must reference an attribute verified in the provider schema. +- Use generic output names such as `id`, `name`, `arn`, `url`, or `fqdn`, without resource-type prefixes. +- When `id` is the same value as another output, say so in the `id` description. +- Do not mark outputs sensitive by default; callers handle sensitivity in their own interface. + +## Version Constraints + +- Set a Terraform version and provider constraints that avoid untested major upgrades. +- Pin providers to an appropriate compatible minor range, following the current skeleton and provider-specific precedent. +- Keep provider configuration out of the root module; examples own provider configuration. + +## AWS API Reference Check + +For AWS primitive modules, consult the official AWS service API reference when practical. Derive validation ranges, enum values, formats, and cross-field constraints from the API reference and provider schema. If no suitable public API reference is found after a reasonable search, skip the step and do not retry indefinitely. + +## Example Requirements + +- The example must pass through every root module variable. +- Mutually exclusive root variables should both be represented with coherent defaults. +- The example should demonstrate the secure pattern for the resource type. +- `examples/complete/README.md` usage must exactly match `examples/complete/main.tf`. +- Example outputs must expose every value used by tests. +- Use the Launch resource naming module correctly: `for_each = var.resource_names_map`, `class_env`, numeric `instance_env` and `instance_resource`, and each entry's `name` and `max_length` for `cloud_resource_type` and `maximum_length`. +- Read naming outputs by map key, for example `module.resource_names[""].standard`. Account-scoped names need a random suffix when concurrent or sequential tests could collide. +- Use the provider's regional convention for the naming module. AWS and GCP patterns may need hyphens removed from region names. + +## Common Anti-Patterns + +- Wrapping more than one primary resource type. +- Using `assert.NotEmpty` where a specific expected value is known. +- Copying functional tests into readonly tests unchanged. +- Leaving an empty terraform-docs block. +- Writing root `README.md` from scratch and dropping skeleton boilerplate. +- Leaving `TEMPLATED_README.md` content unincorporated. diff --git a/.agents/references/primitive/testing.md b/.agents/references/primitive/testing.md new file mode 100644 index 0000000..072d4e2 --- /dev/null +++ b/.agents/references/primitive/testing.md @@ -0,0 +1,62 @@ +# Primitive Module Testing + +Primitive tests must prove the Terraform interface and the cloud resource behavior. + +## Test Shape + +- `post_deploy_functional` performs write or behavior-changing operations where the resource type supports them. +- `post_deploy_functional_readonly` performs read-only verification only. +- The two test packages must call different implementation functions. +- Shared setup and provider clients belong in `tests/testimpl/`. +- Use `t.Parallel()` where the test framework and provisioned resources support concurrent execution. +- Functional tests deploy and clean up the complete example. Readonly tests assume deployed infrastructure. + +## Assertions + +- Assert specific expected values from Terraform inputs, computed naming outputs, provider API responses, or known example configuration. +- Avoid `assert.NotEmpty` and `require.NotEmpty` when the value is knowable. +- Use `require` for attributes that must exist before deeper assertions. +- Verify security settings through the cloud provider API when the module configures encryption, policies, public access controls, identity, or networking. +- For a required security API attribute, use `require.True(t, ok, ...)` before assertions rather than an `if ok` branch that silently skips a missing setting. +- The limited valid uses of `NotEmpty` are checking a collection before indexing it or verifying a required environment variable. Configuration and API-returned values should have known expected values. + +## Readonly Tests + +Readonly tests must not create, update, invoke mutating operations, publish messages, write objects, or alter state. + +## Readonly Test Runner + +The readonly package (`tests/post_deploy_functional_readonly`) must: + +- Call `lib.RunNonDestructiveTest`, not `lib.RunSetupTestTeardown`. The setup/teardown runner turns a readonly suite into a full apply, test, and destroy flow. +- Pass a `tests/testimpl` function whose name begins with `TestComposable`, such as `TestComposableCompleteReadOnly`. `lcaf-component-terratest` fails the test at runtime when the name does not meet this requirement. + +CI excludes the readonly binary from its test command, so both requirements must be correct by construction. + +## Functional Tests + +Functional tests should exercise the resource behavior, not just Terraform outputs. Examples include writing and reading data, invoking a function, publishing a message, checking access policies, or using the relevant provider SDK operation. + +## Go Quality + +- Keep provider SDK helpers small and purpose-focused. +- Run `go mod tidy` after adding SDK dependencies. +- Before cloud-backed test runs, run the available Go linter, `go get -u ./...`, `go mod tidy`, and `go build ./...`; resolve failures before running the wider test flow. +- Build or run targeted Go tests before cloud-backed runs when possible. +- Keep example outputs and Go expected values synchronized. + +## Provider State Verification + +- Verify both Terraform outputs and real cloud state. Prefer Terratest provider helpers, then use the provider SDK where no helper covers the resource. +- Read cloud credentials and region or project context from the environment. Fail clearly when required configuration is missing. +- Compare API values to Terraform outputs or known example values, including security configuration such as encryption, TLS, access policy, private networking, or identity settings. + +## Test Review Checklist + +- No configuration assertion uses `assert.NotEmpty` or `require.NotEmpty` when a specific value is available. +- Functional and readonly entrypoints call different `tests/testimpl/` functions and are not copies of each other. +- The readonly entrypoint uses `lib.RunNonDestructiveTest` and a `TestComposable*` implementation function. +- Functional coverage includes a safe write or behavior operation when the resource supports one. +- Readonly coverage performs no writes, invocation, publishing, resource creation, updates, or state changes. +- Security-critical provider attributes are required and compared to expected values. +- Test-consumed Terraform outputs exist in `examples/complete/outputs.tf`. diff --git a/.agents/references/reference-architecture/release-history.md b/.agents/references/reference-architecture/release-history.md new file mode 100644 index 0000000..a9f49c0 --- /dev/null +++ b/.agents/references/reference-architecture/release-history.md @@ -0,0 +1,51 @@ +# Reference Architecture Guidance Release History + +This file preserves the learnings from the previous monolithic reference architecture guide. + +## 3.1 + +- Skeleton updates now add the `.agents/`, `.claude/`, and `.cursor/` guidance trees. Reviewers of automatic update PRs should expect this additive template change. + +## 2.0 + +- Readonly tests must use non-destructive helpers. Add a skeleton transformation verification gate. Include Lambda source directories when examples need deployable source. Avoid duplicate CloudWatch log groups with `terraform-aws-modules/lambda/aws`. Use registry module source formats. Keep output prefixes consistent. Prefer `create_*` feature flags. Strengthen the ban on loose non-empty assertions. + +## 1.9 + +- Complete examples must not redundantly compose `resource_names`. Do not commit `providers.tf` in examples. Use `RunNonDestructiveTest` for readonly tests. Check community module compatibility before choosing versions. Remove stale skeleton names from go.mod, imports, and test functions. Avoid duplicate IAM permissions. + +## 1.8 + +- Strengthen specific-value assertions, differentiate functional and readonly tests, mandate KMS/encryption verification where configured, and require README tables to match actual code. + +## 1.7 + +- Keep cloud-provider guidance balanced. Add Azure networking and security patterns alongside AWS examples. + +## 1.6 + +- Provider SDK verification examples were added for AWS services and read-only versus destructive test flows. + +## 1.5 + +- SDK verification is mandatory for meaningful tests. IAM should use least privilege and avoid duplicated policy attachments. Community module versions need compatibility checks. + +## 1.4 + +- Legacy GitHub agent files required frontmatter first to be recognized. + +## 1.3 + +- Agent guidance moved into `.github/agents/` in the older layout and gained a skeleton cleanup checklist. + +## 1.2 + +- Resource naming module usage was corrected: use `for_each = var.resource_names_map`, `class_env`, required `cloud_resource_type` and `maximum_length`, output format references, and no obsolete `resource_names_strategy` variable. + +## 1.1 + +- Terratest should verify real resource state through provider APIs, and reference architecture tests should cover optional features enabled by examples. + +## 1.0 + +- Initial reference architecture guidance. diff --git a/.agents/references/reference-architecture/standards.md b/.agents/references/reference-architecture/standards.md new file mode 100644 index 0000000..f1ef87d --- /dev/null +++ b/.agents/references/reference-architecture/standards.md @@ -0,0 +1,72 @@ +# Reference Architecture Standards + +Reference architecture modules compose multiple primitives and selected community modules into complete, opinionated infrastructure patterns. + +## Architecture + +- Compose multiple resources or modules into a coherent pattern. +- Use Launch primitive modules from `terraform.registry.launch.nttdata.com` when available. +- Use mature public registry modules for complex AWS patterns when they reduce implementation risk. +- Use registry source addresses, not `git::` module URLs. +- Use bounded version constraints for internal and community modules. +- Keep feature flags named `create_*` for optional resources. + +## Required Structure + +```text +examples/complete/ + main.tf + variables.tf + outputs.tf + versions.tf + test.tfvars + README.md +tests/ +main.tf +variables.tf +outputs.tf +versions.tf +locals.tf +README.md +Makefile +go.mod +go.sum +``` + +## Composition + +- Start with the Launch resource naming module. +- Azure architectures normally create or consume a resource group and pass `location`. +- AWS architectures normally use `data.aws_region.current` and tags. +- Do not ask consumers to provide names for resources the architecture owns. +- Do not duplicate naming modules inside examples when the root module already handles naming. + +## Variables + +- Include naming context variables required by the resource naming module. +- Use high-level service variables rather than exposing every nested primitive argument directly. +- Validate mutually exclusive fields such as `name` and `name_prefix`. +- Complex object variables need validation for required field groups and contradictory combinations. +- Optional features should have explicit `create_*` booleans and object inputs with safe defaults. + +## Outputs + +- Expose aggregate identifiers, names, ARNs/IDs, endpoints, and optional feature state. +- Prefix outputs consistently by resource or feature. +- Avoid exposing raw nested module internals unless they are part of the intended public contract. + +## Example Requirements + +- `examples/complete` is the reference implementation and should enable meaningful optional features. +- Do not commit `examples/complete/providers.tf`; the build system generates `provider.tf`. +- Include required runtime source assets for examples, such as Lambda source directories. +- Keep example README usage, variables, and outputs synchronized with actual example files. + +## Common Anti-Patterns + +- Replacing composition with one giant raw resource implementation. +- Using `git::` module sources. +- Duplicating provider blocks in examples. +- Composing resource naming in both root and example. +- Creating duplicate IAM permissions or duplicate CloudWatch log groups when community modules can already manage them. +- Using inconsistent output prefixes. diff --git a/.agents/references/reference-architecture/testing.md b/.agents/references/reference-architecture/testing.md new file mode 100644 index 0000000..20a7543 --- /dev/null +++ b/.agents/references/reference-architecture/testing.md @@ -0,0 +1,43 @@ +# Reference Architecture Testing + +Reference architecture tests must verify the composed infrastructure, not just Terraform outputs. + +## Test Scope + +- Validate each major composed resource through the provider API where practical. +- Verify optional features that the complete example enables. +- Check security controls such as encryption, private networking, IAM policies, logging, and monitoring. +- Verify resource naming outputs against expected naming formats. + +## Destructive and Readonly Tests + +- Destructive or functional tests may create and exercise resources. +- Readonly tests must not mutate state. +- Do not copy the destructive test body into the readonly test unchanged. + +## Readonly Test Runner + +The readonly package (`tests/post_deploy_functional_readonly`) must: + +- Call `lib.RunNonDestructiveTest`, not `lib.RunSetupTestTeardown`. The setup/teardown runner turns a readonly suite into a full apply, test, and destroy flow. +- Pass a `tests/testimpl` function whose name begins with `TestComposable`, such as `TestComposableCompleteReadOnly`. `lcaf-component-terratest` fails the test at runtime when the name does not meet this requirement. + +CI excludes the readonly binary from its test command, so both requirements must be correct by construction. + +## Assertions + +- Prefer `assert.Equal`, `assert.Contains`, and provider-specific state checks with known expected values. +- Avoid `assert.NotEmpty` where the expected value can be derived from `test.tfvars`, module outputs, or provider API response fields. +- Use `require` for required API response fields before asserting nested values. + +## Provider-Specific Checks + +- AWS Lambda architectures should verify Lambda configuration, runtime, handler, environment variables, CloudWatch log behavior, IAM role/policies, KMS encryption, and source package behavior where applicable. +- Azure PostgreSQL-style architectures should verify server configuration, networking, private DNS, firewall/public access settings, identity, and encryption where applicable. +- For other providers and services, derive equivalent checks from the provider SDK and service API. + +## Go Quality + +- Keep provider SDK clients and test helpers in reusable test implementation files. +- Run `go mod tidy` after adding or changing SDK dependencies. +- Build tests before cloud-backed runs when credentials are not available. diff --git a/.agents/references/shared/terraform-module-standards.md b/.agents/references/shared/terraform-module-standards.md new file mode 100644 index 0000000..41d8575 --- /dev/null +++ b/.agents/references/shared/terraform-module-standards.md @@ -0,0 +1,55 @@ +# Shared Terraform Module Standards + +These standards apply to both primitive and reference architecture modules. + +## Providers + +- Supported providers are Azure (`azurerm`), AWS (`aws`), and Google Cloud (`google`). +- Keep provider-specific guidance labeled by provider. +- Do not add provider-specific examples to shared rules unless each provider is represented or the example is explicitly scoped. + +## Naming + +- Repository names use `tf--module_primitive-` for primitives. +- Repository names use `tf--module_reference-` for reference architectures. +- Terraform variables use `snake_case`. +- Avoid Terraform reserved names for variables, including `source`, `target`, `version`, `count`, `for`, and `provider`. +- Use the Launch resource naming module for generated cloud resource names. +- `resource_names_map` is used with `for_each`; read outputs with `module.resource_names[""].`. + +## Terraform Interface + +- Every variable needs an explicit type and useful description. +- Add validation for bounded numeric values, enums, formats, mutually exclusive fields, and cross-field requirements. +- Optional object variables must validate coherent combinations, not just individual field values. +- Outputs should expose useful resource attributes individually and describe what each output represents. +- If an output `id` equals another attribute, say so in the description. +- Provider and module version constraints should be bounded enough to avoid untested major upgrades. + +## Documentation + +- Keep the root `README.md` derived from `TEMPLATED_README.md`. +- Replace module title and overview, add accurate usage, run terraform-docs, and preserve Module Development, Local Validation, Review & Merge Process, and Automatic Updates sections. +- `examples/complete/README.md` must match `examples/complete/main.tf`, variables, and outputs. +- Do not leave empty terraform-docs blocks. + +## Validation + +- Prefer `mise run ` when a suitable task exists. +- Otherwise use `mise exec -- ` for Terraform, Go, pre-commit, and related tools. +- Run formatting and linting before broader test flows. +- For examples, validate initialization, Terraform validation, and plan where credentials and backend constraints allow. +- For Go tests, run `go mod tidy`, build or targeted tests, and broader Terratest only when the required cloud access is available. +- Keep the skeleton's `configure`, `check`, `lint`, `test`, and `docs` Makefile targets. Use those targets when they fit the task instead of recreating their underlying workflow. +- `provider.tf` in an example is generated by the Makefile. Do not edit or commit it unless the current template explicitly makes it a source file. + +## Cleanup + +Before completion, search for: + +- `TODO` +- skeleton repository names +- template resource names +- stale test imports or package names +- copied example snippets that no longer match code +- generated provider files that should not be committed diff --git a/.agents/skills/primitive-module/SKILL.md b/.agents/skills/primitive-module/SKILL.md new file mode 100644 index 0000000..03a21e0 --- /dev/null +++ b/.agents/skills/primitive-module/SKILL.md @@ -0,0 +1,39 @@ +--- +name: primitive-module +description: Create, clean up, validate, test, or document a Launch Terraform primitive module. +--- + +# Primitive Module Workflow + +Use this skill for repositories named `tf--module_primitive-` or tasks that explicitly ask for a primitive module. + +## Read First + +1. Read `AGENTS.md`. +2. Read `.agents/references/shared/terraform-module-standards.md`. +3. Read `.agents/references/primitive/standards.md`. +4. Read `.agents/references/primitive/creation.md` when creating a module from this skeleton or completing template cleanup. +5. Read `.agents/references/primitive/testing.md` before writing or changing tests. +6. Read `.agents/references/primitive/release-history.md` only when changing this guidance or investigating why a rule exists. + +## Workflow + +1. Confirm the target cloud provider and single resource type. +2. Inspect the existing repository structure and current Terraform files. +3. Remove skeleton remnants and rename template resources, tests, imports, package names, and README content. +4. Implement one primitive resource interface with explicit variable types, descriptions, validations, resource outputs, and provider constraints. +5. Build `examples/complete/` as the canonical secure usage example. +6. Keep `README.md` derived from `TEMPLATED_README.md`; replace the module-specific sections and preserve skeleton development boilerplate. +7. Add or update Terratest code so assertions verify specific expected values and security settings through provider APIs when applicable. +8. Run focused validation, then broader checks such as formatting, linting, Terraform init/validate/plan for the example, README generation, and Go test build or Terratest where practical. + +## Completion Gate + +- The module contains one primary resource type and no architecture-level business logic. +- Example variables pass through all root variables. +- Example README matches `examples/complete/main.tf`. +- Root README retains the non-module-specific sections from `TEMPLATED_README.md` and has a populated terraform-docs block. +- Functional and readonly tests are meaningfully different. +- Tests avoid `assert.NotEmpty` or `require.NotEmpty` when a specific expected value is known. +- Security features configured by the module are verified through provider APIs where practical. +- No TODO placeholders, skeleton resources, stale imports, or template names remain. diff --git a/.agents/skills/reference-architecture/SKILL.md b/.agents/skills/reference-architecture/SKILL.md new file mode 100644 index 0000000..1875114 --- /dev/null +++ b/.agents/skills/reference-architecture/SKILL.md @@ -0,0 +1,37 @@ +--- +name: reference-architecture +description: Create, clean up, validate, test, or document a Launch Terraform reference architecture module. +--- + +# Reference Architecture Workflow + +Use this skill for repositories named `tf--module_reference-` or tasks that explicitly ask for a reference architecture. + +## Read First + +1. Read `AGENTS.md`. +2. Read `.agents/references/shared/terraform-module-standards.md`. +3. Read `.agents/references/reference-architecture/standards.md`. +4. Read `.agents/references/reference-architecture/testing.md` before writing or changing tests. +5. Read `.agents/references/reference-architecture/release-history.md` only when changing this guidance or investigating why a rule exists. + +## Workflow + +1. Confirm the target cloud provider, architecture purpose, and expected optional features. +2. Inspect existing module files and examples before changing code. +3. Remove skeleton remnants and rename template resources, tests, imports, package names, and README content. +4. Compose internal primitive modules and selected mature community modules using registry source addresses and bounded version constraints. +5. Use the resource naming module for all generated names. Do not ask consumers to provide names that the architecture should own. +6. Model optional features with explicit `create_*` flags and coherent object validation. +7. Build `examples/complete/` as the canonical secure architecture example, including required source assets such as Lambda source directories where applicable. +8. Add or update Terratest coverage that validates the composed resources through provider APIs, differentiates destructive and readonly flows, and checks optional features enabled by the example. +9. Run focused validation, then broader checks such as formatting, linting, Terraform init/validate/plan for the example, README generation, and Go test build or Terratest where practical. + +## Completion Gate + +- The module composes a complete pattern rather than wrapping a single resource. +- Internal modules use Launch registry addresses; community modules use public registry addresses, not `git::` URLs. +- Example code does not duplicate provider configuration generated by the build system. +- Outputs have consistent prefixes and expose useful aggregate state. +- Readonly tests use non-destructive test helpers. +- No skeleton resources, stale imports, TODO placeholders, or template names remain. diff --git a/.claude/rules/agent-routing.md b/.claude/rules/agent-routing.md new file mode 100644 index 0000000..1cfb8d2 --- /dev/null +++ b/.claude/rules/agent-routing.md @@ -0,0 +1,9 @@ +# Agent Routing + +Use `AGENTS.md` as the shared baseline. For task workflows, prefer the files under `.agents/skills/` rather than expanding always-loaded Claude instructions. + +- Primitive module work: `.agents/skills/primitive-module/SKILL.md` +- Reference architecture work: `.agents/skills/reference-architecture/SKILL.md` +- Shared standards: `.agents/references/shared/terraform-module-standards.md` + +Do not read every reference file up front. Load the specific reference files routed by the selected workflow. diff --git a/.claude/rules/terraform-files.md b/.claude/rules/terraform-files.md new file mode 100644 index 0000000..09a1980 --- /dev/null +++ b/.claude/rules/terraform-files.md @@ -0,0 +1,15 @@ +--- +paths: + - "**/*.tf" + - "examples/**/*.tf" + - "tests/**/*.go" + - "README.md" + - "TEMPLATED_README.md" +--- + +# Terraform Module Files + +- Keep module type boundaries clear: primitives wrap one resource type; reference architectures compose complete patterns. +- Keep examples synchronized with root variables, outputs, and README usage snippets. +- Prefer provider API constraints, Terraform validation blocks, and specific test assertions over loose or non-empty checks. +- Remove skeleton placeholders and stale template references before completion. diff --git a/.copier-answers.yml b/.copier-answers.yml new file mode 100644 index 0000000..da645a3 --- /dev/null +++ b/.copier-answers.yml @@ -0,0 +1,5 @@ +# NEVER EDIT MANUALLY +# https://copier.readthedocs.io/en/stable/updating/#never-change-the-answers-file-manually + +_commit: 0.7.1 +_src_path: gh:launchbynttdata/launch-terraform-skeleton diff --git a/.cursor/rules/launch-agent-routing.mdc b/.cursor/rules/launch-agent-routing.mdc new file mode 100644 index 0000000..736f8f0 --- /dev/null +++ b/.cursor/rules/launch-agent-routing.mdc @@ -0,0 +1,11 @@ +--- +alwaysApply: true +--- + +# Launch Agent Routing + +- Use `AGENTS.md` as the shared baseline for this repository. +- For primitive module creation or cleanup, follow `.agents/skills/primitive-module/SKILL.md`. +- For reference architecture creation or cleanup, follow `.agents/skills/reference-architecture/SKILL.md`. +- Read files under `.agents/references/` only when a selected workflow routes you there. +- Keep generated module changes small, explicit, and validated. diff --git a/.cursor/rules/primitive-module-workflow.mdc b/.cursor/rules/primitive-module-workflow.mdc new file mode 100644 index 0000000..11b086e --- /dev/null +++ b/.cursor/rules/primitive-module-workflow.mdc @@ -0,0 +1,18 @@ +--- +description: "Terraform primitive module creation, cleanup, validation, testing, and documentation workflow" +alwaysApply: false +--- + +# Primitive Module Workflow + +Use this rule when creating or modifying a `tf--module_primitive-` repository. + +Follow `.agents/skills/primitive-module/SKILL.md`. + +Read these references only as needed: + +- `.agents/references/shared/terraform-module-standards.md` +- `.agents/references/primitive/standards.md` +- `.agents/references/primitive/creation.md` +- `.agents/references/primitive/testing.md` +- `.agents/references/primitive/release-history.md` diff --git a/.cursor/rules/reference-architecture-workflow.mdc b/.cursor/rules/reference-architecture-workflow.mdc new file mode 100644 index 0000000..99b64e6 --- /dev/null +++ b/.cursor/rules/reference-architecture-workflow.mdc @@ -0,0 +1,17 @@ +--- +description: "Terraform reference architecture creation, cleanup, validation, testing, and documentation workflow" +alwaysApply: false +--- + +# Reference Architecture Workflow + +Use this rule when creating or modifying a `tf--module_reference-` repository. + +Follow `.agents/skills/reference-architecture/SKILL.md`. + +Read these references only as needed: + +- `.agents/references/shared/terraform-module-standards.md` +- `.agents/references/reference-architecture/standards.md` +- `.agents/references/reference-architecture/testing.md` +- `.agents/references/reference-architecture/release-history.md` diff --git a/.github/agents/primitive-module-creator.agent.md b/.github/agents/primitive-module-creator.agent.md new file mode 100644 index 0000000..831d065 --- /dev/null +++ b/.github/agents/primitive-module-creator.agent.md @@ -0,0 +1,24 @@ +--- +name: Terraform Primitive Module Creator +description: Compatibility stub for agents looking for the legacy primitive module creator guide. +--- + + + +# Terraform Primitive Module Creator + +The primitive module guide has moved to the agent-agnostic instruction layout. + +Use these files instead: + +- Baseline operating rules: [AGENTS.md](../../AGENTS.md) +- Reusable workflow: [.agents/skills/primitive-module/SKILL.md](../../.agents/skills/primitive-module/SKILL.md) +- Shared Terraform standards: [.agents/references/shared/terraform-module-standards.md](../../.agents/references/shared/terraform-module-standards.md) +- Primitive interface standards: [.agents/references/primitive/standards.md](../../.agents/references/primitive/standards.md) +- Creation, cleanup, and documentation guidance: [.agents/references/primitive/creation.md](../../.agents/references/primitive/creation.md) +- Terratest guidance: [.agents/references/primitive/testing.md](../../.agents/references/primitive/testing.md) +- Condensed release history and preserved learnings: [.agents/references/primitive/release-history.md](../../.agents/references/primitive/release-history.md) +- Cursor project rule: [.cursor/rules/primitive-module-workflow.mdc](../../.cursor/rules/primitive-module-workflow.mdc) +- Claude routing rules: [.claude/rules/agent-routing.md](../../.claude/rules/agent-routing.md) and [.claude/rules/terraform-files.md](../../.claude/rules/terraform-files.md) + +This file remains only for compatibility with tools or prompts that still inspect `.github/agents/`. diff --git a/.github/agents/reference-architecture-creator.agent.md b/.github/agents/reference-architecture-creator.agent.md new file mode 100644 index 0000000..d7259a2 --- /dev/null +++ b/.github/agents/reference-architecture-creator.agent.md @@ -0,0 +1,23 @@ +--- +name: Terraform Reference Architecture Creator +description: Compatibility stub for agents looking for the legacy reference architecture creator guide. +--- + + + +# Terraform Reference Architecture Creator + +The reference architecture guide has moved to the agent-agnostic instruction layout. + +Use these files instead: + +- Baseline operating rules: [AGENTS.md](../../AGENTS.md) +- Codex-style workflow skill: [.agents/skills/reference-architecture/SKILL.md](../../.agents/skills/reference-architecture/SKILL.md) +- Cursor project rule: [.cursor/rules/reference-architecture-workflow.mdc](../../.cursor/rules/reference-architecture-workflow.mdc) +- Claude routing rules: [.claude/rules/agent-routing.md](../../.claude/rules/agent-routing.md) +- Shared Terraform standards: [.agents/references/shared/terraform-module-standards.md](../../.agents/references/shared/terraform-module-standards.md) +- Reference architecture standards: [.agents/references/reference-architecture/standards.md](../../.agents/references/reference-architecture/standards.md) +- Reference architecture testing guidance: [.agents/references/reference-architecture/testing.md](../../.agents/references/reference-architecture/testing.md) +- Condensed release history: [.agents/references/reference-architecture/release-history.md](../../.agents/references/reference-architecture/release-history.md) + +This file remains only for compatibility with tools or prompts that still inspect `.github/agents/`. diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 33f0854..18d7ff1 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,32 +1,34 @@ version: 2 - multi-ecosystem-groups: - module: + tests: + commit-message: + prefix: "chore" + include: scope schedule: - interval: "weekly" + interval: "daily" workflows: + commit-message: + prefix: "chore" + include: scope schedule: - interval: "weekly" - + interval: "daily" updates: - package-ecosystem: "github-actions" directory: "/" multi-ecosystem-group: "workflows" - patterns: ["*"] - + patterns: [ "*" ] + exclude-paths: + - .github/workflows/pull-request-automation.yml + - .github/workflows/pull-request-check-terraform.yml + - .github/workflows/pull-request-dependabot-automerge.yml + - .github/workflows/pull-request-label.yml + - .github/workflows/pull-request-precommit-checks.yml + - .github/workflows/rebuild-cache-on-merge.yml + - .github/workflows/release-publish.yml + - .github/workflows/update-from-skeleton.yml - package-ecosystem: "gomod" directory: "/" - multi-ecosystem-group: "module" - patterns: ["*"] - - - package-ecosystem: "terraform" - directory: "/" - multi-ecosystem-group: "module" - patterns: ["*"] - ignore: - - dependency-name: "hashicorp/*" - update-types: ["version-update:semver-major"] - - dependency-name: "microsoft/*" - update-types: ["version-update:semver-major"] - - dependency-name: "azure/*" - update-types: ["version-update:semver-major"] + multi-ecosystem-group: "tests" + patterns: [ "*" ] + cooldown: + default-days: 14 diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml index 6ab7d45..6fe7e59 100644 --- a/.github/release-drafter.yml +++ b/.github/release-drafter.yml @@ -13,37 +13,53 @@ template: | categories: - title: ":warning: Breaking Changes" labels: - - "major" + - "breaking change" - title: "๐Ÿš€ Features" labels: - - "minor" + - "feature" - title: "๐Ÿ”ง Fixes" collapse-after: 3 labels: - - "patch" - -autolabeler: - - label: "major" - branch: - - '/(patch|bug|fix|feature|feat|chore)!\/.+/' - - label: "minor" - branch: - - '/(feature|feat)\/.+/' - - label: "patch" - branch: - - '/(patch|bug|fix|chore)\/.+/' + - "fix" + - title: "๐Ÿ“ Documentation" + collapse-after: 3 + labels: + - "documentation" + - title: "๐Ÿงช Test" + collapse-after: 3 + labels: + - "test" + - title: "โš™๏ธ CI/CD" + collapse-after: 3 + labels: + - "CI/CD" + - title: "โ™ป๏ธ Refactor" + collapse-after: 3 + labels: + - "refactor" + - title: "โšก Performance" + collapse-after: 3 + labels: + - "performance" + - title: "๐Ÿงน Chore" + collapse-after: 3 + labels: + - "chore" + - title: "โช Revert" + collapse-after: 3 + labels: + - "revert" change-template: "- $TITLE @$AUTHOR (#$NUMBER)" version-resolver: major: labels: - - "major" + - "breaking change" minor: labels: - - "minor" - patch: - labels: - - "patch" - - "dependencies" + - "feature" default: patch + +exclude-labels: + - "skip-changelog" diff --git a/.github/scripts/validate-readonly-test.sh b/.github/scripts/validate-readonly-test.sh new file mode 100755 index 0000000..d6039a1 --- /dev/null +++ b/.github/scripts/validate-readonly-test.sh @@ -0,0 +1,87 @@ +#!/usr/bin/env bash +# +# Validates the post-deploy readonly test wiring. +# +# CI never executes the readonly test binary -- `make test` excludes it +# (go list ./tests/... | grep -v post_deploy_functional_readonly) because +# post-deploy tests need live infrastructure. That leaves two defects +# invisible to CI until they hit a real test run: +# +# 1. Wrong runner: the readonly suite calls lib.RunSetupTestTeardown +# (apply -> test -> destroy) instead of lib.RunNonDestructiveTest, so +# the "read-only" suite is not actually read-only. +# 2. Wrong name: the testimpl function passed to RunNonDestructiveTest does +# not start with TestComposable. lcaf-component-terratest's +# demandAllTests2RunAreComposableOnes calls t.FailNow() in that case, so +# the readonly suite hard-fails the moment it runs. +# +# This hook catches both statically, at commit time. To stay robust to source +# formatting it strips // line comments and flattens newlines before matching, +# so multiline calls and identifiers mentioned in comments are handled. See +# launchbynttdata/launch-workflows#92. + +set -euo pipefail + +# Fixed path -- matches the hook's files: filter in .pre-commit-config.yaml and +# the Makefile's GO_TEST_READONLY_DIRECTORY default. (The directory is not +# configurable here on purpose: the pre-commit files: filter can't read env +# vars, so honoring an override in the script alone would be misleading.) +readonly_dir="tests/post_deploy_functional_readonly" + +status=0 + +# Nothing to validate if the module has no readonly suite. +[ -d "$readonly_dir" ] || exit 0 + +go_files="$(find "$readonly_dir" -name '*.go' -type f 2>/dev/null || true)" +[ -n "$go_files" ] || exit 0 + +# Comment-stripped, newline-flattened view of the readonly Go sources, so the +# checks below are robust to multiline calls and to identifiers appearing in +# comments. (Strips // line comments; block comments are not handled.) +flat="$(cat $go_files | sed -e 's://.*$::' | tr '\n' ' ')" + +# 1. Wrong runner: the destructive runner must not be *called* in the readonly suite. +if printf '%s' "$flat" | grep -Eq 'RunSetupTestTeardown[[:space:]]*\('; then + echo "ERROR: $readonly_dir calls lib.RunSetupTestTeardown." >&2 + echo " The readonly suite must use lib.RunNonDestructiveTest -- it must not apply/destroy." >&2 + status=1 +fi + +# The readonly suite must call the non-destructive runner. +if ! printf '%s' "$flat" | grep -Eq 'RunNonDestructiveTest[[:space:]]*\('; then + echo "ERROR: $readonly_dir does not call lib.RunNonDestructiveTest." >&2 + echo " The readonly suite must drive its assertions through that runner." >&2 + status=1 +fi + +# 2. Wrong name: every function passed to RunNonDestructiveTest must start with +# TestComposable (the lcaf runtime requirement that CI cannot see). The last +# argument is the testimpl function; tolerate a gofmt trailing comma and an +# optional package qualifier. +names="$(printf '%s' "$flat" \ + | grep -oE 'RunNonDestructiveTest[[:space:]]*\([^)]*\)' \ + | sed -E -e 's/^[^(]*\(//' -e 's/\)$//' -e 's/[[:space:]]//g' -e 's/,+$//' \ + | awk -F',' '{print $NF}' \ + | awk -F'.' '{print $NF}' || true)" + +while IFS= read -r fn; do + [ -n "$fn" ] || continue + case "$fn" in + TestComposable*) : ;; + *) + echo "ERROR: function '$fn' is passed to RunNonDestructiveTest in $readonly_dir but does not start with 'TestComposable'." >&2 + echo " lcaf-component-terratest requires a TestComposable* function; CI does not catch this." >&2 + status=1 + ;; + esac +done <&2 + echo "Readonly test validation failed. Background: launchbynttdata/launch-workflows#92" >&2 +fi + +exit "$status" diff --git a/.github/workflows/pull-request-automation.yml b/.github/workflows/pull-request-automation.yml new file mode 100644 index 0000000..f457e2d --- /dev/null +++ b/.github/workflows/pull-request-automation.yml @@ -0,0 +1,14 @@ +name: Automated Approvals + +on: + pull_request: + types: [ opened, reopened, synchronize ] + +jobs: + automated-approvals: + name: Automated Approvals + permissions: + pull-requests: read + contents: read + uses: launchbynttdata/launch-workflows/.github/workflows/reusable-pr-automated-approvals.yml@edddecdce4590f94f30d0fdfdf991d8a692fe8dd + secrets: inherit # pragma: allowlist secret diff --git a/.github/workflows/pull-request-check-terraform.yml b/.github/workflows/pull-request-check-terraform.yml new file mode 100644 index 0000000..1b6d29f --- /dev/null +++ b/.github/workflows/pull-request-check-terraform.yml @@ -0,0 +1,20 @@ +name: "Check Terraform" + +on: + pull_request: + types: [ opened, reopened, synchronize, ready_for_review ] + branches: [ main ] + +jobs: + check: + name: "Reusable Workflow" + permissions: + contents: read + pull-requests: read + id-token: write + statuses: write + uses: launchbynttdata/launch-workflows/.github/workflows/reusable-terraform-check.yml@edddecdce4590f94f30d0fdfdf991d8a692fe8dd + with: + auth_method: ${{ startsWith(github.event.repository.name, 'tf-aws') && 'aws' || + startsWith(github.event.repository.name, 'tf-az') && 'azure' || '' }} + secrets: inherit # pragma: allowlist secret diff --git a/.github/workflows/pull-request-dependabot-automerge.yml b/.github/workflows/pull-request-dependabot-automerge.yml new file mode 100644 index 0000000..4bf4643 --- /dev/null +++ b/.github/workflows/pull-request-dependabot-automerge.yml @@ -0,0 +1,14 @@ +name: Dependabot Auto-Merge + +on: + pull_request: + types: [ opened, reopened, synchronize ] + +jobs: + dependabot-automerge: + name: Dependabot Auto-Merge + permissions: + contents: write + pull-requests: write + uses: launchbynttdata/launch-workflows/.github/workflows/reusable-pr-dependabot-automerge.yml@edddecdce4590f94f30d0fdfdf991d8a692fe8dd + secrets: inherit # pragma: allowlist secret diff --git a/.github/workflows/pull-request-label.yml b/.github/workflows/pull-request-label.yml index 6a2936b..038b727 100644 --- a/.github/workflows/pull-request-label.yml +++ b/.github/workflows/pull-request-label.yml @@ -1,15 +1,14 @@ -name: Label Pull Request +name: Validate PR Title on: pull_request: - types: [opened, reopened, synchronize] + types: [ opened, reopened, edited, ready_for_review, synchronize ] jobs: - check: - name: "Label Pull Request" + validate-title: + name: Validate PR Title permissions: contents: read - issues: write pull-requests: write - uses: launchbynttdata/launch-workflows/.github/workflows/reusable-pr-label-by-branch.yml@0.14.2 - secrets: inherit # pragma: allowlist secret + statuses: write + uses: launchbynttdata/launch-workflows/.github/workflows/reusable-pr-conventional-commit-title.yml@edddecdce4590f94f30d0fdfdf991d8a692fe8dd diff --git a/.github/workflows/pull-request-precommit-checks.yml b/.github/workflows/pull-request-precommit-checks.yml new file mode 100644 index 0000000..1d6924f --- /dev/null +++ b/.github/workflows/pull-request-precommit-checks.yml @@ -0,0 +1,78 @@ +name: Pre-Commit Checks + +on: + pull_request: + types: [ opened, reopened, synchronize, ready_for_review ] + branches: [ main ] + +jobs: + pre-commit: + name: Pre-Commit Checks + permissions: + pull-requests: read + contents: read + statuses: write + runs-on: ubuntu-latest + steps: + - id: checkout + name: Checkout code + uses: actions/checkout@v6 + + - id: setup-python + name: Set up Python 3.14 + uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 + with: + python-version: 3.14 + + - id: install-tools + name: Install Tools with uv + run: | + uv tool install pre-commit --with pre-commit-uv + + # If we've cached the pre-commit environment, restore it based on the hash of the pre-commit config. + - id: cache + name: Restore cached pre-commit environment + uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae + with: + path: ~/.cache/pre-commit + key: ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} + + - id: setup-asdf + name: Setup ASDF + uses: asdf-vm/actions/setup@b7bcd026f18772e44fe1026d729e1611cc435d47 + + - id: restore-asdf-tools + name: Restore ASDF tools from cache + uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae + with: + key: ${{ runner.os }}-asdf-${{ hashFiles('.tool-versions') }} + restore-keys: | + ${{ runner.os }}-asdf- + path: | + ~/.asdf/plugins + ~/.asdf/installs + + - id: install-asdf + name: Install ASDF tools on cache-miss + if: ${{ steps.restore-asdf-tools.outputs.cache-hit != 'true' }} + uses: asdf-vm/actions/install@b7bcd026f18772e44fe1026d729e1611cc435d47 + + - id: asdf-reshim + name: Reshim installed ASDF tools + run: asdf reshim + + - id: pre-commit + name: Run pre-commit checks + run: uv run pre-commit run --all-files --show-diff-on-failure + + - id: set-status-check + name: Set Status Check + if: always() + uses: launchbynttdata/launch-workflows/.github/actions/update-status-check@edddecdce4590f94f30d0fdfdf991d8a692fe8dd + with: + check_name: "Pre-Commit Checks" + status: ${{ steps.pre-commit.outcome == 'success' && 'success' || + steps.pre-commit.outcome == 'failure' && 'failure' || 'error' }} + description: "Pre-Commit Checks ${{steps.pre-commit.outcome }}" + target_url: "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ + github.run_id }}" diff --git a/.github/workflows/pull-request-terraform-check-aws.yml b/.github/workflows/pull-request-terraform-check-aws.yml deleted file mode 100644 index 109766b..0000000 --- a/.github/workflows/pull-request-terraform-check-aws.yml +++ /dev/null @@ -1,22 +0,0 @@ -name: Check AWS Terraform Code - -on: - pull_request: - types: [opened, reopened, synchronize, ready_for_review] - branches: [main] - -permissions: - id-token: write - contents: read - -jobs: - check: - name: "Check AWS Terraform Code" - permissions: - contents: read - id-token: write - uses: launchbynttdata/launch-workflows/.github/workflows/reusable-terraform-check-aws.yml@0.14.2 - with: - assume_role_arn: ${{ vars.TERRAFORM_CHECK_AWS_ASSUME_ROLE_ARN }} - region: ${{ vars.TERRAFORM_CHECK_AWS_REGION }} - secrets: inherit # pragma: allowlist secret diff --git a/.github/workflows/rebuild-cache-on-merge.yml b/.github/workflows/rebuild-cache-on-merge.yml new file mode 100644 index 0000000..bffa84f --- /dev/null +++ b/.github/workflows/rebuild-cache-on-merge.yml @@ -0,0 +1,56 @@ +name: Rebuild Caches on Merge + +on: + push: + branches: + - main + paths: + - ".pre-commit-config.yaml" + - ".tool-versions" + workflow_dispatch: + +jobs: + pre-commit: + name: Rebuild Caches + permissions: + contents: read + runs-on: ubuntu-latest + steps: + - id: checkout + name: Checkout + uses: actions/checkout@v6 + + - id: setup-python + name: Set up Python 3.14 + uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 + with: + python-version: 3.14 + + - id: install-tools + name: Install Tools with uv + run: | + uv tool install pre-commit --with pre-commit-uv + + - id: asdf-install + name: Setup tools using asdf + uses: asdf-vm/actions/install@b7bcd026f18772e44fe1026d729e1611cc435d47 + + - id: pre-commit + name: Run pre-commit checks + run: uv run pre-commit run --all-files + + - id: save-pre-commit-cache + name: Save pre-commit cache + uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae + with: + path: ~/.cache/pre-commit + key: ${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} + + - id: save-asdf-cache + name: Save asdf cache + uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae + with: + path: | + ~/.asdf/installs + ~/.asdf/plugins + key: ${{ runner.os }}-asdf-${{ hashFiles('.tool-versions') }} diff --git a/.github/workflows/release-publish.yml b/.github/workflows/release-publish.yml index 8f7d50d..3cef49e 100644 --- a/.github/workflows/release-publish.yml +++ b/.github/workflows/release-publish.yml @@ -14,5 +14,5 @@ jobs: permissions: contents: write pull-requests: write - uses: launchbynttdata/launch-workflows/.github/workflows/reusable-release-on-merge.yml@0.14.2 + uses: launchbynttdata/launch-workflows/.github/workflows/reusable-release-on-merge.yml@edddecdce4590f94f30d0fdfdf991d8a692fe8dd secrets: inherit # pragma: allowlist secret diff --git a/.github/workflows/update-from-skeleton.yml b/.github/workflows/update-from-skeleton.yml new file mode 100644 index 0000000..b6ce8be --- /dev/null +++ b/.github/workflows/update-from-skeleton.yml @@ -0,0 +1,24 @@ +name: Update Repository from Skeleton + +on: + schedule: + - cron: "0 0 * * 0" # Every Sunday at midnight + workflow_dispatch: + inputs: + recopy: + description: "Perform a full recopy instead of an incremental update (overwrites + all templated files), and re-runs any tasks defined in copier.yml. The + resulting PR will require manual review and merging." + type: boolean + default: false + +jobs: + update-from-skeleton: + name: Update from Skeleton + permissions: + contents: write + pull-requests: write + uses: launchbynttdata/launch-workflows/.github/workflows/reusable-update-from-skeleton.yml@edddecdce4590f94f30d0fdfdf991d8a692fe8dd + with: + recopy: ${{ github.event.inputs.recopy == 'true' }} + secrets: inherit # pragma: allowlist secret diff --git a/.gitignore b/.gitignore index 4be3fa8..9e17ca7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,10 @@ +.DS_Store +Thumbs.db + terraform.* .repo/ components/ .semverbot.toml -.tflint.hcl -.golangci.yaml .idea !examples/*.tfvars @@ -15,7 +16,7 @@ components/ **/.test-data/* # Local .terraform directories -**/.terraform +**/.terraform/* # Local .terragrunt directories **/.terragrunt/* @@ -56,16 +57,6 @@ terraform.rc # Files from common modules azure_env.sh .releaserc.json -.tflint.hcl - -# Pre-commit hook -.pre-commit-config.yaml # VS Code .vscode/ - -# Lambda build files -**/build -**/builds -**/*.egg-info -**/.DS_Store diff --git a/.golangci.yaml b/.golangci.yaml new file mode 100644 index 0000000..4da23dc --- /dev/null +++ b/.golangci.yaml @@ -0,0 +1,5 @@ +version: "2" +run: + # Timeout for analysis, e.g. 30s, 5m. + timeout: 10m + allow-parallel-runners: true diff --git a/.lcafenv b/.lcafenv deleted file mode 100644 index 363338c..0000000 --- a/.lcafenv +++ /dev/null @@ -1,23 +0,0 @@ -# Use this file to preset variables used by the Makefile. -# This file will be included when make is run. The variables below will -# take precedence over what is defined in Makefile when they are set. This -# allows overriding certain settings without modifying the Makefile. - -# REPO_MANIFESTS_URL="https://github.com/launchbynttdata/launch-common-automation-framework.git" -# REPO_BRANCH="refs/tags/1.0.0" -# REPO_MANIFEST="manifests/terraform_modules/seed/manifest.xml" -# REPO_URL="https://github.com/launchbynttdata/git-repo.git" -# REPO_REV="main" -# GITBASE="https://github.com/launchbynttdata/" -# GITREV="main" -# IS_PIPELINE="false" -# IS_AUTHENTICATED="false" -# JOB_NAME="job" -# JOB_EMAIL="job@job.job" -# PLATFORM_VER= -# CONTAINER_VER= -# PIPELINES_VER= -# WEBHOOK_VER= -# PYTHON_VER= -# TERRAGRUNT_VER= -# TERRAFORM_VER= diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..d48c0d6 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,44 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.6.0 + hooks: + - id: trailing-whitespace + - id: check-case-conflict + - id: check-executables-have-shebangs + - id: check-json + - id: check-merge-conflict + args: + - --assume-in-merge + - id: check-shebang-scripts-are-executable + - id: check-yaml + args: + - --allow-multiple-documents + - id: end-of-file-fixer + - id: mixed-line-ending + args: + - --fix=auto + - repo: https://github.com/antonbabenko/pre-commit-terraform + rev: v1.99.0 + hooks: + - id: terraform_fmt + - id: terraform_validate + - id: terraform_docs + args: + - --hook-config=--path-to-file=README.md + - --hook-config=--add-to-existing-file=true + - --hook-config=--create-file-if-not-exist=true + - --args=--hide providers + - repo: https://github.com/Yelp/detect-secrets + rev: v1.5.0 + hooks: + - id: detect-secrets + args: ["--baseline", ".secrets.baseline"] + exclude: package.lock.json + - repo: local + hooks: + - id: validate-readonly-test + name: Validate readonly test wiring (RunNonDestructiveTest + TestComposable*) + entry: .github/scripts/validate-readonly-test.sh + language: script + files: ^tests/post_deploy_functional_readonly/.*\.go$ + pass_filenames: false diff --git a/.tflint.hcl b/.tflint.hcl new file mode 100644 index 0000000..35ef36f --- /dev/null +++ b/.tflint.hcl @@ -0,0 +1,13 @@ +config { + plugin_dir = "~/.tflint.d/plugins" + + call_module_type = "all" + force = false + disabled_by_default = false + + ignore_module = { + } + + varfile = [] + variables = [] +} diff --git a/.tool-versions b/.tool-versions index 867b51e..aa78ebd 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,9 +1,8 @@ conftest 0.56.0 -golang 1.24.2 -golangci-lint 2.10.1 -pre-commit 4.2.0 -regula 3.2.1 # https://github.com/launchbynttdata/asdf-regula -terraform 1.12.2 +golang 1.26.1 +golangci-lint 2.11.3 +pre-commit 4.6.0 +terraform 1.10.4 terraform-docs 0.20.0 terragrunt 0.77.22 tflint 0.57.0 diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..d79b9d8 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,36 @@ +# Launch Terraform Module Agent Guide + +This repository follows the Launch Terraform module standards. Keep this file small: it is the shared baseline for coding agents and should not contain workflow playbooks or long reference material. + +## Working Agreement + +- Read the current files before making changes. Do not assume the generated template still matches this repository. +- Make the smallest change that satisfies the requested outcome. +- Keep primitive modules and reference architecture modules distinct. +- Prefer clear Terraform, explicit variable types, useful descriptions, and validation for constrained inputs. +- Treat examples and tests as part of the public contract. Update them with implementation changes. +- Do not preserve skeleton placeholders, TODOs, or copied template names in completed modules. +- Do not introduce provider-specific guidance into shared rules unless it is clearly labeled by provider. +- Use `mise` for local tool execution when available. Prefer `mise run ` for configured tasks and `mise exec -- ` otherwise. +- Use SSH-based Git remotes or `gh` for GitHub repository operations. If SSH or `gh` is not working, stop and resolve that rather than silently switching to HTTPS Git remotes. +- GitHub API access through `gh api` or `gh api graphql` is acceptable when repository metadata is needed. + +## Module Types + +- Primitive modules wrap one cloud resource type and expose a reusable, low-opinion interface. +- Reference architecture modules compose primitives and selected mature community modules into opinionated infrastructure patterns. +- If the requested work does not clearly identify the module type, inspect the repository name and module structure before proceeding. + +## Task Routing + +- For primitive module creation or cleanup, use `.agents/skills/primitive-module/SKILL.md`. +- For reference architecture creation or cleanup, use `.agents/skills/reference-architecture/SKILL.md`. +- For shared Terraform standards, read `.agents/references/shared/terraform-module-standards.md`. +- For long examples, historical rationale, and provider-specific notes, read only the reference files routed by the selected skill. +- Do not load every file in `.agents/references/` by default. + +## Validation Expectations + +- Run the narrowest useful validation first, then broaden when the change affects shared behavior. +- Before considering module creation complete, validate formatting, linting, Terraform initialization/validation for examples, README generation, and Terratest readiness where practical. +- If full cloud-backed tests cannot be run, state what was validated and what remains unproven. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..2f3720b --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,9 @@ +@AGENTS.md + +# Claude Code Notes + +Use `AGENTS.md` as the shared operating contract. Claude-specific routing lives in `.claude/rules/`. + +When a task matches primitive module creation, reference architecture creation, or module cleanup, follow the corresponding workflow under `.agents/skills/` and read only the referenced material needed for that task. + +Keep `CLAUDE.md` concise. Put task-specific guidance in `.claude/rules/` or `.agents/skills/` rather than expanding this file. diff --git a/CODEOWNERS b/CODEOWNERS index d5d106b..3397f05 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1 +1 @@ -* @launchbynttdata/terraform-administrators +* @launchbynttdata/terraform-administrators diff --git a/Makefile b/Makefile index 8f87fc3..8ab409c 100644 --- a/Makefile +++ b/Makefile @@ -10,82 +10,213 @@ # See the License for the specific language governing permissions and # limitations under the License. -# Include custom values from .lcafenv. Repository root is assumed to be the working directory. -# Including overriding values in this file is preferred over modifying the contents below. -LCAF_ENV_FILE = .lcafenv --include $(LCAF_ENV_FILE) - -# Source repository for repo manifests -REPO_MANIFESTS_URL ?= https://github.com/launchbynttdata/launch-common-automation-framework.git -# Branch of source repository for repo manifests. Other tags not currently supported. -REPO_BRANCH ?= refs/tags/1.8.1 -# Path to seed manifest in repository referenced in REPO_MANIFESTS_URL -REPO_MANIFEST ?= manifests/terraform_modules/seed/manifest.xml - -# Settings to pull in Nexient version of (google) repo utility that supports environment substitution: -REPO_URL ?= https://github.com/launchbynttdata/git-repo.git -# Branch of the repository referenced by REPO_URL to use -REPO_REV ?= main -export REPO_REV REPO_URL - -# Example variable to substituted after init, but before sync in repo manifests. -GITBASE ?= https://github.com/launchbynttdata/ -GITREV ?= main -export GITBASE GITREV - -# Set to true in a pipeline context -IS_PIPELINE ?= false - -IS_AUTHENTICATED ?= false +SHELL := /bin/bash JOB_NAME ?= job JOB_EMAIL ?= job@job.job -COMPONENTS_DIR = components --include $(COMPONENTS_DIR)/Makefile - -MODULE_DIR ?= ${COMPONENTS_DIR}/module - PYTHON3_INSTALLED = $(shell which python3 > /dev/null 2>&1; echo $$?) MISE_INSTALLED = $(shell which mise > /dev/null 2>&1; echo $$?) ASDF_INSTALLED = $(shell which asdf > /dev/null 2>&1; echo $$?) -REPO_INSTALLED = $(shell which repo > /dev/null 2>&1; echo $$?) GIT_USER_SET = $(shell git config --get user.name > /dev/null 2>&1; echo $$?) GIT_EMAIL_SET = $(shell git config --get user.email > /dev/null 2>&1; echo $$?) -.PHONY: configure-git-hooks -configure-git-hooks: configure-dependencies -ifeq ($(PYTHON3_INSTALLED), 0) - pre-commit install -else - $(error Missing python3, which is required for pre-commit. Install python3 and rerun.) -endif +# ------------------------------------------------------------------------------ +# Binaries +# ------------------------------------------------------------------------------ + +GO ?= go +GOLANGCI_LINT ?= golangci-lint +TEE ?= tee +GREP ?= grep +FIND ?= find +TERRAFORM ?= terraform +RM ?= rm -rf +TFLINT ?= tflint + +# ------------------------------------------------------------------------------ +# Variables โ€” Golang +# ------------------------------------------------------------------------------ + +GO_TEST_DIRECTORIES ?= tests +GO_TEST_TIMEOUT ?= 2h +GO_TEST_READONLY_DIRECTORY ?= post_deploy_functional_readonly +TEST_RUN_ONLY_READONLY = #intentionally empty +TEST_RUN_EXCLUDE_READONLY = -v +GOLANGCI_LINT_CONFIG ?= .golangci.yaml +DISABLE_MAKE_CHECK_LINT ?= false +CURRENT_DIR = $(notdir $(shell pwd)) -ifeq ($(IS_PIPELINE),true) -.PHONY: git-config -git-config: - @set -ex; \ - git config --global user.name "$(JOB_NAME)"; \ - git config --global user.email "$(JOB_EMAIL)"; \ - git config --global color.ui false +# If ARM_SUBSCRIPTION_ID is not already set by the environment, we'll try to use the `az` command +# to retrieve it. If `az` isn't found or not logged in, we'll silently continue here and catch +# that case elsewhere. +export ARM_SUBSCRIPTION_ID ?= $(shell command -v az >/dev/null 2>&1 && az account show 2>/dev/null | jq -r .id) -configure: git-config +# ------------------------------------------------------------------------------ +# Variables โ€” Terraform module +# ------------------------------------------------------------------------------ + +ALL_TF_MODULES = $(shell $(call list_terraform_modules)) +ALL_EXAMPLES = $(shell $(call list_examples)) +TFLINT_CONFIG ?= .tflint.hcl +VAR_FILE ?= test.tfvars +AWS_PROFILE ?= +AWS_REGION ?= us-east-2 +GITHUB_OWNER ?= $(or $(GITHUB_REPOSITORY_OWNER),launchbynttdata) + +ifneq ($(strip $(AWS_PROFILE)),) +export AWS_PROFILE endif -ifeq ($(IS_AUTHENTICATED),true) -.PHONY: git-auth -git-auth: - $(call config,Bearer $(GIT_TOKEN)) +ifneq ($(strip $(AWS_PROFILE)),) +ifneq ($(shell printf '%s' '$(AWS_PROFILE)' | grep -Eq '^[A-Za-z0-9_.-]+$$'; echo $$?),0) +$(error AWS_PROFILE contains unsupported characters) +endif +endif + +# ------------------------------------------------------------------------------ +# Functions โ€” Golang +# ------------------------------------------------------------------------------ + +# Checks for Go files in the GO_TEST_DIRECTORIES. If they exist, runs the default configuration for golangci-lint +# https://golangci-lint.run/usage/quick-start/ +define go_lint + $(FIND) $(1)/ -name '*.go' | $(GREP) -q '\.go' || exit 0; $(GOLANGCI_LINT) run -c $(GOLANGCI_LINT_CONFIG) -v ./$(1)/...; -define config - @set -ex; \ - git config --global http.extraheader "AUTHORIZATION: $(1)"; \ - git config --global http.https://gerrit.googlesource.com/git-repo/.extraheader ''; \ - git config --global http.version HTTP/1.1; endef -configure: git-auth -endif +# Check for Go files. If they exist, run tests. Either runs only readonly tests(default) or tests except readonly ones +define go_test + $(FIND) $(1)/ -name '*.go' |$(GREP) $(2) $(GO_TEST_READONLY_DIRECTORY) | $(GREP) -q '\.go' || exit 0; $(GO) test -v -count=1 -timeout=$(GO_TEST_TIMEOUT) $$($(GO) list ./$(1)/...|$(GREP) $(2) $(GO_TEST_READONLY_DIRECTORY)) ; + +endef + +# ------------------------------------------------------------------------------ +# Functions โ€” Terraform module +# ------------------------------------------------------------------------------ + +define check_terraform_fmt + echo && echo "Formatting Terraform files ..."; + $(TERRAFORM) fmt -recursive; + +endef + +define clean_terraform_module + $(RM) -fr $(1)/.terraform* $(1)/terraform.*; + $(RM) -f $(1)/provider.tf +endef + +define init_terraform_module + echo && echo "Initializing $(1) ..."; + $(TERRAFORM) -chdir=$(1) init -backend=false -input=false; + +endef + +define list_terraform_modules + $(FIND) . -path "*/.terraform" -prune -o -name "main.tf" -not -path '*pipeline*' -not -path '*examples*' -exec dirname {} \;; + +endef + +define list_examples + $(FIND) ./examples -path "*/.terraform" -prune -o -name "main.tf" -not -path '*pipeline*' -exec dirname {} \; 2>/dev/null +endef + +# ------------------------------------------------------------------------------ +# Provider templates +# ------------------------------------------------------------------------------ +# When running `make lint` or `make test`, provider configuration files are +# auto-generated for each example directory based on the providers declared in +# the root module's `terraform providers` output. The define blocks below +# contain the default content written to each example's provider.tf for the +# matching registry provider. Examples that declare additional providers not +# present in the root module will not receive blocks for those providers. +# +# To override auto-generation entirely, set the PROVIDER_TEMPLATE environment +# variable to the path of a custom provider.tf file: +# +# $ PROVIDER_TEMPLATE=my-provider.tf make test +# +# When set, that file is copied verbatim into every example directory instead of +# running the per-provider detection logic below. +# ------------------------------------------------------------------------------ + +PROVIDER_TEMPLATE ?= + +define aws_provider_base +provider \"aws\" {\n region = \"$(AWS_REGION)\"\n$(if $(strip $(AWS_PROFILE)), profile = \"$(AWS_PROFILE)\"\n)}\n +endef + +define aws_provider_global +\nprovider \"aws\" {\n alias = \"global\"\n region = \"us-east-1\"\n$(if $(strip $(AWS_PROFILE)), profile = \"$(AWS_PROFILE)\"\n)}\n +endef + +define aws_provider +$(call aws_provider_base)$(if $(INCLUDE_AWS_GLOBAL),$(call aws_provider_global)) +endef + +define azurerm_provider +provider \"azurerm\" {\n skip_provider_registration = true\n features {\n resource_group {\n prevent_deletion_if_contains_resources = false\n }\n }\n}\n +endef + +define azapi_provider +provider \"azapi\" {\n use_cli = true\n use_msi = false\n}\n +endef + +define azuredevops_provider +provider \"azuredevops\" {}\n +endef + +define github_provider +provider \"github\" {\n owner = \"$(GITHUB_OWNER)\"\n}\n +endef + +define provider_file_path +$(1)/provider.tf +endef + +define add_provider_details + $(if $(findstring hashicorp/aws,$(2)),grep -qs "aws" $(1) || bash -c 'echo -e "$(call aws_provider)"' >> $(1),) + $(if $(findstring azure/azapi,$(2)),grep -qs "azapi" $(1) || bash -c 'echo -e "$(call azapi_provider)"' >> $(1),) + $(if $(findstring microsoft/azuredevops,$(2)),grep -qs "azuredevops" $(1) || bash -c 'echo -e "$(call azuredevops_provider)"' >> $(1),) + $(if $(findstring hashicorp/azurerm,$(2)),grep -qs "azurerm" $(1) || bash -c 'echo -e "$(call azurerm_provider)"' >> $(1),) + $(if $(findstring integrations/github,$(2)),grep -qs "github" $(1) || bash -c 'echo -e "$(call github_provider)"' >> $(1),) +endef + +define create_example_providers + $(eval PROVIDER_FILE_PATH:=$(call provider_file_path,$(1))) + $(eval INCLUDE_AWS_GLOBAL:=$(if $(filter .,$(1)),$(findstring aws.global,$(shell grep -se "\\s*provider\\s*=" *.tf || true)),)) + $(foreach PROVIDER,$(shell terraform providers | sed -re 's/.+\[(.+\/.+\/.+)\].+/\1/g' | grep registry | sort | uniq),$(call add_provider_details,$(PROVIDER_FILE_PATH),$(PROVIDER))) +endef + +define plan_terraform_module + echo && echo "Planning $(1) ..."; + if [ -f "$(1)/$(VAR_FILE)" ]; then \ + echo "Using $(VAR_FILE) for variable inputs"; \ + $(TERRAFORM) -chdir=$(1) plan -input=false -out=terraform.tfplan -var-file $(VAR_FILE); \ + else \ + echo "$(VAR_FILE) doesn't exist, falling back to default (terraform.tfvars)"; \ + $(TERRAFORM) -chdir=$(1) plan -input=false -out=terraform.tfplan; \ + fi; + echo && echo "Creating JSON plan output for $(1) ..."; + cd $(1) && $(TERRAFORM) show -json ./terraform.tfplan > ./terraform.tfplan.json; + +endef + +define tflint_terraform_module + echo && echo "Linting $(1) ..."; + (cd $(1) && TF_LOG=info $(TFLINT) -c $(PWD)/$(TFLINT_CONFIG)) || exit 1; + +endef + +define validate_terraform_module + echo && echo "Validating $(1) ..."; + $(TERRAFORM) -chdir=$(1) validate || exit 1; + +endef + +# ============================================================================== +# Targets โ€” Setup / Configuration +# ============================================================================== .PHONY: configure-dependencies configure-dependencies: @@ -99,34 +230,168 @@ else $(error Missing supported dependency manager. Install asdf-vm (https://asdf-vm.com/) or mise (https://mise.jdx.dev/) and rerun) endif +.PHONY: configure-git-hooks +configure-git-hooks: configure-dependencies +ifeq ($(PYTHON3_INSTALLED), 0) + pre-commit install +else + $(error Missing python3, which is required for pre-commit. Install python3 and rerun.) +endif + .PHONY: configure configure: configure-git-hooks ifneq ($(and $(GIT_USER_SET), $(GIT_EMAIL_SET)), 0) $(error Git identities are not set! Set your user.name and user.email using 'git config' and rerun) endif -ifeq ($(REPO_INSTALLED), 0) - echo n | repo --color=never init --no-repo-verify \ - -u "$(REPO_MANIFESTS_URL)" \ - -b "$(REPO_BRANCH)" \ - -m "$(REPO_MANIFEST)" - repo envsubst - repo sync -else - $(error Missing Repo, which is required for platform sync. Install Repo (https://gerrit.googlesource.com/git-repo) and rerun.) -endif -# The first line finds and removes all the directories pulled in by repo -# The second line finds and removes all the broken symlinks from removing things -# https://stackoverflow.com/questions/42828021/removing-files-with-rm-using-find-and-xargs .PHONY: clean clean: - -repo list | awk '{ print $1; }' | cut -d '/' -f1 | uniq | xargs rm -rf - find . -type l ! -exec test -e {} \; -print | xargs rm -rf - -.PHONY: init-clean -init-clean: - rm -rf .git - git init --initial-branch=main -ifneq (,$(wildcard ./TEMPLATED_README.md)) - mv TEMPLATED_README.md README.MD + $(MAKE) tfmodule/clean + +# ============================================================================== +# Targets โ€” Check / Lint / Test (base double-colon rules) +# ============================================================================== + +.PHONY: check +check: + $(MAKE) lint + $(MAKE) tfmodule/test + $(MAKE) test + +.PHONY: lint +lint:: + @true + +.PHONY: test +test:: + @true + +# ============================================================================== +# Targets โ€” Golang +# ============================================================================== + +.PHONY: go/test/environment/az +go/test/environment/az: +ifeq (,$(ARM_SUBSCRIPTION_ID)) + $(error "ARM_SUBSCRIPTION_ID was not set and `az` was not found in your PATH.") +else + @echo "Terratest will use Azure Subscription ID $$ARM_SUBSCRIPTION_ID" +endif + +.PHONY: go/test/environment/aws +go/test/environment/aws: + @echo "No environment configuration for AWS is defined." + +.PHONY: go/test/environment/gcp +go/test/environment/gcp: + @echo "No environment configuration for GCP is defined." + +# Performs any environmental setup required for a cloud provider based on the name of the repository. +.PHONY: go/test/environment +go/test/environment: +ifneq (,$(findstring tf-aws,$(CURRENT_DIR))) + $(MAKE) go/test/environment/aws +else ifneq (,$(findstring tf-az,$(CURRENT_DIR))) + $(MAKE) go/test/environment/az +else ifneq (,$(findstring tf-gcp,$(CURRENT_DIR))) + $(MAKE) go/test/environment/gcp +else + @echo "Unrecognized module type, no environmental setup will be performed." +endif + +.PHONY: go/list +go/list: + @echo -n "Test dirs: " + @echo $(GO_TEST_DIRECTORIES) | tr ' ' '\n' | sort + +.PHONY: go/lint +go/lint: + $(foreach test_dir,$(GO_TEST_DIRECTORIES),$(call go_lint,$(test_dir))) + +.PHONY: go/test +go/test: go/test/environment + $(foreach test_dir,$(GO_TEST_DIRECTORIES),$(call go_test,$(test_dir),$(TEST_RUN_EXCLUDE_READONLY))) + +.PHONY: go/readonly_test +go/readonly_test: + $(foreach test_dir,$(GO_TEST_DIRECTORIES),$(call go_test,$(test_dir),$(TEST_RUN_ONLY_READONLY))) + +# Golang extensions to lint / test +.PHONY: lint +lint:: +ifeq ($(DISABLE_MAKE_CHECK_LINT),false) + $(MAKE) go/lint +else + $(info "make go/lint has been disabled!") +endif + +.PHONY: test +test:: tfmodule/plan + $(MAKE) go/test + +# ============================================================================== +# Targets โ€” Terraform module +# ============================================================================== + +.PHONY: tfmodule/all +tfmodule/all: lint + +.PHONY: tfmodule/clean +tfmodule/clean: + @$(foreach module,$(ALL_TF_MODULES),$(call clean_terraform_module,$(module))) + @$(foreach module,$(ALL_EXAMPLES),$(call clean_terraform_module,$(module))) + +.PHONY: tfmodule/fmt +tfmodule/fmt: + $(TERRAFORM) fmt -recursive; + +.PHONY: tfmodule/init +tfmodule/init: + @$(foreach module,$(ALL_TF_MODULES),$(call init_terraform_module,$(module))) + @$(foreach module,$(ALL_EXAMPLES),$(call init_terraform_module,$(module))) + +.PHONY: tfmodule/lint +tfmodule/lint: tfmodule/init + @$(call check_terraform_fmt) + @$(foreach module,$(ALL_TF_MODULES),$(call tflint_terraform_module,$(module))) + @$(foreach module,$(ALL_TF_MODULES),$(call validate_terraform_module,$(module))) + @$(foreach module,$(ALL_EXAMPLES),$(call tflint_terraform_module,$(module))) + @$(foreach module,$(ALL_EXAMPLES),$(call validate_terraform_module,$(module))) + +.PHONY: tfmodule/list +tfmodule/list: + @echo -n "Modules: " + @echo $(ALL_TF_MODULES) | tr ' ' '\n' | sort + @echo -n "Examples: " + @echo $(ALL_EXAMPLES) | tr ' ' '\n' | sort + +.PHONY: tfmodule/plan +tfmodule/plan: tfmodule/init + @$(foreach module,$(ALL_EXAMPLES),$(call plan_terraform_module,$(module))) + +.PHONY: tfmodule/test +tfmodule/test: tfmodule/init + @$(FIND) . -maxdepth 2 -path './.terraform' -prune -o -name '*.tftest.hcl' -print | $(GREP) -q 'tftest' || { echo "No Terraform test files found, skipping terraform test."; exit 0; }; \ + echo && echo "Running Terraform tests ..."; \ + $(TERRAFORM) test + +.PHONY: tfmodule/create_example_providers +tfmodule/create_example_providers: tfmodule/init +ifdef PROVIDER_TEMPLATE + @$(if $(wildcard $(PROVIDER_TEMPLATE)),,$(error PROVIDER_TEMPLATE is set to '$(PROVIDER_TEMPLATE)' but the file does not exist)) + @$(foreach example,$(ALL_EXAMPLES),cp $(PROVIDER_TEMPLATE) $(call provider_file_path,$(example));) +else + @$(if $(findstring aws.global,$(shell grep -se "\\s*provider\\s*=" *.tf || true)),$(call create_example_providers,.),) + @$(foreach example,$(ALL_EXAMPLES),$(call create_example_providers,$(example))) endif + +# Terraform module extensions to lint / test +.PHONY: lint +lint:: + $(MAKE) tfmodule/create_example_providers + $(MAKE) tfmodule/lint + +.PHONY: test +test:: + $(MAKE) tfmodule/create_example_providers + $(MAKE) tfmodule/plan diff --git a/README.md b/README.md index 3212c0f..081e097 100644 --- a/README.md +++ b/README.md @@ -111,10 +111,6 @@ If `make check` target is successful, developer is good to commit the code to pr | [terraform](#requirement\_terraform) | ~> 1.5 | | [aws](#requirement\_aws) | ~> 5.14 | -## Providers - -No providers. - ## Modules | Name | Source | Version | @@ -129,71 +125,71 @@ No resources. | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| -| [name](#input\_name) | The name of this Lambda Function | `string` | n/a | yes | -| [description](#input\_description) | Description of your Lambda Function | `string` | `""` | no | -| [handler](#input\_handler) | Lambda Function entrypoint in your code | `string` | `"index.lambda_handler"` | no | -| [runtime](#input\_runtime) | Lambda Function runtime | `string` | `"python3.9"` | no | +| [allowed\_triggers](#input\_allowed\_triggers) | Map of allowed triggers to create Lambda permissions | `map(any)` | `{}` | no | | [architectures](#input\_architectures) | (Optional) Instruction set architecture for your Lambda function. Valid architectures are x86\_64 (default) and arm64. | `list(string)` |
[
"x86_64"
]
| no | -| [publish](#input\_publish) | Whether to publish creation/change as new Lambda Function Version. | `bool` | `true` | no | -| [ephemeral\_storage\_size](#input\_ephemeral\_storage\_size) | mount of ephemeral storage (/tmp) in MB your Lambda Function can use at runtime. Valid values are between 512 MB to 10,240 MB (10 GB). | `number` | `512` | no | -| [environment\_variables](#input\_environment\_variables) | A map that defines environment variables for the Lambda Function. | `map(string)` | `{}` | no | -| [memory\_size](#input\_memory\_size) | Amount of memory in MB your Lambda Function can use at runtime. Valid values are between 128 MB to 10,240 MB (10 GB), in 64 MB increments. | `number` | `128` | no | -| [timeout](#input\_timeout) | The amount of time your Lambda Function has to run in seconds. The maximum lifetime of a Lambda function execution is 15 minutes (900 seconds). | `number` | `3` | no | -| [create\_package](#input\_create\_package) | Controls whether Lambda package should be created | `bool` | `false` | no | -| [source\_path](#input\_source\_path) | The absolute path to a local file or directory containing your Lambda source code. Only valid if `create_package` is set to `true`. | `any` | `null` | no | -| [zip\_file\_path](#input\_zip\_file\_path) | Path of the source zip file with respect to module root | `string` | `null` | no | -| [store\_on\_s3](#input\_store\_on\_s3) | Whether to store produced artifacts on S3 or locally. | `bool` | `false` | no | -| [s3\_existing\_package](#input\_s3\_existing\_package) | The S3 bucket object with keys bucket, key, version pointing to an existing zip-file to use. Only valid if `create_package` is set to `false`. | `map(string)` | `null` | no | -| [s3\_bucket](#input\_s3\_bucket) | S3 bucket to store artifacts. Required if `store_on_s3` is set to `true`, ignored otherwise. | `string` | `null` | no | -| [s3\_prefix](#input\_s3\_prefix) | Directory name where artifacts should be stored in the S3 bucket. Defaults to `builds`. Required if `store_on_s3` is set to `true`, ignored otherwise. | `string` | `"builds"` | no | -| [layers](#input\_layers) | List of Lambda Layer Version ARNs (maximum of 5) to attach to your Lambda Function. | `list(string)` | `null` | no | -| [hash\_extra](#input\_hash\_extra) | The string to add into hashing function. Useful when building same source path for different functions. | `string` | `""` | no | -| [ignore\_source\_code\_hash](#input\_ignore\_source\_code\_hash) | Whether to ignore changes to the function's source code hash. Set to true if you manage infrastructure and code deployments separately. | `bool` | `false` | no | -| [authorization\_type](#input\_authorization\_type) | The type of authentication that the Lambda Function URL uses. Set to `AWS_IAM` to restrict access to authenticated IAM users only. Set to `NONE` to bypass IAM authentication and create a public endpoint (default). | `string` | `"NONE"` | no | -| [cors](#input\_cors) | CORS settings to be used by the Lambda Function URL |
object({
allow_credentials = optional(bool, false)
allow_headers = optional(list(string), null)
allow_methods = optional(list(string), null)
allow_origins = optional(list(string), null)
expose_headers = optional(list(string), null)
max_age = optional(number, 0)
})
| `{}` | no | -| [create\_lambda\_function\_url](#input\_create\_lambda\_function\_url) | Whether the Lambda Function URL resource should be created (default true). | `bool` | `true` | no | -| [invoke\_mode](#input\_invoke\_mode) | Invoke mode of the Lambda Function URL. Valid values are `BUFFERED` (default) and `RESPONSE_STREAM`. | `string` | `"BUFFERED"` | no | -| [attach\_policy\_statements](#input\_attach\_policy\_statements) | Controls whether `policy_statements` should be added to IAM role for Lambda Function | `bool` | `false` | no | -| [policy\_statements](#input\_policy\_statements) | Map of dynamic policy statements to attach to Lambda Function role | `any` | `{}` | no | -| [attach\_policy](#input\_attach\_policy) | Controls whether `policy` should be added to IAM role for Lambda Function | `bool` | `false` | no | -| [policy](#input\_policy) | Policy statement ARN to attach to Lambda Function role | `string` | `null` | no | +| [assume\_role\_policy\_statements](#input\_assume\_role\_policy\_statements) | Map of dynamic policy statements for assuming Lambda Function role (trust relationship) | `map(string)` | `{}` | no | +| [attach\_async\_event\_policy](#input\_attach\_async\_event\_policy) | Controls whether async event policy should be added to IAM role for Lambda Function | `bool` | `false` | no | +| [attach\_cloudwatch\_logs\_policy](#input\_attach\_cloudwatch\_logs\_policy) | Controls whether CloudWatch Logs policy should be added to IAM role for Lambda Function | `bool` | `true` | no | +| [attach\_create\_log\_group\_permission](#input\_attach\_create\_log\_group\_permission) | Controls whether to add the create log group permission to the CloudWatch logs policy | `bool` | `true` | no | +| [attach\_dead\_letter\_policy](#input\_attach\_dead\_letter\_policy) | Controls whether SNS/SQS dead letter notification policy should be added to IAM role for Lambda Function. Defaults to `false`. | `bool` | `false` | no | +| [attach\_network\_policy](#input\_attach\_network\_policy) | Controls whether VPC/network policy should be added to IAM role for Lambda Function | `bool` | `false` | no | | [attach\_policies](#input\_attach\_policies) | Controls whether `policies` should be added to IAM role for Lambda Function | `bool` | `false` | no | -| [policies](#input\_policies) | List of policy statement ARNs to attach to Lambda Function role | `list(string)` | `[]` | no | +| [attach\_policy](#input\_attach\_policy) | Controls whether `policy` should be added to IAM role for Lambda Function | `bool` | `false` | no | | [attach\_policy\_json](#input\_attach\_policy\_json) | Controls whether `policy_json` should be added to IAM role for Lambda Function | `bool` | `false` | no | -| [policy\_json](#input\_policy\_json) | An additional policy document as JSON to attach to the Lambda Function role | `string` | `null` | no | | [attach\_policy\_jsons](#input\_attach\_policy\_jsons) | Controls whether `policy_jsons` should be added to IAM role for Lambda Function | `bool` | `false` | no | -| [policy\_jsons](#input\_policy\_jsons) | An additional policy documents as JSON to attach to the Lambda Function role | `list(string)` | `[]` | no | -| [attach\_dead\_letter\_policy](#input\_attach\_dead\_letter\_policy) | Controls whether SNS/SQS dead letter notification policy should be added to IAM role for Lambda Function. Defaults to `false`. | `bool` | `false` | no | -| [dead\_letter\_target\_arn](#input\_dead\_letter\_target\_arn) | The ARN of an SNS topic or SQS queue to notify when an invocation fails. | `string` | `null` | no | -| [attach\_network\_policy](#input\_attach\_network\_policy) | Controls whether VPC/network policy should be added to IAM role for Lambda Function | `bool` | `false` | no | -| [attach\_async\_event\_policy](#input\_attach\_async\_event\_policy) | Controls whether async event policy should be added to IAM role for Lambda Function | `bool` | `false` | no | +| [attach\_policy\_statements](#input\_attach\_policy\_statements) | Controls whether `policy_statements` should be added to IAM role for Lambda Function | `bool` | `false` | no | | [attach\_tracing\_policy](#input\_attach\_tracing\_policy) | Controls whether X-Ray tracing policy should be added to IAM role for Lambda Function | `bool` | `false` | no | -| [assume\_role\_policy\_statements](#input\_assume\_role\_policy\_statements) | Map of dynamic policy statements for assuming Lambda Function role (trust relationship) | `map(string)` | `{}` | no | -| [trusted\_entities](#input\_trusted\_entities) | List of additional trusted entities for assuming Lambda Function role (trust relationship) | `any` | `[]` | no | -| [allowed\_triggers](#input\_allowed\_triggers) | Map of allowed triggers to create Lambda permissions | `map(any)` | `{}` | no | -| [attach\_cloudwatch\_logs\_policy](#input\_attach\_cloudwatch\_logs\_policy) | Controls whether CloudWatch Logs policy should be added to IAM role for Lambda Function | `bool` | `true` | no | -| [attach\_create\_log\_group\_permission](#input\_attach\_create\_log\_group\_permission) | Controls whether to add the create log group permission to the CloudWatch logs policy | `bool` | `true` | no | +| [authorization\_type](#input\_authorization\_type) | The type of authentication that the Lambda Function URL uses. Set to `AWS_IAM` to restrict access to authenticated IAM users only. Set to `NONE` to bypass IAM authentication and create a public endpoint (default). | `string` | `"NONE"` | no | | [cloudwatch\_logs\_kms\_key\_id](#input\_cloudwatch\_logs\_kms\_key\_id) | The ARN of the KMS Key to use when encrypting log data. | `string` | `null` | no | | [cloudwatch\_logs\_log\_group\_class](#input\_cloudwatch\_logs\_log\_group\_class) | Specified the log class of the log group. Possible values are: `STANDARD` (default) or `INFREQUENT_ACCESS` | `string` | `"STANDARD"` | no | | [cloudwatch\_logs\_retention\_in\_days](#input\_cloudwatch\_logs\_retention\_in\_days) | Specifies the number of days you want to retain log events in the specified log group. Possible values are: 1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, 400, 545, 731, 1827, and 3653. Defaults to 30. | `number` | `30` | no | | [cloudwatch\_logs\_skip\_destroy](#input\_cloudwatch\_logs\_skip\_destroy) | Whether to keep the log group (and any logs it may contain) at destroy time. Defaults to false. | `bool` | `false` | no | | [cloudwatch\_logs\_tags](#input\_cloudwatch\_logs\_tags) | A map of tags to assign to the logs resource. | `map(string)` | `{}` | no | -| [tracing\_mode](#input\_tracing\_mode) | Tracing mode of the Lambda Function. Valid value can be either PassThrough (default) or Active. | `string` | `"PassThrough"` | no | -| [vpc\_security\_group\_ids](#input\_vpc\_security\_group\_ids) | List of security group ids when Lambda Function should run in the VPC. | `list(string)` | `null` | no | -| [vpc\_subnet\_ids](#input\_vpc\_subnet\_ids) | List of subnet ids when Lambda Function should run in the VPC. Usually private or intra subnets. | `list(string)` | `null` | no | +| [cors](#input\_cors) | CORS settings to be used by the Lambda Function URL |
object({
allow_credentials = optional(bool, false)
allow_headers = optional(list(string), null)
allow_methods = optional(list(string), null)
allow_origins = optional(list(string), null)
expose_headers = optional(list(string), null)
max_age = optional(number, 0)
})
| `{}` | no | +| [create](#input\_create) | Controls whether resources should be created. | `bool` | `false` | no | +| [create\_lambda\_function\_url](#input\_create\_lambda\_function\_url) | Whether the Lambda Function URL resource should be created (default true). | `bool` | `true` | no | +| [create\_package](#input\_create\_package) | Controls whether Lambda package should be created | `bool` | `false` | no | +| [dead\_letter\_target\_arn](#input\_dead\_letter\_target\_arn) | The ARN of an SNS topic or SQS queue to notify when an invocation fails. | `string` | `null` | no | +| [description](#input\_description) | Description of your Lambda Function | `string` | `""` | no | +| [environment\_variables](#input\_environment\_variables) | A map that defines environment variables for the Lambda Function. | `map(string)` | `{}` | no | +| [ephemeral\_storage\_size](#input\_ephemeral\_storage\_size) | mount of ephemeral storage (/tmp) in MB your Lambda Function can use at runtime. Valid values are between 512 MB to 10,240 MB (10 GB). | `number` | `512` | no | +| [handler](#input\_handler) | Lambda Function entrypoint in your code | `string` | `"index.lambda_handler"` | no | +| [hash\_extra](#input\_hash\_extra) | The string to add into hashing function. Useful when building same source path for different functions. | `string` | `""` | no | +| [ignore\_source\_code\_hash](#input\_ignore\_source\_code\_hash) | Whether to ignore changes to the function's source code hash. Set to true if you manage infrastructure and code deployments separately. | `bool` | `false` | no | +| [invoke\_mode](#input\_invoke\_mode) | Invoke mode of the Lambda Function URL. Valid values are `BUFFERED` (default) and `RESPONSE_STREAM`. | `string` | `"BUFFERED"` | no | | [lambda\_at\_edge](#input\_lambda\_at\_edge) | Set this to true if using Lambda@Edge, to enable publishing, limit the timeout, and allow edgelambda.amazonaws.com to invoke the function | `bool` | `false` | no | | [lambda\_at\_edge\_logs\_all\_regions](#input\_lambda\_at\_edge\_logs\_all\_regions) | Whether to specify a wildcard in IAM policy used by Lambda@Edge to allow logging in all regions | `bool` | `true` | no | +| [layers](#input\_layers) | List of Lambda Layer Version ARNs (maximum of 5) to attach to your Lambda Function. | `list(string)` | `null` | no | +| [memory\_size](#input\_memory\_size) | Amount of memory in MB your Lambda Function can use at runtime. Valid values are between 128 MB to 10,240 MB (10 GB), in 64 MB increments. | `number` | `128` | no | +| [name](#input\_name) | The name of this Lambda Function | `string` | n/a | yes | +| [policies](#input\_policies) | List of policy statement ARNs to attach to Lambda Function role | `list(string)` | `[]` | no | +| [policy](#input\_policy) | Policy statement ARN to attach to Lambda Function role | `string` | `null` | no | +| [policy\_json](#input\_policy\_json) | An additional policy document as JSON to attach to the Lambda Function role | `string` | `null` | no | +| [policy\_jsons](#input\_policy\_jsons) | An additional policy documents as JSON to attach to the Lambda Function role | `list(string)` | `[]` | no | +| [policy\_statements](#input\_policy\_statements) | Map of dynamic policy statements to attach to Lambda Function role | `any` | `{}` | no | +| [publish](#input\_publish) | Whether to publish creation/change as new Lambda Function Version. | `bool` | `true` | no | +| [runtime](#input\_runtime) | Lambda Function runtime | `string` | `"python3.9"` | no | +| [s3\_bucket](#input\_s3\_bucket) | S3 bucket to store artifacts. Required if `store_on_s3` is set to `true`, ignored otherwise. | `string` | `null` | no | +| [s3\_existing\_package](#input\_s3\_existing\_package) | The S3 bucket object with keys bucket, key, version pointing to an existing zip-file to use. Only valid if `create_package` is set to `false`. | `map(string)` | `null` | no | +| [s3\_prefix](#input\_s3\_prefix) | Directory name where artifacts should be stored in the S3 bucket. Defaults to `builds`. Required if `store_on_s3` is set to `true`, ignored otherwise. | `string` | `"builds"` | no | +| [source\_path](#input\_source\_path) | The absolute path to a local file or directory containing your Lambda source code. Only valid if `create_package` is set to `true`. | `any` | `null` | no | +| [store\_on\_s3](#input\_store\_on\_s3) | Whether to store produced artifacts on S3 or locally. | `bool` | `false` | no | | [tags](#input\_tags) | Map of tags to apply to this resource. | `map(string)` | `{}` | no | -| [create](#input\_create) | Controls whether resources should be created. | `bool` | `false` | no | +| [timeout](#input\_timeout) | The amount of time your Lambda Function has to run in seconds. The maximum lifetime of a Lambda function execution is 15 minutes (900 seconds). | `number` | `3` | no | +| [tracing\_mode](#input\_tracing\_mode) | Tracing mode of the Lambda Function. Valid value can be either PassThrough (default) or Active. | `string` | `"PassThrough"` | no | +| [trusted\_entities](#input\_trusted\_entities) | List of additional trusted entities for assuming Lambda Function role (trust relationship) | `any` | `[]` | no | +| [vpc\_security\_group\_ids](#input\_vpc\_security\_group\_ids) | List of security group ids when Lambda Function should run in the VPC. | `list(string)` | `null` | no | +| [vpc\_subnet\_ids](#input\_vpc\_subnet\_ids) | List of subnet ids when Lambda Function should run in the VPC. Usually private or intra subnets. | `list(string)` | `null` | no | +| [zip\_file\_path](#input\_zip\_file\_path) | Path of the source zip file with respect to module root | `string` | `null` | no | ## Outputs | Name | Description | |------|-------------| -| [lambda\_function\_arn](#output\_lambda\_function\_arn) | n/a | -| [lambda\_function\_name](#output\_lambda\_function\_name) | n/a | | [lambda\_cloudwatch\_log\_group\_arn](#output\_lambda\_cloudwatch\_log\_group\_arn) | n/a | | [lambda\_cloudwatch\_log\_group\_name](#output\_lambda\_cloudwatch\_log\_group\_name) | n/a | +| [lambda\_function\_arn](#output\_lambda\_function\_arn) | n/a | +| [lambda\_function\_name](#output\_lambda\_function\_name) | n/a | | [lambda\_function\_url](#output\_lambda\_function\_url) | n/a | | [lambda\_role\_arn](#output\_lambda\_role\_arn) | n/a | | [lambda\_role\_name](#output\_lambda\_role\_name) | n/a | diff --git a/examples/source_from_folder/README.md b/examples/source_from_folder/README.md index 6e23cdd..e6ea8d1 100644 --- a/examples/source_from_folder/README.md +++ b/examples/source_from_folder/README.md @@ -2,7 +2,7 @@ Demonstrates creating a Lambda Function from a local folder. - + ## Requirements | Name | Version | @@ -10,10 +10,6 @@ Demonstrates creating a Lambda Function from a local folder. | [terraform](#requirement\_terraform) | ~> 1.5 | | [aws](#requirement\_aws) | ~> 5.14 | -## Providers - -No providers. - ## Modules | Name | Source | Version | @@ -29,25 +25,25 @@ No resources. | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| -| [resource\_names\_map](#input\_resource\_names\_map) | A map of key to resource\_name that will be used by tf-launch-module\_library-resource\_name to generate resource names |
map(object({
name = string
max_length = optional(number, 60)
region = optional(string, "eastus2")
}))
|
{
"lambda_function": {
"max_length": 80,
"name": "fn",
"region": "us-east-2"
}
}
| no | -| [instance\_env](#input\_instance\_env) | Number that represents the instance of the environment. | `number` | `0` | no | -| [instance\_resource](#input\_instance\_resource) | Number that represents the instance of the resource. | `number` | `0` | no | -| [logical\_product\_family](#input\_logical\_product\_family) | (Required) Name of the product family for which the resource is created.
Example: org\_name, department\_name. | `string` | `"launch"` | no | -| [logical\_product\_service](#input\_logical\_product\_service) | (Required) Name of the product service for which the resource is created.
For example, backend, frontend, middleware etc. | `string` | `"lambda"` | no | | [class\_env](#input\_class\_env) | (Required) Environment where resource is going to be deployed. For example. dev, qa, uat | `string` | `"demo"` | no | | [create\_package](#input\_create\_package) | Controls whether Lambda package should be created | `bool` | `false` | no | | [handler](#input\_handler) | Lambda Function entrypoint in your code | `string` | `"index.lambda_handler"` | no | +| [instance\_env](#input\_instance\_env) | Number that represents the instance of the environment. | `number` | `0` | no | +| [instance\_resource](#input\_instance\_resource) | Number that represents the instance of the resource. | `number` | `0` | no | +| [logical\_product\_family](#input\_logical\_product\_family) | (Required) Name of the product family for which the resource is created.
Example: org\_name, department\_name. | `string` | `"launch"` | no | +| [logical\_product\_service](#input\_logical\_product\_service) | (Required) Name of the product service for which the resource is created.
For example, backend, frontend, middleware etc. | `string` | `"lambda"` | no | +| [resource\_names\_map](#input\_resource\_names\_map) | A map of key to resource\_name that will be used by tf-launch-module\_library-resource\_name to generate resource names |
map(object({
name = string
max_length = optional(number, 60)
region = optional(string, "eastus2")
}))
|
{
"lambda_function": {
"max_length": 80,
"name": "fn",
"region": "us-east-2"
}
}
| no | | [source\_path](#input\_source\_path) | The absolute path to a local file or directory containing your Lambda source code. Only valid if `create_package` is set to `true`. | `any` | `null` | no | ## Outputs | Name | Description | |------|-------------| -| [lambda\_function\_arn](#output\_lambda\_function\_arn) | n/a | -| [lambda\_function\_name](#output\_lambda\_function\_name) | n/a | | [lambda\_cloudwatch\_log\_group\_arn](#output\_lambda\_cloudwatch\_log\_group\_arn) | n/a | | [lambda\_cloudwatch\_log\_group\_name](#output\_lambda\_cloudwatch\_log\_group\_name) | n/a | +| [lambda\_function\_arn](#output\_lambda\_function\_arn) | n/a | +| [lambda\_function\_name](#output\_lambda\_function\_name) | n/a | | [lambda\_function\_url](#output\_lambda\_function\_url) | n/a | | [lambda\_role\_arn](#output\_lambda\_role\_arn) | n/a | | [lambda\_role\_name](#output\_lambda\_role\_name) | n/a | - + diff --git a/examples/source_from_zip/README.md b/examples/source_from_zip/README.md index 5e5139d..dd48b7f 100644 --- a/examples/source_from_zip/README.md +++ b/examples/source_from_zip/README.md @@ -2,7 +2,7 @@ Demonstrates creating a Lambda Function from a zip file. - + ## Requirements | Name | Version | @@ -10,10 +10,6 @@ Demonstrates creating a Lambda Function from a zip file. | [terraform](#requirement\_terraform) | ~> 1.5 | | [aws](#requirement\_aws) | ~> 5.14 | -## Providers - -No providers. - ## Modules | Name | Source | Version | @@ -29,25 +25,25 @@ No resources. | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| -| [resource\_names\_map](#input\_resource\_names\_map) | A map of key to resource\_name that will be used by tf-launch-module\_library-resource\_name to generate resource names |
map(object({
name = string
max_length = optional(number, 60)
region = optional(string, "eastus2")
}))
|
{
"lambda_function": {
"max_length": 80,
"name": "fn",
"region": "us-east-2"
}
}
| no | -| [instance\_env](#input\_instance\_env) | Number that represents the instance of the environment. | `number` | `0` | no | -| [instance\_resource](#input\_instance\_resource) | Number that represents the instance of the resource. | `number` | `0` | no | -| [logical\_product\_family](#input\_logical\_product\_family) | (Required) Name of the product family for which the resource is created.
Example: org\_name, department\_name. | `string` | `"launch"` | no | -| [logical\_product\_service](#input\_logical\_product\_service) | (Required) Name of the product service for which the resource is created.
For example, backend, frontend, middleware etc. | `string` | `"lambda"` | no | | [class\_env](#input\_class\_env) | (Required) Environment where resource is going to be deployed. For example. dev, qa, uat | `string` | `"demo"` | no | | [create\_package](#input\_create\_package) | Controls whether Lambda package should be created | `bool` | `false` | no | | [handler](#input\_handler) | Lambda Function entrypoint in your code | `string` | `"index.lambda_handler"` | no | +| [instance\_env](#input\_instance\_env) | Number that represents the instance of the environment. | `number` | `0` | no | +| [instance\_resource](#input\_instance\_resource) | Number that represents the instance of the resource. | `number` | `0` | no | +| [logical\_product\_family](#input\_logical\_product\_family) | (Required) Name of the product family for which the resource is created.
Example: org\_name, department\_name. | `string` | `"launch"` | no | +| [logical\_product\_service](#input\_logical\_product\_service) | (Required) Name of the product service for which the resource is created.
For example, backend, frontend, middleware etc. | `string` | `"lambda"` | no | +| [resource\_names\_map](#input\_resource\_names\_map) | A map of key to resource\_name that will be used by tf-launch-module\_library-resource\_name to generate resource names |
map(object({
name = string
max_length = optional(number, 60)
region = optional(string, "eastus2")
}))
|
{
"lambda_function": {
"max_length": 80,
"name": "fn",
"region": "us-east-2"
}
}
| no | | [zip\_file\_path](#input\_zip\_file\_path) | Path of the source zip file with respect to module root | `string` | `null` | no | ## Outputs | Name | Description | |------|-------------| -| [lambda\_function\_arn](#output\_lambda\_function\_arn) | n/a | -| [lambda\_function\_name](#output\_lambda\_function\_name) | n/a | | [lambda\_cloudwatch\_log\_group\_arn](#output\_lambda\_cloudwatch\_log\_group\_arn) | n/a | | [lambda\_cloudwatch\_log\_group\_name](#output\_lambda\_cloudwatch\_log\_group\_name) | n/a | +| [lambda\_function\_arn](#output\_lambda\_function\_arn) | n/a | +| [lambda\_function\_name](#output\_lambda\_function\_name) | n/a | | [lambda\_function\_url](#output\_lambda\_function\_url) | n/a | | [lambda\_role\_arn](#output\_lambda\_role\_arn) | n/a | | [lambda\_role\_name](#output\_lambda\_role\_name) | n/a | - + From dcc96bcf38582bd2b2f4430c23bfd51d162198ba Mon Sep 17 00:00:00 2001 From: oscar-hernandez-nttd <335457@nttdata.com> Date: Tue, 21 Jul 2026 09:51:16 -0600 Subject: [PATCH 2/2] fix: implement real readonly test and rename copy-pasted test entrypoints The readonly suite was a byte-for-byte copy of the functional suite, applying/destroying infra and invoking the function over HTTP instead of only performing read-only verification. Add TestComposableCompleteReadOnly (GetFunction checks only, no invoke), wire the readonly entrypoint to lib.RunNonDestructiveTest, and have TestComposableComplete build on the readonly impl before adding the invoke assertions. Also rename both entrypoints from TestLambdaLayerModule, a leftover scaffold name for a different Lambda resource type, to TestLambdaFunctionModule(ReadOnly). --- tests/post_deploy_functional/main_test.go | 2 +- tests/post_deploy_functional_readonly/main_test.go | 9 ++------- tests/testimpl/test_impl.go | 9 +++++++-- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/post_deploy_functional/main_test.go b/tests/post_deploy_functional/main_test.go index 9ef78f2..c4d71b4 100644 --- a/tests/post_deploy_functional/main_test.go +++ b/tests/post_deploy_functional/main_test.go @@ -25,7 +25,7 @@ const ( infraTFVarFileNameDefault = "test.tfvars" ) -func TestLambdaLayerModule(t *testing.T) { +func TestLambdaFunctionModule(t *testing.T) { ctx := types.CreateTestContextBuilder(). SetTestConfig(&testimpl.ThisTFModuleConfig{}). diff --git a/tests/post_deploy_functional_readonly/main_test.go b/tests/post_deploy_functional_readonly/main_test.go index 9ef78f2..fa84952 100644 --- a/tests/post_deploy_functional_readonly/main_test.go +++ b/tests/post_deploy_functional_readonly/main_test.go @@ -25,18 +25,13 @@ const ( infraTFVarFileNameDefault = "test.tfvars" ) -func TestLambdaLayerModule(t *testing.T) { +func TestLambdaFunctionModuleReadOnly(t *testing.T) { ctx := types.CreateTestContextBuilder(). SetTestConfig(&testimpl.ThisTFModuleConfig{}). SetTestConfigFolderName(testConfigsExamplesFolderDefault). SetTestConfigFileName(infraTFVarFileNameDefault). - SetTestSpecificFlags(map[string]types.TestFlags{ - "source_from_folder": { - "IS_TERRAFORM_IDEMPOTENT_APPLY": false, - }, - }). Build() - lib.RunSetupTestTeardown(t, *ctx, testimpl.TestComposableComplete) + lib.RunNonDestructiveTest(t, *ctx, testimpl.TestComposableCompleteReadOnly) } diff --git a/tests/testimpl/test_impl.go b/tests/testimpl/test_impl.go index fc8cce1..c42d8d8 100644 --- a/tests/testimpl/test_impl.go +++ b/tests/testimpl/test_impl.go @@ -15,12 +15,11 @@ import ( "github.com/stretchr/testify/require" ) -func TestComposableComplete(t *testing.T, ctx types.TestContext) { +func TestComposableCompleteReadOnly(t *testing.T, ctx types.TestContext) { lambdaClient := GetAWSLambdaClient(t) functionArn := terraform.Output(t, ctx.TerratestTerraformOptions(), "lambda_function_arn") functionName := terraform.Output(t, ctx.TerratestTerraformOptions(), "lambda_function_name") - functionUrl := terraform.Output(t, ctx.TerratestTerraformOptions(), "lambda_function_url") t.Run("TestLambdaFunctionExists", func(t *testing.T) { function, err := lambdaClient.GetFunction(context.TODO(), &lambda.GetFunctionInput{ @@ -33,6 +32,12 @@ func TestComposableComplete(t *testing.T, ctx types.TestContext) { assert.Equal(t, *function.Configuration.FunctionArn, functionArn, "Expected ARN did not match actual ARN!") assert.Equal(t, *function.Configuration.FunctionName, functionName, "Expected Name did not match actual Name!") }) +} + +func TestComposableComplete(t *testing.T, ctx types.TestContext) { + TestComposableCompleteReadOnly(t, ctx) + + functionUrl := terraform.Output(t, ctx.TerratestTerraformOptions(), "lambda_function_url") t.Run("InvokeExampleSourceFromFolder", func(t *testing.T) { ctx.EnabledOnlyForTests(t, "source_from_folder")