diff --git a/.github/workflows/checkov.yaml b/.github/workflows/checkov.yaml deleted file mode 100644 index ccec113..0000000 --- a/.github/workflows/checkov.yaml +++ /dev/null @@ -1,34 +0,0 @@ -name: "Checkov GitHub Action" -permissions: read-all - -on: - pull_request: - branches: [test, dev, qa, prod, main] - -jobs: - checkov: - name: checkov - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v3 - with: - submodules: "recursive" - token: ${{ secrets.GITHUB_TOKEN }} - - name: Create empty baseline (if needed) - run: | - if [ -f .checkov.baseline ]; then - echo "⏩⏩⏩ Baseline file exists - do nothing." - else - echo "🆕🆕🆕 Baseline file does not exist - creating empty baseline file." - echo "{}" >> .checkov.baseline - fi - - name: Output baseline contents to console - run: | - echo "Checkov baseline file (.checkov.baseline) contents:" - cat .checkov.baseline - - name: Run Checkov - id: checkov - uses: bridgecrewio/checkov-action@master - with: - config_file: ".checkov.yaml" diff --git a/.github/workflows/semantic-pr.yaml b/.github/workflows/semantic-pr.yaml index 3627556..41441c9 100644 --- a/.github/workflows/semantic-pr.yaml +++ b/.github/workflows/semantic-pr.yaml @@ -4,7 +4,7 @@ # Use this workflow for public repos, since public repos cannot access our internal # workflows repo. --- -name: public-semantic-pr +name: repository-semantic-pr permissions: contents: write pull-requests: write @@ -18,8 +18,8 @@ on: - synchronize jobs: - public-semantic-pr: - name: public-semantic-pr + repository-semantic-pr: + name: repository-semantic-pr runs-on: ubuntu-latest steps: - uses: amannn/action-semantic-pull-request@v4 diff --git a/.github/workflows/terraform.yaml b/.github/workflows/terraform.yaml deleted file mode 100644 index cbdc29f..0000000 --- a/.github/workflows/terraform.yaml +++ /dev/null @@ -1,33 +0,0 @@ -name: "Terraform GitHub Action" -on: - pull_request: - # This workflow is meant for public Terraform module repositories - # which are generally component modules that follow trunk-based development. - branches: [main] -jobs: - terraform: - name: "terraform" - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - submodules: "recursive" - - name: Set up Terraform - uses: hashicorp/setup-terraform@v3 - with: - cli_config_credentials_token: ${{ secrets.TF_API_TOKEN }} - - name: Terraform Format - id: fmt - run: terraform fmt - continue-on-error: true - - name: Terraform Init - id: init - run: terraform init - - name: Terraform Validate - id: validate - run: terraform validate -no-color - - name: Terraform Plan - id: plan - run: terraform plan -no-color - continue-on-error: true diff --git a/.github/workflows/terratest.yaml b/.github/workflows/terratest.yaml deleted file mode 100644 index 05041b1..0000000 --- a/.github/workflows/terratest.yaml +++ /dev/null @@ -1,30 +0,0 @@ -name: "Terratest GitHub Action" -on: - pull_request: - branches: [test, dev, qa, prod, main] - push: - branches: [test, dev, qa, prod, main] -env: - AWS_ACCESS_KEY_ID: ${{ secrets.TERRATEST_AWS_ACCESS_KEY_ID }} - AWS_SECRET_KEY: ${{ secrets.TERRATEST_AWS_SECRET_ACCESS_KEY }} - AWS_DEFAULT_REGION: ${{ secrets.TERRATEST_AWS_REGION }} - AWS_REGION: ${{ secrets.TERRATEST_AWS_REGION }} -jobs: - terratest: - name: terratest - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - submodules: true - - name: Set up Go - uses: actions/setup-go@v5 - with: - go-version: 1.20 - id: go - - name: Run 'go test -v -timeout 60m' - run: | - cd test - go mod download - go test -v -timeout 30m diff --git a/.github/workflows/terratest.yml b/.github/workflows/terratest.yml index 718e733..da2c092 100644 --- a/.github/workflows/terratest.yml +++ b/.github/workflows/terratest.yml @@ -26,10 +26,10 @@ jobs: uses: actions/checkout@v2 with: submodules: true - - name: Set up Go (1.17) + - name: Set up Go (1.22) uses: actions/setup-go@v2 with: - go-version: 1.17 + go-version: 1.22 id: go - name: Login to Google Cloud uses: google-github-actions/auth@v0 @@ -40,7 +40,7 @@ jobs: run: gcloud config set project $GOOGLE_PROJECT - name: Run terratest run: | - make test_and_cover + make tests - name: Release uses: cycjimmy/semantic-release-action@v3 env: diff --git a/examples/bucket/main.tf b/examples/bucket/main.tf index b54ec0f..7ea8722 100644 --- a/examples/bucket/main.tf +++ b/examples/bucket/main.tf @@ -24,3 +24,18 @@ module "test_bucket" { force_destroy = true } + +module "test_bucket_with_retention" { + source = "../../modules/gcp_gcs_bucket" + + location = "asia-southeast2" + name = "test-bucket-with-retention" + project_id = "storage-0994" + + object_versioning_enabled = false + retention_lock_enabled = true + + soft_delete_retention_duration_seconds = 0 + + force_destroy = true +} diff --git a/examples/bucket/outputs.tf b/examples/bucket/outputs.tf index 1b4992c..37e0258 100644 --- a/examples/bucket/outputs.tf +++ b/examples/bucket/outputs.tf @@ -5,3 +5,11 @@ output "test_bucket_id" { output "test_bucket_name" { value = module.test_bucket.name } + +output "test_bucket_with_retention_id" { + value = module.test_bucket_with_retention.id +} + +output "test_bucket_with_retention_name" { + value = module.test_bucket_with_retention.name +} diff --git a/modules/gcp_gcs_bucket/README.md b/modules/gcp_gcs_bucket/README.md index 7c3f969..70b0947 100644 --- a/modules/gcp_gcs_bucket/README.md +++ b/modules/gcp_gcs_bucket/README.md @@ -7,17 +7,17 @@ This module will create bucket in GCP with enable server-side encryption and log | Name | Version | |------|---------| -| [google](#requirement\_google) | >= 4.51.0 | -| [random](#requirement\_random) | >= 3.1.2 | -| [time](#requirement\_time) | >= 0.7.2 | +| [google](#requirement\_google) | ~> 5.22 | +| [random](#requirement\_random) | >= 3.0, < 4.0 | +| [time](#requirement\_time) | >= 0.11, < 1.0 | ## Providers | Name | Version | |------|---------| -| [google](#provider\_google) | 4.65.2 | -| [random](#provider\_random) | 3.5.1 | -| [time](#provider\_time) | 0.9.1 | +| [google](#provider\_google) | ~> 5.22 | +| [random](#provider\_random) | >= 3.0, < 4.0 | +| [time](#provider\_time) | >= 0.11, < 1.0 | ## Modules @@ -42,9 +42,15 @@ No modules. |------|-------------|------|---------|:--------:| | [default\_event\_based\_hold](#input\_default\_event\_based\_hold) | (Optional) Whether or not to automatically apply an eventBasedHold to new objects added to the bucket. | `bool` | `false` | no | | [force\_destroy](#input\_force\_destroy) | When deleting a bucket, this boolean option will delete all contained objects. If you try to delete a bucket that contains objects, Terraform will fail that run. | `bool` | `false` | no | +| [lifecycle\_rules](#input\_lifecycle\_rules) | List of lifecycle rules to configure. Format is the same as described in provider documentation https://www.terraform.io/docs/providers/google/r/storage_bucket.html#lifecycle_rule except condition.matches\_storage\_class should be a comma delimited string. |
list(object({| `[]` | no | | [location](#input\_location) | (Required) The GCS location | `string` | n/a | yes | | [name](#input\_name) | (Required) The name of the bucket. | `string` | n/a | yes | +| [object\_versioning\_enabled](#input\_object\_versioning\_enabled) | If set to true, the bucket will be versioned. | `bool` | `true` | no | | [project\_id](#input\_project\_id) | (Required) Id of the project in which the bucket is created | `string` | n/a | yes | +| [retention\_lock\_bucket\_enabled](#input\_retention\_lock\_bucket\_enabled) | Bucket will be locked and cannot be deleted from retention policy | `bool` | `false` | no | +| [retention\_lock\_duration\_seconds](#input\_retention\_lock\_duration\_seconds) | The duration in seconds that objects in the bucket must be retained and cannot be deleted or replaced. The value must be in between 0 and 3155695200 (100 years). | `number` | `86400` | no | +| [retention\_lock\_enabled](#input\_retention\_lock\_enabled) | If set to true, the bucket will be locked and objects in the bucket will be protected from deletion. Note that retention\_policy cannot be used with object versioning. They are mutually exclusive. | `bool` | `false` | no | +| [soft\_delete\_retention\_duration\_seconds](#input\_soft\_delete\_retention\_duration\_seconds) | The duration in seconds that soft-deleted objects in the bucket will be retained and cannot be permanently deleted. Default value is 2678400 (30 days). The value must be in between 604800(7 days) and 7776000(90 days). Note: To disable the soft delete policy on a bucket, This field must be set to 0. | `number` | `2678400` | no | | [storage\_class](#input\_storage\_class) | (Optional, Default: 'STANDARD') The Storage Class of the new bucket. Supported values include: STANDARD, MULTI\_REGIONAL, REGIONAL, NEARLINE, COLDLINE, ARCHIVE. | `string` | `"STANDARD"` | no | ## Outputs diff --git a/modules/gcp_gcs_bucket/inputs.tf b/modules/gcp_gcs_bucket/inputs.tf index e53de63..3c03ed5 100644 --- a/modules/gcp_gcs_bucket/inputs.tf +++ b/modules/gcp_gcs_bucket/inputs.tf @@ -56,6 +56,35 @@ variable "lifecycle_rules" { default = [] } +variable "object_versioning_enabled" { + type = bool + description = "If set to true, the bucket will be versioned." + default = true +} + +variable "retention_lock_bucket_enabled" { + type = bool + description = "Bucket will be locked and cannot be deleted from retention policy" + default = false +} + +variable "retention_lock_enabled" { + type = bool + description = "If set to true, the bucket will be locked and objects in the bucket will be protected from deletion. Note that retention_policy cannot be used with object versioning. They are mutually exclusive." + default = false +} + +variable "retention_lock_duration_seconds" { + type = number + description = "The duration in seconds that objects in the bucket must be retained and cannot be deleted or replaced. The value must be in between 0 and 3155695200 (100 years)." + default = 86400 # 1 day + + validation { + condition = var.retention_lock_duration_seconds >= 0 && var.retention_lock_duration_seconds <= 3155695200 + error_message = "The retention_lock_duration_seconds must be between 0 and 3155695200 (100 years)." + } +} + variable "soft_delete_retention_duration_seconds" { type = number description = "The duration in seconds that soft-deleted objects in the bucket will be retained and cannot be permanently deleted. Default value is 2678400 (30 days). The value must be in between 604800(7 days) and 7776000(90 days). Note: To disable the soft delete policy on a bucket, This field must be set to 0." diff --git a/modules/gcp_gcs_bucket/main.tf b/modules/gcp_gcs_bucket/main.tf index ac1e09f..e7195c0 100644 --- a/modules/gcp_gcs_bucket/main.tf +++ b/modules/gcp_gcs_bucket/main.tf @@ -74,6 +74,7 @@ resource "google_storage_bucket" "google_storage_bucket_logging" { } resource "google_storage_bucket" "google_storage_bucket" { + #checkov:skip=CKV_GCP_78: Bucket versioning should be enabled by default however skipping the Checkov rule as it is not a requirement for all buckets with retention policy enabled. name = "${var.name}_${random_id.random_id.hex}" location = var.location force_destroy = var.force_destroy @@ -87,7 +88,7 @@ resource "google_storage_bucket" "google_storage_bucket" { public_access_prevention = "enforced" versioning { - enabled = true + enabled = var.object_versioning_enabled } logging { @@ -121,6 +122,14 @@ resource "google_storage_bucket" "google_storage_bucket" { } } + dynamic "retention_policy" { + for_each = var.retention_lock_enabled ? [1] : [] + content { + is_locked = var.retention_lock_bucket_enabled + retention_period = var.retention_lock_duration_seconds + } + } + soft_delete_policy { retention_duration_seconds = var.soft_delete_retention_duration_seconds } diff --git a/test/gcp_gcs_bucket_test.go b/test/gcp_gcs_bucket_test.go index b12e97d..6674a72 100644 --- a/test/gcp_gcs_bucket_test.go +++ b/test/gcp_gcs_bucket_test.go @@ -40,6 +40,12 @@ func TestGCSBucketCreation(t *testing.T) { name := terraform.Output(t, options, "test_bucket_name") assert.NotEmpty(t, name) + id_retention := terraform.Output(t, options, "test_bucket_with_retention_id") + assert.NotEmpty(t, id_retention) + + name_retention := terraform.Output(t, options, "test_bucket_with_retention_name") + assert.NotEmpty(t, name_retention) + ctx := context.Background() client, err := storage.NewClient(ctx, option.WithCredentialsJSON([]byte(credentials))) if err != nil {
# Object with keys:
# - type - The type of the action of this Lifecycle Rule. Supported values: Delete and SetStorageClass.
# - storage_class - (Required if action type is SetStorageClass) The target Storage Class of objects affected by this Lifecycle Rule.
action = map(string)
# Object with keys:
# - age - (Optional) Minimum age of an object in days to satisfy this condition.
# - created_before - (Optional) Creation date of an object in RFC 3339 (e.g. 2017-06-13) to satisfy this condition.
# - with_state - (Optional) Match to live and/or archived objects. Supported values include: "LIVE", "ARCHIVED", "ANY".
# - matches_storage_class - (Optional) Comma delimited string for storage class of objects to satisfy this condition. Supported values include: MULTI_REGIONAL, REGIONAL, NEARLINE, COLDLINE, STANDARD, DURABLE_REDUCED_AVAILABILITY.
# - matches_prefix - (Optional) One or more matching name prefixes to satisfy this condition.
# - matches_suffix - (Optional) One or more matching name suffixes to satisfy this condition.
# - num_newer_versions - (Optional) Relevant only for versioned objects. The number of newer versions of an object to satisfy this condition.
# - custom_time_before - (Optional) A date in the RFC 3339 format YYYY-MM-DD. This condition is satisfied when the customTime metadata for the object is set to an earlier date than the date used in this lifecycle condition.
# - days_since_custom_time - (Optional) The number of days from the Custom-Time metadata attribute after which this condition becomes true.
# - days_since_noncurrent_time - (Optional) Relevant only for versioned objects. Number of days elapsed since the noncurrent timestamp of an object.
# - noncurrent_time_before - (Optional) Relevant only for versioned objects. The date in RFC 3339 (e.g. 2017-06-13) when the object became nonconcurrent.
condition = map(string)
}))