Commit b9c71bad authored by Pierre Smeyers's avatar Pierre Smeyers
Browse files

Merge branch 'master' into 'master'

feat(tftest): terraform test framework

Closes #64

See merge request to-be-continuous/terraform!144
parents 5c7bdacd 2c7872b5
Loading
Loading
Loading
Loading
+68 −4
Original line number Diff line number Diff line
@@ -490,6 +490,70 @@ In addition to a textual report in the console, this job produces the following
| -------------- | ---------------------------------------------------------------------------- | ----------------- |
| `$TF_PROJECT_DIR/reports/tflint.xunit.xml` | [xUnit](https://en.wikipedia.org/wiki/XUnit) test report(s) | [GitLab integration](https://docs.gitlab.com/ci/yaml/artifacts_reports/#artifactsreportsjunit) |

### `tf-tftest` job

[tftest](https://developer.hashicorp.com/terraform/language/tests) is a native Terraform testing framework and uses the following variables:

| Input / Variable      | Description                              | Default value     |
| --------------------- | ---------------------------------------- | ----------------- |
| `tftest-strategy` / `TF_TFTEST_STRATEGY` | terraform test strategy<br\>one of: `disabled`, `single` (will run tests only on the environment mapped to the pipeline branch) or `cascading` (will also run tests on downstream environments) | `disabled` |
| `tftest-opts` / `TF_TFTEST_OPTS` | tftest [options and args](https://developer.hashicorp.com/terraform/cli/commands/test) | "" |

In addition to a textual report in the console, this job produces the following reports, kept for one day and only available for download by users with the Developer role or higher:

| Report         | Format                                                                       | Usage             |
| -------------- | ---------------------------------------------------------------------------- | ----------------- |
| `$TF_PROJECT_DIR/reports/${ENV_TYPE}-tftest.xunit.xml` | [xUnit](https://en.wikipedia.org/wiki/XUnit) test report(s) | [GitLab integration](https://docs.gitlab.com/ci/yaml/artifacts_reports/#artifactsreportsjunit) |

#### Customizing Terraform Tests per Environment

By default, when `tftest-strategy` is set to `cascading`, Terraform tests will run for all enabled environments. However, you may want to disable tests for certain environments (e.g., production) to avoid resource costs or long execution times.

To disable tests for a specific environment while keeping other jobs (plan/apply) enabled:

```yaml
include:
  - component: $CI_SERVER_FQDN/to-be-continuous/terraform/gitlab-ci-terraform@6.1.2
    inputs:
      tftest-strategy: cascading
      review-enabled: true
      staging-enabled: true
      # production env is enabled for deployment
      prod-enabled: true

# Disable tests for production environment only
tf-test-production:
  rules:
    - when: never
```

This approach allows production deployments to continue working normally while preventing resource-intensive infrastructure tests from running in production.

#### Customizing Test Files per Environment

You can run different test files or use different test directories for each environment using the TF_<ENV>_TFTEST_OPTS variables.

**Using different test directories**

```yaml
variables:
  TF_REVIEW_TFTEST_OPTS: "-test-directory=tests/lightweight"
  TF_STAGING_TFTEST_OPTS: "-test-directory=tests/comprehensive"
  TF_PROD_TFTEST_OPTS: "-test-directory=tests/smoke"
```

**Filtering specific test files**

```yaml
variables:
  TF_REVIEW_TFTEST_OPTS: "-filter=basic_test.tftest.hcl"
  TF_INTEG_TFTEST_OPTS: "-filter=integration_test.tftest.hcl -filter=performance_test.tftest.hcl"
  TF_STAGING_TFTEST_OPTS: "-filter=full_suite_test.tftest.hcl"
  TF_PROD_TFTEST_OPTS: "-filter=smoke_test.tftest.hcl"
```

This allows you to run lightweight tests in review environments and more comprehensive tests in staging, while potentially skipping expensive tests in production entirely.

### [DEPRECATED] `tf-tfsec` job

:warning: `tfsec` has been deprecated, it is recommended to use [trivy](#tf-trivy-job) instead.
+19 −0
Original line number Diff line number Diff line
@@ -230,6 +230,25 @@
        }
      ]
    },
    {
      "id": "tftest",
      "name": "Test Feature",
      "description": "A feature for testing purposes",
      "variables": [
        {
          "name": "TF_TFTEST_STRATEGY",
          "description": "terraform test strategy\n\none of: `disabled`, `single` (will run tests only on the environment mapped to the pipeline branch) or `cascading` (will also run tests on downstream environments)",
          "type": "enum",
          "values": ["disabled", "single", "cascading"],
          "default": "disabled"
        },
        {
          "name": "TF_TFTEST_OPTS",
          "description": "Extra options for the terraform native test feature",
          "advanced": true
         }
      ]
    },
    {
      "id": "tfpublish",
      "name": "publish module",
+170 −2
Original line number Diff line number Diff line
@@ -135,6 +135,19 @@ spec:
    docs-output-dir:
      description: terraform docs output directory (relative to `$TF_PROJECT_DIR`)
      default: docs
    tftest-strategy:
      description: |
        terraform test strategy

        one of: `disabled`, `single` (will run tests only on the environment mapped to the pipeline branch) or `cascading` (will also run tests on downstream environments)
      options:
      - disabled
      - single
      - cascading
      default: disabled
    tftest-opts:
      description: terraform test extra [options and args](https://developer.hashicorp.com/terraform/cli/commands/test)
      default: ''
    publish-enabled:
      description: Enable publish module
      type: boolean
@@ -332,6 +345,9 @@ variables:
  TF_DOCS_IMAGE: $[[ inputs.docs-image ]]
  TF_DOCS_CONFIG: $[[ inputs.docs-config ]]
  TF_DOCS_OUTPUT_DIR: $[[ inputs.docs-output-dir ]]
  # tf-tftest job config
  TF_TFTEST_STRATEGY: $[[ inputs.tftest-strategy ]]
  TF_TFTEST_OPTS: $[[ inputs.tftest-opts ]]
  # tf-module-publish job config
  TF_PUBLISH_IMAGE: $[[ inputs.publish-image ]]
  TF_MODULE_NAME: $[[ inputs.module-name ]]
@@ -826,6 +842,41 @@ stages:
    fi
  }

  tf_test() {
    opts=${ENV_TFTEST_OPTS:-$TF_TFTEST_OPTS}

    # shellcheck disable=SC2154
    log_info "--- \\e[32mtest\\e[0m"

    # unset any upstream deployment env & artifacts
    rm -f terraform.env

    # maybe execute pre test script
    prescript="$TF_SCRIPTS_DIR/tf-pre-test.sh"
    if [[ -f "$prescript" ]]; then
      log_info "--- \\e[32mpre-test\\e[0m hook (\\e[33;1m${prescript}\\e[0m) found: execute"
      exec_hook "$prescript"
    else
      log_info "--- \\e[32mpre-test\\e[0m hook (\\e[33;1m${prescript}\\e[0m) not found: skip"
    fi

    mkdir -p "$TF_PROJECT_DIR"/reports/
    tf_pre_init
    terraform init -backend=false

    # shellcheck disable=SC2046
    terraform test -junit-xml="reports/${ENV_TYPE}-tftest.xunit.xml" $(echo "$opts" | envsubst_cli)

    # maybe execute post test script
    postscript="$TF_SCRIPTS_DIR/tf-post-test.sh"
    if [[ -f "$postscript" ]]; then
      log_info "--- \\e[32mpost-test\\e[0m hook (\\e[33;1m${postscript}\\e[0m) found: execute"
      exec_hook "$postscript"
    else
      log_info "--- \\e[32mpost-test\\e[0m hook (\\e[33;1m${postscript}\\e[0m) not found: skip"
    fi
  }

  tf_plan() {
    opts=${ENV_PLAN_OPTS:-$TF_PLAN_OPTS}
    extra_opts=${ENV_EXTRA_OPTS:-$TF_EXTRA_OPTS}
@@ -981,6 +1032,7 @@ stages:
  default:
    init: tf_init
    select_workspace: tf_workspace_select
    test: tf_test
    plan: tf_plan
    apply: tf_apply
    destroy: tf_destroy
@@ -988,6 +1040,8 @@ stages:
    - !reference [ .tf-commands, default, init ]
  select_workspace:
    - !reference [ .tf-commands, default, select_workspace ]
  test:
    - !reference [ .tf-commands, default, test ]
  plan:
    - !reference [ .tf-commands, default, plan ]
  apply:
@@ -995,6 +1049,7 @@ stages:
  destroy:
    - !reference [ .tf-commands, default, destroy ]


# =============================================================================
# === Job Prototypes
# =============================================================================
@@ -1064,6 +1119,19 @@ stages:
    reports:
      terraform: $TF_PROJECT_DIR/${ENV_TYPE}-plan.json

.tf-test:
  extends: .tf-base
  stage: test
  script:
    - !reference [ .tf-commands, test ]
  artifacts:
    name: "$CI_JOB_NAME artifacts from $CI_PROJECT_NAME on $CI_COMMIT_REF_SLUG"
    expire_in: 1 day
    access: developer
    reports:
      junit: $TF_PROJECT_DIR/reports/${ENV_TYPE}-tftest.xunit.xml


# Destroy job prototype
# @arg ENV_DESTROY_OPTS: environment tf destroy arguments
.tf-destroy:
@@ -1339,6 +1407,30 @@ tf-plan-review:
    # if $TF_REVIEW_ENABLED: auto
    - if: '$TF_REVIEW_ENABLED == "true"'

# terraform test job for review
tf-test-review:
  variables:
    ENV_TYPE: review
    ENV_TFTEST_OPTS: $TF_REVIEW_TFTEST_OPTS
  extends: .tf-test
  resource_group: tf-review/$CI_COMMIT_REF_NAME
  rules:
    # skip if tftest not enabled
    - if: '$TF_TFTEST_STRATEGY != "single" && $TF_TFTEST_STRATEGY != "cascading"'
      when: never
    # skip tags
    - if: $CI_COMMIT_TAG
      when: never
    # skip if review env is not enabled
    - if: '$TF_REVIEW_ENABLED != "true"'
      when: never
    # skip integration and production branches
    - if: '$CI_COMMIT_REF_NAME =~ $PROD_REF || $CI_COMMIT_REF_NAME =~ $INTEG_REF'
      when: never
    # apply global test policy (adaptive pipeline)
    - !reference [.test-policy, rules]


# create review env (only on feature branches)
tf-review:
  extends: .tf-create
@@ -1418,6 +1510,32 @@ tf-plan-integration:
    # if $TF_INTEG_ENABLED: auto
    - if: '$TF_INTEG_ENABLED == "true"'

# terraform test job for integration
tf-test-integration:
  variables:
    ENV_TYPE: integ
    ENV_TFTEST_OPTS: $TF_INTEG_TFTEST_OPTS
  extends: .tf-test
  resource_group:  tf-integration
  rules:
    # skip if tftest not enabled
    - if: '$TF_TFTEST_STRATEGY != "single" && $TF_TFTEST_STRATEGY != "cascading"'
      when: never
    # skip tags
    - if: $CI_COMMIT_TAG
      when: never
    # skip if integration env is not enabled
    - if: '$TF_INTEG_ENABLED != "true"'
      when: never
    # skip on production branch
    - if: '$CI_COMMIT_REF_NAME =~ $PROD_REF'
      when: never
    # skip on non integration branch if 'single' strategy
    - if: '$TF_TFTEST_STRATEGY == "single" && $CI_COMMIT_REF_NAME !~ $INTEG_REF'
      when: never
    # apply global test policy (adaptive pipeline)
    - !reference [.test-policy, rules]

# create integration env (only on develop branch)
tf-integration:
  extends: .tf-create
@@ -1491,6 +1609,30 @@ tf-plan-staging:
    # if $TF_STAGING_ENABLED: auto
    - if: '$TF_STAGING_ENABLED == "true"'

# terraform test job for staging
tf-test-staging:
  variables:
    ENV_TYPE: staging
    ENV_TFTEST_OPTS: $TF_STAGING_TFTEST_OPTS
  extends: .tf-test
  resource_group: tf-staging
  rules:
    # skip if tftest not enabled
    - if: '$TF_TFTEST_STRATEGY != "single" && $TF_TFTEST_STRATEGY != "cascading"'
      when: never
    # skip tags
    - if: $CI_COMMIT_TAG
      when: never
    # skip if staging env is not enabled
    - if: '$TF_STAGING_ENABLED != "true"'
      when: never
    # skip on non production branch if 'single' strategy
    - if: '$TF_TFTEST_STRATEGY == "single" && $CI_COMMIT_REF_NAME !~ $PROD_REF'
      when: never
    # apply global test policy (adaptive pipeline)
    - !reference [.test-policy, rules]


# create staging env (only on master branch)
tf-staging:
  extends: .tf-create
@@ -1574,6 +1716,32 @@ tf-plan-production:
    # enabled on production branches
    - if: '$CI_COMMIT_REF_NAME =~ $PROD_REF'


# terraform test job for production
tf-test-production:
  variables:
    ENV_TYPE: production
    ENV_TFTEST_OPTS: $TF_PROD_TFTEST_OPTS
  extends: .tf-test
  resource_group: tf-production
  rules:
    # skip if tftest not enabled
    - if: '$TF_TFTEST_STRATEGY != "single" && $TF_TFTEST_STRATEGY != "cascading"'
      when: never
    # skip tags
    - if: $CI_COMMIT_TAG
      when: never
    # skip if production env is not enabled
    - if: '$TF_PROD_ENABLED != "true"'
      when: never
    # skip on non production branch if 'single' strategy
    - if: '$TF_TFTEST_STRATEGY == "single" && $CI_COMMIT_REF_NAME !~ $PROD_REF'
      when: never
    # apply global test policy (adaptive pipeline)
    - !reference [.test-policy, rules]



# create production env if on branch master and variable TF_PROD_ENABLED defined
tf-production:
  extends: .tf-create