diff --git a/.github/workflows/on-pull-request.yaml b/.github/workflows/on-pull-request.yaml index b33b6ed..1856954 100644 --- a/.github/workflows/on-pull-request.yaml +++ b/.github/workflows/on-pull-request.yaml @@ -2,20 +2,20 @@ name: "Pull Request Jobs" on: pull_request: - + workflow_dispatch: env: - tf_version: 1.7.1 + tf_version: 1.9.8 working_dir: . jobs: terraform-doc-generation: permissions: - contents: 'write' - id-token: 'write' - pull-requests: 'write' - issues: 'write' - name: "Terraform Documentation Generation" + contents: "write" + id-token: "write" + pull-requests: "write" + issues: "write" + name: "Terraform doc generation and tests" runs-on: ubuntu-latest defaults: run: @@ -37,7 +37,7 @@ jobs: run: terraform fmt -check - name: Generate TF docs - uses: terraform-docs/gh-actions@v1.0.0 + uses: terraform-docs/gh-actions@v1.3.0 with: find-dir: modules/ recursive: true @@ -45,3 +45,15 @@ jobs: git-push: true output-method: replace template: "{{ .Content }}" + + # recurse every directory in the ./modules directory + # and run the terraform test command + - name: Run Terraform Unit Tests + run: | + for dir in $(find ${{env.working_dir}}/modules -type d -not -path '*/\.terraform/*'); do + echo "Running tests in $dir" + cd $dir + terraform init + terraform test + cd - + done diff --git a/modules/enterprise-organization/organization.tftest.hcl b/modules/enterprise-organization/organization.tftest.hcl new file mode 100644 index 0000000..d6897ab --- /dev/null +++ b/modules/enterprise-organization/organization.tftest.hcl @@ -0,0 +1,44 @@ +mock_provider "github" {} + +variables { + # required variables + enterprise_id = "1234567890" + name = "github-foundations" + display_name = "GitHub Foundations" + description = "GitHub Foundations Organization" + billing_email = "billingemail@focisolutions.com" + admin_logins = ["admin1", "admin2"] +} + +run "organization_test" { + command = apply + + assert { + condition = github_enterprise_organization.organization.id != null + error_message = "The organization was not created." + } + assert { + condition = github_enterprise_organization.organization.enterprise_id == var.enterprise_id + error_message = "The organization id is incorrect. Expected ${var.enterprise_id} but got ${github_enterprise_organization.organization.enterprise_id}." + } + assert { + condition = github_enterprise_organization.organization.name == var.name + error_message = "The organization name is incorrect. Expected ${var.name} but got ${github_enterprise_organization.organization.name}." + } + assert { + condition = github_enterprise_organization.organization.display_name == var.display_name + error_message = "The organization display name is incorrect. Expected ${var.display_name} but got ${github_enterprise_organization.organization.display_name}." + } + assert { + condition = github_enterprise_organization.organization.description == var.description + error_message = "The organization description is incorrect. Expected ${var.description} but got ${github_enterprise_organization.organization.description}." + } + assert { + condition = github_enterprise_organization.organization.billing_email == var.billing_email + error_message = "The organization billing email is incorrect. Expected ${var.billing_email} but got ${github_enterprise_organization.organization.billing_email}." + } + assert { + condition = length(github_enterprise_organization.organization.admin_logins) == length(var.admin_logins) + error_message = "The organization admin logins are incorrect. Expected ${length(var.admin_logins)} but got ${length(github_enterprise_organization.organization.admin_logins)}." + } +} diff --git a/modules/github-aws-oidc/oidc.tftest.hcl b/modules/github-aws-oidc/oidc.tftest.hcl new file mode 100644 index 0000000..f920314 --- /dev/null +++ b/modules/github-aws-oidc/oidc.tftest.hcl @@ -0,0 +1,97 @@ +mock_provider "github" {} +mock_provider "aws" {} + +variables { + # required variables + github_foundations_organization_name = "github-foundations-org" + github_thumbprints = ["990F4193972F2BECF12DDEDA5237F9C952F20D9E"] +} + +run "oidc_provider_entry_test" { + command = apply + + assert { + condition = aws_iam_openid_connect_provider.oidc_provider_entry.url == "https://token.actions.githubusercontent.com" + error_message = "The url of the openid connect provider entry is incorrect. Expected 'https://token.actions.githubusercontent.com' but got '${aws_iam_openid_connect_provider.oidc_provider_entry.url}'." + } + assert { + condition = aws_iam_openid_connect_provider.oidc_provider_entry.client_id_list != null + error_message = "The client id list of the openid connect provider entry is incorrect. Expected a non-null value but got 'null'." + } + assert { + condition = aws_iam_openid_connect_provider.oidc_provider_entry.thumbprint_list[0] == "990F4193972F2BECF12DDEDA5237F9C952F20D9E" + error_message = "The thumbprint list of the openid connect provider entry is incorrect. Expected '990F4193972F2BECF12DDEDA5237F9C952F20D9E' but got '${aws_iam_openid_connect_provider.oidc_provider_entry.thumbprint_list[0]}'." + } +} + +run "organizations_role_test" { + + assert { + condition = aws_iam_role.organizations_role.name == "GhFoundationsOrganizationsAction" + error_message = "The name of the organizations role is incorrect. Expected 'GhFoundationsOrganizationsAction' but got '${aws_iam_role.organizations_role.name}'." + } + assert { + condition = jsondecode(aws_iam_role.organizations_role.assume_role_policy)["Statement"][0]["Effect"] == "Allow" + error_message = "The assume role policy of the organizations role is incorrect. Expected 'Allow' but got '${jsondecode(aws_iam_role.organizations_role.assume_role_policy)["Statement"][0]["Effect"]}'." + } + assert { + condition = jsondecode(aws_iam_role.organizations_role.assume_role_policy)["Statement"][0]["Action"] == "sts:AssumeRoleWithWebIdentity" + error_message = "The assume role policy action of the organizations role is incorrect. Expected 'sts:AssumeRoleWithWebIdentity' but got '${jsondecode(aws_iam_role.organizations_role.assume_role_policy)["Statement"][0]["Action"]}'." + } + assert { + condition = jsondecode(aws_iam_role.organizations_role.assume_role_policy)["Statement"][0]["Principal"]["Federated"] == aws_iam_openid_connect_provider.oidc_provider_entry.arn + error_message = "The assume role policy principal federated of the organizations role is incorrect. Expected '${aws_iam_openid_connect_provider.oidc_provider_entry.arn}' but got '${jsondecode(aws_iam_role.organizations_role.assume_role_policy)["Statement"][0]["Principal"]["Federated"]}'." + } + assert { + condition = jsondecode(aws_iam_role.organizations_role.assume_role_policy)["Statement"][0]["Condition"]["StringEquals"]["token.actions.githubusercontent.com:aud"][0] == "sts.amazonaws.com" + error_message = "The assume role policy condition string equals token actions githubusercontent com aud of the organizations role is incorrect. Expected 'sts.amazonaws.com' but got '${jsondecode(aws_iam_role.organizations_role.assume_role_policy)["Statement"][0]["Condition"]["StringEquals"]["token.actions.githubusercontent.com:aud"][0]}'." + } + assert { + condition = jsondecode(aws_iam_role.organizations_role.assume_role_policy)["Statement"][0]["Condition"]["StringLike"]["token.actions.githubusercontent.com:sub"][0] == "repo:github-foundations-org/organizations:*" + error_message = "The assume role policy condition string like token actions githubusercontent com sub of the organizations role is incorrect. Expected 'repo:github-foundations-org/organizations:*' but got '${jsondecode(aws_iam_role.organizations_role.assume_role_policy)["Statement"][0]["Condition"]["StringLike"]["token.actions.githubusercontent.com:sub"][0]}'." + } + assert { + condition = aws_iam_role.organizations_role.tags.Purpose == "Github Foundations" + error_message = "The tags of the organizations role are incorrect. Expected 'Github Foundations' but got '${aws_iam_role.organizations_role.tags.Purpose}'." + } +} + +run "organizations_role_policy_test" { + + assert { + condition = aws_iam_role_policy.organizations_role_policy.name == "organizations-tf-state-management-policy" + error_message = "The name of the organizations role policy is incorrect. Expected 'organizations-tf-state-management-policy' but got '${aws_iam_role_policy.organizations_role_policy.name}'." + } + assert { + condition = aws_iam_role_policy.organizations_role_policy.role == aws_iam_role.organizations_role.id + error_message = "The role of the organizations role policy is incorrect. Expected '${aws_iam_role.organizations_role.id}' but got '${aws_iam_role_policy.organizations_role_policy.role}'." + } + assert { + condition = jsondecode(aws_iam_role_policy.organizations_role_policy.policy)["Statement"][0]["Sid"] == "StateBucketFullAccess" + error_message = "The organizations role policy statement sid is incorrect. Expected 'StateBucketFullAccess' but got '${jsondecode(aws_iam_role_policy.organizations_role_policy.policy)["Statement"][0]["Sid"]}'." + } + assert { + condition = jsondecode(aws_iam_role_policy.organizations_role_policy.policy)["Statement"][0]["Action"][0] == "s3:PutObject" + error_message = "The organizations role policy statement action is incorrect. Expected 's3:PutObject' but got '${jsondecode(aws_iam_role_policy.organizations_role_policy.policy)["Statement"][0]["Action"][0]}'." + } + assert { + condition = jsondecode(aws_iam_role_policy.organizations_role_policy.policy)["Statement"][0]["Action"][1] == "s3:GetObject" + error_message = "The organizations role policy statement action is incorrect. Expected 's3:GetObject' but got '${jsondecode(aws_iam_role_policy.organizations_role_policy.policy)["Statement"][0]["Action"][1]}'." + } + assert { + condition = jsondecode(aws_iam_role_policy.organizations_role_policy.policy)["Statement"][0]["Action"][2] == "s3:ListBucket" + error_message = "The organizations role policy statement action is incorrect. Expected 's3:ListBucket' but got '${jsondecode(aws_iam_role_policy.organizations_role_policy.policy)["Statement"][0]["Action"][2]}'." + } + assert { + condition = jsondecode(aws_iam_role_policy.organizations_role_policy.policy)["Statement"][0]["Effect"] == "Allow" + error_message = "The organizations role policy statement effect is incorrect. Expected 'Allow' but got '${jsondecode(aws_iam_role_policy.organizations_role_policy.policy)["Statement"][0]["Effect"]}'." + } + assert { + condition = jsondecode(aws_iam_role_policy.organizations_role_policy.policy)["Statement"][0]["Resource"][0] == aws_s3_bucket.state_bucket.arn + error_message = "The organizations role policy statement resource is incorrect. Expected '${aws_s3_bucket.state_bucket.arn}' but got '${jsondecode(aws_iam_role_policy.organizations_role_policy.policy)["Statement"][0]["Resource"][0]}'." + } + assert { + condition = jsondecode(aws_iam_role_policy.organizations_role_policy.policy)["Statement"][0]["Resource"][1] == "${aws_s3_bucket.state_bucket.arn}/*" + error_message = "The organizations role policy statement resource is incorrect. Expected '${aws_s3_bucket.state_bucket.arn}/*' but got '${jsondecode(aws_iam_role_policy.organizations_role_policy.policy)["Statement"][0]["Resource"][1]}'." + } +} diff --git a/modules/github-aws-oidc/outputs.tftest.hcl b/modules/github-aws-oidc/outputs.tftest.hcl new file mode 100644 index 0000000..4e92813 --- /dev/null +++ b/modules/github-aws-oidc/outputs.tftest.hcl @@ -0,0 +1,45 @@ +mock_provider "github" {} +mock_provider "aws" {} + +override_resource { + target = aws_s3_bucket.state_bucket + values = { + region = "us-west-2" + } +} +override_resource { + target = aws_iam_role.organizations_role + values = { + arn = "arn:aws:iam::123456789012:role/GhFoundationsOrganizationsAction" + } +} + +variables { + # required variables + github_foundations_organization_name = "github-foundations-org" + github_thumbprints = ["990F4193972F2BECF12DDEDA5237F9C952F20D9E"] + + # Variables for this test + bucket_name = "GithubFoundationStateBuckettyBucket" +} + +run "create_test" { + command = apply + + assert { + condition = output.s3_bucket_name == var.bucket_name + error_message = "The name of the state bucket is incorrect. Expected '${var.bucket_name}' but got '${output.s3_bucket_name}'." + } + assert { + condition = output.s3_bucket_region == "us-west-2" + error_message = "The region of the state bucket is incorrect. Expected 'us-west-2' but got '${output.s3_bucket_region}'." + } + assert { + condition = output.dynamodb_table_name == "TFLockIds" + error_message = "The name of the dynamodb table is incorrect. Expected 'TFLockIds' but got '${output.dynamodb_table_name}'." + } + assert { + condition = output.organizations_runner_role == "arn:aws:iam::123456789012:role/GhFoundationsOrganizationsAction" + error_message = "The ARN of the role is incorrect. Expected 'arn:aws:iam::123456789012:role/GhFoundationsOrganizationsAction' but got '${output.organizations_runner_role}'." + } +} diff --git a/modules/github-aws-oidc/resource_group.tftest.hcl b/modules/github-aws-oidc/resource_group.tftest.hcl new file mode 100644 index 0000000..e113aab --- /dev/null +++ b/modules/github-aws-oidc/resource_group.tftest.hcl @@ -0,0 +1,34 @@ +mock_provider "github" {} +mock_provider "aws" {} + +variables { + # required variables + github_foundations_organization_name = "github-foundations-org" + github_thumbprints = ["990F4193972F2BECF12DDEDA5237F9C952F20D9E"] +} + +run "github_foundations_rg_test" { + command = apply + + assert { + condition = aws_resourcegroups_group.github_foundations_rg.name == "GithubFoundationResources" + error_message = "The name of the resource group is incorrect. Expected 'GithubFoundationResources' but got '${aws_resourcegroups_group.github_foundations_rg.name}'." + } + assert { + condition = aws_resourcegroups_group.github_foundations_rg.resource_query[0].query == "{\"ResourceTypeFilters\":[\"AWS::AllSupported\"],\"TagFilters\":[{\"Key\":\"Purpose\",\"Values\":[\"Github Foundations\"]}]}" + error_message = "The resource query of the resource group is incorrect. Expected '{\"ResourceTypeFilters\":[\"AWS::AllSupported\"],\"TagFilters\":[{\"Key\":\"Purpose\",\"Values\":[\"Github Foundations\"]}]}' but got '${aws_resourcegroups_group.github_foundations_rg.resource_query[0].query}'." + } +} + +run "github_foundations_rg_test_rg_name" { + variables { + rg_name = "ghf-set-by-test-rg" + } + + command = apply + + assert { + condition = aws_resourcegroups_group.github_foundations_rg.name == "ghf-set-by-test-rg" + error_message = "The name of the resource group is incorrect. Expected 'ghf-set-by-test-rg' but got '${aws_resourcegroups_group.github_foundations_rg.name}'." + } +} diff --git a/modules/github-aws-oidc/storage.tftest.hcl b/modules/github-aws-oidc/storage.tftest.hcl new file mode 100644 index 0000000..24ae94b --- /dev/null +++ b/modules/github-aws-oidc/storage.tftest.hcl @@ -0,0 +1,122 @@ +mock_provider "github" {} +mock_provider "aws" {} + +variables { + # required variables + github_foundations_organization_name = "github-foundations-org" + + # Variables for this test + github_thumbprints = ["990F4193972F2BECF12DDEDA5237F9C952F20D9E", "990F4193972F2BECF12DDEDA5237F9C952F20D9F"] +} + +run "encryption_key_test" { + command = apply + + assert { + condition = aws_kms_key.encryption_key.description == "This key is used to encrypt state bucket objects" + error_message = "The description of the encryption key is incorrect. Expected 'This key is used to encrypt state bucket objects' but got '${aws_kms_key.encryption_key.description}'." + } + assert { + condition = aws_kms_key.encryption_key.deletion_window_in_days == 10 + error_message = "The deletion window in days of the encryption key is incorrect. Expected 10 but got ${aws_kms_key.encryption_key.deletion_window_in_days}." + } + assert { + condition = aws_kms_key.encryption_key.enable_key_rotation == true + error_message = "The enable key rotation of the encryption key is incorrect. Expected true but got ${aws_kms_key.encryption_key.enable_key_rotation}." + } + assert { + condition = aws_kms_key.encryption_key.tags.Purpose == "Github Foundations" + error_message = "The tags of the encryption key are incorrect. Expected 'Github Foundations' but got '${aws_kms_key.encryption_key.tags.Purpose}'." + } +} + +run "state_bucket_test" { + assert { + condition = aws_s3_bucket.state_bucket.bucket == "GithubFoundationState" + error_message = "The name of the state bucket is incorrect. Expected 'GithubFoundationState' but got '${aws_s3_bucket.state_bucket.bucket}'." + } + assert { + condition = aws_s3_bucket.state_bucket.tags.Purpose == "Github Foundations" + error_message = "The tags of the state bucket are incorrect. Expected 'Github Foundations' but got '${aws_s3_bucket.state_bucket.tags.Purpose}'." + } +} + +run "state_bucket_versioning_test" { + assert { + condition = aws_s3_bucket_versioning.state_bucket_versioning.bucket == aws_s3_bucket.state_bucket.id + error_message = "The state bucket versioning configuration is incorrect. Expected '${aws_s3_bucket.state_bucket.id}' but got '${aws_s3_bucket_versioning.state_bucket_versioning.bucket}'." + } + assert { + condition = aws_s3_bucket_versioning.state_bucket_versioning.versioning_configuration[0].status == "Enabled" + error_message = "The state bucket versioning status is incorrect. Expected 'Enabled' but got '${aws_s3_bucket_versioning.state_bucket_versioning.versioning_configuration[0].status}'." + } +} + +run "state_bucket_encryption_test" { + assert { + condition = aws_s3_bucket_server_side_encryption_configuration.state_bucket_encryption.bucket == aws_s3_bucket.state_bucket.id + error_message = "The state bucket encryption configuration is incorrect. Expected '${aws_s3_bucket.state_bucket.id}' but got '${aws_s3_bucket_server_side_encryption_configuration.state_bucket_encryption.bucket}'." + } + assert { + condition = aws_s3_bucket_server_side_encryption_configuration.state_bucket_encryption.rule != null + error_message = "The state bucket encryption rule is incorrect. Expected 1 but got null." + } +} + +run "state_bucket_access_test" { + assert { + condition = aws_s3_bucket_public_access_block.state_bucket_access.bucket == aws_s3_bucket.state_bucket.id + error_message = "The state bucket access configuration is incorrect. Expected '${aws_s3_bucket.state_bucket.id}' but got '${aws_s3_bucket_public_access_block.state_bucket_access.bucket}'." + } + assert { + condition = aws_s3_bucket_public_access_block.state_bucket_access.block_public_acls == true + error_message = "The state bucket access block public acls is incorrect. Expected true but got '${aws_s3_bucket_public_access_block.state_bucket_access.block_public_acls}'." + } + assert { + condition = aws_s3_bucket_public_access_block.state_bucket_access.block_public_policy == true + error_message = "The state bucket access block public policy is incorrect. Expected true but got '${aws_s3_bucket_public_access_block.state_bucket_access.block_public_policy}'." + } + assert { + condition = aws_s3_bucket_public_access_block.state_bucket_access.ignore_public_acls == true + error_message = "The state bucket access ignore public acls is incorrect. Expected true but got '${aws_s3_bucket_public_access_block.state_bucket_access.ignore_public_acls}'." + } + assert { + condition = aws_s3_bucket_public_access_block.state_bucket_access.restrict_public_buckets == true + error_message = "The state bucket access restrict public buckets is incorrect. Expected true but got '${aws_s3_bucket_public_access_block.state_bucket_access.restrict_public_buckets}'." + } +} + +run "state_lock_table_test" { + assert { + condition = aws_dynamodb_table.state_lock_table.name == "TFLockIds" + error_message = "The name of the state lock table is incorrect. Expected 'TFLockIds' but got '${aws_dynamodb_table.state_lock_table.name}'." + } + assert { + condition = aws_dynamodb_table.state_lock_table.read_capacity == 20 + error_message = "The read capacity of the state lock table is incorrect. Expected 20 but got '${aws_dynamodb_table.state_lock_table.read_capacity}'." + } + assert { + condition = aws_dynamodb_table.state_lock_table.write_capacity == 20 + error_message = "The write capacity of the state lock table is incorrect. Expected 20 but got '${aws_dynamodb_table.state_lock_table.write_capacity}'." + } + assert { + condition = aws_dynamodb_table.state_lock_table.billing_mode == "PROVISIONED" + error_message = "The billing mode of the state lock table is incorrect. Expected 'PROVISIONED' but got '${aws_dynamodb_table.state_lock_table.billing_mode}'." + } + assert { + condition = aws_dynamodb_table.state_lock_table.hash_key == "LockID" + error_message = "The hash key of the state lock table is incorrect. Expected 'LockID' but got '${aws_dynamodb_table.state_lock_table.hash_key}'." + } + assert { + condition = aws_dynamodb_table.state_lock_table.point_in_time_recovery[0].enabled == true + error_message = "The point in time recovery of the state lock table is incorrect. Expected true but got '${aws_dynamodb_table.state_lock_table.point_in_time_recovery[0].enabled}'." + } + assert { + condition = aws_dynamodb_table.state_lock_table.attribute != null + error_message = "The number of attributes of the state lock table is incorrect. Expected 1 but got null." + } + assert { + condition = aws_dynamodb_table.state_lock_table.tags.Purpose == "Github Foundations" + error_message = "The tags of the state lock table are incorrect. Expected 'Github Foundations' but got '${aws_dynamodb_table.state_lock_table.tags.Purpose}'." + } +} diff --git a/modules/github-azure-oidc/README.md b/modules/github-azure-oidc/README.md index 12a3503..16ac08e 100644 --- a/modules/github-azure-oidc/README.md +++ b/modules/github-azure-oidc/README.md @@ -3,14 +3,14 @@ | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 1.6 | -| [azurerm](#requirement\_azurerm) | >=3.0.0 | +| [azurerm](#requirement\_azurerm) | >=4.7.0 | | [random](#requirement\_random) | >= 3.6 | ## Providers | Name | Version | |------|---------| -| [azurerm](#provider\_azurerm) | >=3.0.0 | +| [azurerm](#provider\_azurerm) | >=4.7.0 | ## Modules @@ -55,7 +55,7 @@ No modules. | [sa\_tier](#input\_sa\_tier) | The tier of the storage account for github foundations. Valid options are Standard and Premium. Defaults to Standard. | `string` | `"Standard"` | no | | [tf\_state\_container](#input\_tf\_state\_container) | The name of the container to store the terraform state file(s) in. | `string` | `"tfstate"` | no | | [tf\_state\_container\_anonymous\_access\_level](#input\_tf\_state\_container\_anonymous\_access\_level) | The anonymous access level of the container to store the terraform state file(s) in. | `string` | `"private"` | no | -| [tf\_state\_container\_default\_encryption\_scope](#input\_tf\_state\_container\_default\_encryption\_scope) | The default encryption scope of the container to store the terraform state file(s) in. |
object({
name = string
source = string
key_vault_key_id = optional(string)
})
|
{
"name": "",
"source": "",
"storage_account_id": ""
}
| no | +| [tf\_state\_container\_default\_encryption\_scope](#input\_tf\_state\_container\_default\_encryption\_scope) | The default encryption scope of the container to store the terraform state file(s) in. |
object({
name = string
source = string
key_vault_key_id = optional(string)
})
|
{
"name": "",
"source": "",
"storage_account_id": ""
}
| no | | [tf\_state\_container\_encryption\_scope\_override\_enabled](#input\_tf\_state\_container\_encryption\_scope\_override\_enabled) | Whether or not the encryption scope override is enabled for the container to store the terraform state file(s) in. Defaults to false | `bool` | `false` | no | ## Outputs diff --git a/modules/github-azure-oidc/oidc.tftest.hcl b/modules/github-azure-oidc/oidc.tftest.hcl new file mode 100644 index 0000000..aec7552 --- /dev/null +++ b/modules/github-azure-oidc/oidc.tftest.hcl @@ -0,0 +1,223 @@ +mock_provider "github" {} +mock_provider "azurerm" { + # We need to mock the azurerm provider to avoid creating real resources + override_resource { + target = azurerm_user_assigned_identity.organization_identity + values = { + id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/github-foundations/providers/Microsoft.ManagedIdentity/userAssignedIdentities/ghf-organizations-identity" + } + } + override_resource { + target = azurerm_user_assigned_identity.bootstrap_identity + values = { + id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/github-foundations/providers/Microsoft.ManagedIdentity/userAssignedIdentities/ghf-bootstrap-identity" + } + } + override_resource { + target = azurerm_storage_account.github_foundations_sa + values = { + name = "ghfoundations" + id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/github-foundations/providers/Microsoft.Storage/storageAccounts/ghfoundations" + } + } +} + +override_data { + target = data.azurerm_key_vault.key_vault[0] + values = { + id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/kvrg/providers/Microsoft.KeyVault/vaults/awesome-possum" + } +} + +# local overrides +override_resource { + target = azurerm_storage_container.github_foundations_tf_state_container[0] + values = { + resource_manager_id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/github-foundations/providers/Microsoft.Storage/storageAccounts/ghfoundations/blobServices/default/containers/ghf-state" + } +} + +variables { + # required variables + github_foundations_organization_name = "github-foundations" + rg_name = "github-foundations" + rg_location = "eastus" + sa_name = "ghfoundations" + drift_detection_branch_name = "drift-test-branch" + kv_resource_group = "kvrg" + + # variables for this test + tf_state_container = "ghf-state" + kv_name = "awesome-possum" + +} + +run "bootstrap_identity_test" { + command = apply + + assert { + condition = azurerm_user_assigned_identity.bootstrap_identity.location == var.rg_location + error_message = "The location of the bootstrap identity is incorrect. Expected: ${var.rg_location}, Got: ${azurerm_user_assigned_identity.bootstrap_identity.location}" + } + assert { + condition = azurerm_user_assigned_identity.bootstrap_identity.resource_group_name == var.rg_name + error_message = "The resource group name of the bootstrap identity is incorrect. Expected: ${var.rg_name}, Got: ${azurerm_user_assigned_identity.bootstrap_identity.resource_group_name}" + } + assert { + condition = azurerm_user_assigned_identity.bootstrap_identity.name == "${var.bootstrap_repo_name}-identity" + error_message = "The name of the bootstrap identity is incorrect. Expected: ${var.bootstrap_repo_name}-identity, Got: ${azurerm_user_assigned_identity.bootstrap_identity.name}" + } +} + +run "bootstrap_role_assignment" { + assert { + condition = azurerm_role_assignment.bootstrap_role_assignment["storage-account-ghfoundations-contributor"].scope == azurerm_storage_account.github_foundations_sa.id + error_message = "The scope of the bootstrap role assignment is incorrect. Expected: ${azurerm_storage_account.github_foundations_sa.id}, Got: ${azurerm_role_assignment.bootstrap_role_assignment["storage-account-ghfoundations-contributor"].scope}" + } + assert { + condition = azurerm_role_assignment.bootstrap_role_assignment["storage-account-ghfoundations-contributor"].role_definition_name == "Storage Account Contributor" + error_message = "The role definition name of the bootstrap role assignment is incorrect. Expected: Storage Account Contributor, Got: ${azurerm_role_assignment.bootstrap_role_assignment["storage-account-ghfoundations-contributor"].role_definition_name}" + } +} + +run "organization_identity_test" { + assert { + condition = azurerm_user_assigned_identity.organization_identity.location == var.rg_location + error_message = "The location of the organization identity is incorrect. Expected: ${var.rg_location}, Got: ${azurerm_user_assigned_identity.organization_identity.location}" + } + assert { + condition = azurerm_user_assigned_identity.organization_identity.resource_group_name == var.rg_name + error_message = "The resource group name of the organization identity is incorrect. Expected: ${var.rg_name}, Got: ${azurerm_user_assigned_identity.organization_identity.resource_group_name}" + } + assert { + condition = azurerm_user_assigned_identity.organization_identity.name == "${var.organizations_repo_name}-identity" + error_message = "The name of the organization identity is incorrect. Expected: ${var.organizations_repo_name}-identity, Got: ${azurerm_user_assigned_identity.organization_identity.name}" + } +} + +run "organization_role_assignment" { + assert { + condition = azurerm_role_assignment.organization_role_assignment["keyvault-${data.azurerm_key_vault.key_vault[0].name}-secret-read"].scope == data.azurerm_key_vault.key_vault[0].id + error_message = "The scope of the organization role assignment is incorrect. Expected: ${data.azurerm_key_vault.key_vault[0].id}, Got: ${azurerm_role_assignment.organization_role_assignment["keyvault-${data.azurerm_key_vault.key_vault[0].name}-secret-read"].scope}" + } + assert { + condition = azurerm_role_assignment.organization_role_assignment["keyvault-${data.azurerm_key_vault.key_vault[0].name}-secret-read"].role_definition_name == "Key Vault Secrets User" + error_message = "The role definition name of the organization role assignment is incorrect. Expected: Key Vault Secrets User, Got: ${azurerm_role_assignment.organization_role_assignment["keyvault-${data.azurerm_key_vault.key_vault[0].name}-secret-read"].role_definition_name}" + } + assert { + condition = azurerm_role_assignment.organization_role_assignment["keyvault-${data.azurerm_key_vault.key_vault[0].name}-vault-read"].role_definition_name == "Key Vault Reader" + error_message = "The role definition name of the organization role assignment is incorrect. Expected: Key Vault Reader, Got: ${azurerm_role_assignment.organization_role_assignment["keyvault-${data.azurerm_key_vault.key_vault[0].name}-vault-read"].role_definition_name}" + } + assert { + condition = azurerm_role_assignment.organization_role_assignment["keyvault-${data.azurerm_key_vault.key_vault[0].name}-vault-read"].scope == data.azurerm_key_vault.key_vault[0].id + error_message = "The scope of the organization role assignment is incorrect. Expected: ${data.azurerm_key_vault.key_vault[0].id}, Got: ${azurerm_role_assignment.organization_role_assignment["keyvault-${data.azurerm_key_vault.key_vault[0].name}-vault-read"].scope}" + } +} + +run "bootstrap_pull_request_credentials_test" { + assert { + condition = azurerm_federated_identity_credential.bootstrap_pull_request_credentials.name == "${var.github_foundations_organization_name}-${var.bootstrap_repo_name}-pr-credentials" + error_message = "The name of the bootstrap pull request credentials is incorrect. Expected: ${var.github_foundations_organization_name}-${var.bootstrap_repo_name}-pr-credentials, Got: ${azurerm_federated_identity_credential.bootstrap_pull_request_credentials.name}" + } + assert { + condition = azurerm_federated_identity_credential.bootstrap_pull_request_credentials.resource_group_name == var.rg_name + error_message = "The resource group name of the bootstrap pull request credentials is incorrect. Expected: ${var.rg_name}, Got: ${azurerm_federated_identity_credential.bootstrap_pull_request_credentials.resource_group_name}" + } + assert { + condition = azurerm_federated_identity_credential.bootstrap_pull_request_credentials.audience[0] == local.default_audience_name + error_message = "The audience of the bootstrap pull request credentials is incorrect. Expected: ${local.default_audience_name}, Got: ${azurerm_federated_identity_credential.bootstrap_pull_request_credentials.audience[0]}" + } + assert { + condition = azurerm_federated_identity_credential.bootstrap_pull_request_credentials.issuer == local.github_issuer_url + error_message = "The issuer of the bootstrap pull request credentials is incorrect. Expected: ${local.github_issuer_url}, Got: ${azurerm_federated_identity_credential.bootstrap_pull_request_credentials.issuer}" + } + assert { + condition = azurerm_federated_identity_credential.bootstrap_pull_request_credentials.parent_id == azurerm_user_assigned_identity.bootstrap_identity.id + error_message = "The parent id of the bootstrap pull request credentials is incorrect. Expected: ${azurerm_user_assigned_identity.bootstrap_identity.id}, Got: ${azurerm_federated_identity_credential.bootstrap_pull_request_credentials.parent_id}" + } + assert { + condition = azurerm_federated_identity_credential.bootstrap_pull_request_credentials.subject == "repo:${var.github_foundations_organization_name}/${var.bootstrap_repo_name}:pull_request" + error_message = "The subject of the bootstrap pull request credentials is incorrect. Expected: repo:${var.github_foundations_organization_name}/${var.bootstrap_repo_name}:pull_request, Got: ${azurerm_federated_identity_credential.bootstrap_pull_request_credentials.subject}" + } +} + +run "bootstrap_drift_credentials_test" { + assert { + condition = azurerm_federated_identity_credential.bootstrap_drift_credentials.name == "${var.github_foundations_organization_name}-${var.bootstrap_repo_name}-drift-credentials" + error_message = "The name of the bootstrap drift credentials is incorrect. Expected: ${var.github_foundations_organization_name}-${var.bootstrap_repo_name}-drift-credentials, Got: ${azurerm_federated_identity_credential.bootstrap_drift_credentials.name}" + } + assert { + condition = azurerm_federated_identity_credential.bootstrap_drift_credentials.resource_group_name == var.rg_name + error_message = "The resource group name of the bootstrap drift credentials is incorrect. Expected: ${var.rg_name}, Got: ${azurerm_federated_identity_credential.bootstrap_drift_credentials.resource_group_name}" + } + assert { + condition = azurerm_federated_identity_credential.bootstrap_drift_credentials.audience[0] == local.default_audience_name + error_message = "The audience of the bootstrap drift credentials is incorrect. Expected: ${local.default_audience_name}, Got: ${azurerm_federated_identity_credential.bootstrap_drift_credentials.audience[0]}" + } + assert { + condition = azurerm_federated_identity_credential.bootstrap_drift_credentials.issuer == local.github_issuer_url + error_message = "The issuer of the bootstrap drift credentials is incorrect. Expected: ${local.github_issuer_url}, Got: ${azurerm_federated_identity_credential.bootstrap_drift_credentials.issuer}" + } + assert { + condition = azurerm_federated_identity_credential.bootstrap_drift_credentials.parent_id == azurerm_user_assigned_identity.bootstrap_identity.id + error_message = "The parent id of the bootstrap drift credentials is incorrect. Expected: ${azurerm_user_assigned_identity.bootstrap_identity.id}, Got: ${azurerm_federated_identity_credential.bootstrap_drift_credentials.parent_id}" + } + assert { + condition = azurerm_federated_identity_credential.bootstrap_drift_credentials.subject == "repo:${var.github_foundations_organization_name}/${var.bootstrap_repo_name}:ref:refs/heads/${var.drift_detection_branch_name}" + error_message = "The subject of the bootstrap drift credentials is incorrect. Expected: repo:${var.github_foundations_organization_name}/${var.bootstrap_repo_name}:ref:refs/heads/${var.drift_detection_branch_name}, Got: ${azurerm_federated_identity_credential.bootstrap_drift_credentials.subject}" + } +} + +run "organization_pull_request_credentials_test" { + assert { + condition = azurerm_federated_identity_credential.organization_pull_request_credentials.name == "${var.github_foundations_organization_name}-${var.organizations_repo_name}-pr-credentials" + error_message = "The name of the organization pull request credentials is incorrect. Expected: ${var.github_foundations_organization_name}-${var.organizations_repo_name}-pr-credentials, Got: ${azurerm_federated_identity_credential.organization_pull_request_credentials.name}" + } + assert { + condition = azurerm_federated_identity_credential.organization_pull_request_credentials.resource_group_name == var.rg_name + error_message = "The resource group name of the organization pull request credentials is incorrect. Expected: ${var.rg_name}, Got: ${azurerm_federated_identity_credential.organization_pull_request_credentials.resource_group_name}" + } + assert { + condition = azurerm_federated_identity_credential.organization_pull_request_credentials.audience[0] == local.default_audience_name + error_message = "The audience of the organization pull request credentials is incorrect. Expected: ${local.default_audience_name}, Got: ${azurerm_federated_identity_credential.organization_pull_request_credentials.audience[0]}" + } + assert { + condition = azurerm_federated_identity_credential.organization_pull_request_credentials.issuer == local.github_issuer_url + error_message = "The issuer of the organization pull request credentials is incorrect. Expected: ${local.github_issuer_url}, Got: ${azurerm_federated_identity_credential.organization_pull_request_credentials.issuer}" + } + assert { + condition = azurerm_federated_identity_credential.organization_pull_request_credentials.parent_id == azurerm_user_assigned_identity.organization_identity.id + error_message = "The parent id of the organization pull request credentials is incorrect. Expected: ${azurerm_user_assigned_identity.organization_identity.id}, Got: ${azurerm_federated_identity_credential.organization_pull_request_credentials.parent_id}" + } + assert { + condition = azurerm_federated_identity_credential.organization_pull_request_credentials.subject == "repo:${var.github_foundations_organization_name}/${var.organizations_repo_name}:pull_request" + error_message = "The subject of the organization pull request credentials is incorrect. Expected: repo:${var.github_foundations_organization_name}/${var.organizations_repo_name}:pull_request, Got: ${azurerm_federated_identity_credential.organization_pull_request_credentials.subject}" + } +} + +run "organization_drift_credentials_test" { + assert { + condition = azurerm_federated_identity_credential.organization_drift_credentials.name == "${var.github_foundations_organization_name}-${var.organizations_repo_name}-drift-credentials" + error_message = "The name of the organization drift credentials is incorrect. Expected: ${var.github_foundations_organization_name}-${var.organizations_repo_name}-drift-credentials, Got: ${azurerm_federated_identity_credential.organization_drift_credentials.name}" + } + assert { + condition = azurerm_federated_identity_credential.organization_drift_credentials.resource_group_name == var.rg_name + error_message = "The resource group name of the organization drift credentials is incorrect. Expected: ${var.rg_name}, Got: ${azurerm_federated_identity_credential.organization_drift_credentials.resource_group_name}" + } + assert { + condition = azurerm_federated_identity_credential.organization_drift_credentials.audience[0] == local.default_audience_name + error_message = "The audience of the organization drift credentials is incorrect. Expected: ${local.default_audience_name}, Got: ${azurerm_federated_identity_credential.organization_drift_credentials.audience[0]}" + } + assert { + condition = azurerm_federated_identity_credential.organization_drift_credentials.issuer == local.github_issuer_url + error_message = "The issuer of the organization drift credentials is incorrect. Expected: ${local.github_issuer_url}, Got: ${azurerm_federated_identity_credential.organization_drift_credentials.issuer}" + } + assert { + condition = azurerm_federated_identity_credential.organization_drift_credentials.parent_id == azurerm_user_assigned_identity.organization_identity.id + error_message = "The parent id of the organization drift credentials is incorrect. Expected: ${azurerm_user_assigned_identity.organization_identity.id}, Got: ${azurerm_federated_identity_credential.organization_drift_credentials.parent_id}" + } + assert { + condition = azurerm_federated_identity_credential.organization_drift_credentials.subject == "repo:${var.github_foundations_organization_name}/${var.organizations_repo_name}:ref:refs/heads/${var.drift_detection_branch_name}" + error_message = "The subject of the organization drift credentials is incorrect. Expected: repo:${var.github_foundations_organization_name}/${var.organizations_repo_name}:ref:refs/heads/${var.drift_detection_branch_name}, Got: ${azurerm_federated_identity_credential.organization_drift_credentials.subject}" + } +} diff --git a/modules/github-azure-oidc/outputs.tftest.hcl b/modules/github-azure-oidc/outputs.tftest.hcl new file mode 100644 index 0000000..244668b --- /dev/null +++ b/modules/github-azure-oidc/outputs.tftest.hcl @@ -0,0 +1,102 @@ +mock_provider "github" {} +mock_provider "azurerm" { + # We need to mock the azurerm provider to avoid creating real resources + override_resource { + target = azurerm_user_assigned_identity.organization_identity + values = { + id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/github-foundations/providers/Microsoft.ManagedIdentity/userAssignedIdentities/ghf-organizations-identity" + client_id = "00000000-0000-0000-0000-000000000000" + } + } + override_resource { + target = azurerm_user_assigned_identity.bootstrap_identity + values = { + id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/github-foundations/providers/Microsoft.ManagedIdentity/userAssignedIdentities/ghf-bootstrap-identity" + client_id = "11111111-1111-1111-1111-111111111111" + } + } + override_resource { + target = azurerm_storage_account.github_foundations_sa + values = { + name = "ghfoundations" + id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/github-foundations/providers/Microsoft.Storage/storageAccounts/ghfoundations" + } + } +} + +override_data { + target = data.azurerm_key_vault.key_vault[0] + values = { + id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/kvrg/providers/Microsoft.KeyVault/vaults/awesome-possum" + } +} +override_data { + target = data.azurerm_client_config.current + values = { + tenant_id = "33333333-3333-3333-3333-333333333333" + subscription_id = "44444444-4444-4444-4444-444444444444" + } +} +override_resource { + target = azurerm_storage_container.github_foundations_tf_state_container[0] + values = { + resource_manager_id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/github-foundations/providers/Microsoft.Storage/storageAccounts/ghfoundations/blobServices/default/containers/ghf-state" + } +} +override_resource { + target = azurerm_storage_container.github_foundations_tf_state_encrypted_container[0] + values = { + resource_manager_id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/github-foundations/providers/Microsoft.Storage/storageAccounts/ghfoundations/blobServices/default/containers/ghf-state" + } +} + +variables { + # required variables + github_foundations_organization_name = "github-foundations" + rg_name = "github-foundations" + rg_location = "eastus" + sa_name = "ghfoundationssa" + drift_detection_branch_name = "drift-test-branch" + kv_resource_group = "kvrg" + + # variables for this test + tf_state_container = "ghf-state-container" + kv_name = "awesome-possum" +} + +run "create_test" { + command = apply + + assert { + condition = output.resource_group == var.rg_name + error_message = "Resource group name does not match. Expected: ${var.rg_name}, got: ${output.resource_group}" + } + assert { + condition = output.bootstrap_client_id == "11111111-1111-1111-1111-111111111111" + error_message = "Bootstrap repository client id does not match. Expected: 11111111-1111-1111-1111-111111111111, got: ${output.bootstrap_client_id}" + } + assert { + condition = output.organization_client_id == "00000000-0000-0000-0000-000000000000" + error_message = "Organizations repository client id does not match. Expected: 00000000-0000-0000-0000-000000000000, got: ${output.organization_client_id}" + } + assert { + condition = output.tenant_id == "33333333-3333-3333-3333-333333333333" + error_message = "Tenant id does not match. Expected: 33333333-3333-3333-3333-333333333333, got: ${output.tenant_id}" + } + assert { + condition = output.subscription_id == "44444444-4444-4444-4444-444444444444" + error_message = "Subscription id does not match. Expected: 44444444-4444-4444-4444-444444444444, got: ${output.subscription_id}" + } + assert { + condition = output.sa_name == var.sa_name + error_message = "Storage account name does not match. Expected: ${var.sa_name}, got: ${output.sa_name}" + } + assert { + condition = output.container_name == var.tf_state_container + error_message = "Container name does not match. Expected: ${var.tf_state_container}, got: ${output.container_name}" + } + assert { + condition = output.key_vault_id == "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/kvrg/providers/Microsoft.KeyVault/vaults/awesome-possum" + error_message = "Key vault id does not match. Expected: /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/kvrg/providers/Microsoft.KeyVault/vaults/awesome-possum, got: ${output.key_vault_id}" + } +} diff --git a/modules/github-azure-oidc/storage.tf b/modules/github-azure-oidc/storage.tf index 109394d..dd9c3fb 100644 --- a/modules/github-azure-oidc/storage.tf +++ b/modules/github-azure-oidc/storage.tf @@ -23,14 +23,14 @@ resource "azurerm_storage_encryption_scope" "encryption_scope" { resource "azurerm_storage_container" "github_foundations_tf_state_container" { count = local.default_encryption_scope == null ? 1 : 0 name = var.tf_state_container - storage_account_name = azurerm_storage_account.github_foundations_sa.name + storage_account_id = azurerm_storage_account.github_foundations_sa.id container_access_type = var.tf_state_container_anonymous_access_level } resource "azurerm_storage_container" "github_foundations_tf_state_encrypted_container" { count = local.default_encryption_scope != null ? 1 : 0 name = var.tf_state_container - storage_account_name = azurerm_storage_account.github_foundations_sa.name + storage_account_id = azurerm_storage_account.github_foundations_sa.id container_access_type = var.tf_state_container_anonymous_access_level default_encryption_scope = local.default_encryption_scope encryption_scope_override_enabled = var.tf_state_container_encryption_scope_override_enabled diff --git a/modules/github-azure-oidc/storage.tftest.hcl b/modules/github-azure-oidc/storage.tftest.hcl new file mode 100644 index 0000000..10ec23a --- /dev/null +++ b/modules/github-azure-oidc/storage.tftest.hcl @@ -0,0 +1,174 @@ +mock_provider "github" {} +mock_provider "azurerm" { + # We need to mock the azurerm provider to avoid creating real resources + override_resource { + target = azurerm_user_assigned_identity.organization_identity + values = { + id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/github-foundations/providers/Microsoft.ManagedIdentity/userAssignedIdentities/ghf-organizations-identity" + } + } + override_resource { + target = azurerm_user_assigned_identity.bootstrap_identity + values = { + id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/github-foundations/providers/Microsoft.ManagedIdentity/userAssignedIdentities/ghf-bootstrap-identity" + } + } + override_resource { + target = azurerm_storage_account.github_foundations_sa + values = { + name = "ghfoundations" + id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/github-foundations/providers/Microsoft.Storage/storageAccounts/ghfoundations" + } + } +} + +override_data { + target = data.azurerm_key_vault.key_vault[0] + values = { + id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/kvrg/providers/Microsoft.KeyVault/vaults/awesome-possum" + } +} +override_resource { + target = azurerm_storage_container.github_foundations_tf_state_container[0] + values = { + resource_manager_id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/github-foundations/providers/Microsoft.Storage/storageAccounts/ghfoundations/blobServices/default/containers/ghf-state" + } +} +override_resource { + target = azurerm_storage_container.github_foundations_tf_state_encrypted_container[0] + values = { + resource_manager_id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/github-foundations/providers/Microsoft.Storage/storageAccounts/ghfoundations/blobServices/default/containers/ghf-state" + } +} + +variables { + # required variables + github_foundations_organization_name = "github-foundations" + rg_name = "github-foundations" + rg_location = "eastus" + sa_name = "ghfoundations" + drift_detection_branch_name = "drift-test-branch" + kv_resource_group = "kvrg" + + # variables for this test + sa_tier = "Premium" + sa_replication_type = "LRS" + tf_state_container = "ghf-state" + tf_state_container_anonymous_access_level = "container" + tf_state_container_encryption_scope_override_enabled = true + tf_state_container_default_encryption_scope = { + name = "defaultEncryptionScope" + source = "Microsoft.Storage" + key_vault_key_id = "https://myvault.vault.azure.net/secrets/mysecret" + } + kv_name = "awesome-possum" + organizations_repo_name = "ghf-organizations" + bootstrap_repo_name = "ghf-bootstrap" +} + +run "github_foundations_sa_test" { + command = apply + + assert { + condition = azurerm_storage_account.github_foundations_sa.resource_group_name == var.rg_name + error_message = "The storage account resource group name is incorrect. Expected ${var.rg_name} but got ${azurerm_storage_account.github_foundations_sa.resource_group_name}." + } + assert { + condition = azurerm_storage_account.github_foundations_sa.location == var.rg_location + error_message = "The storage account location is incorrect. Expected ${var.rg_location} but got ${azurerm_storage_account.github_foundations_sa.location}." + } + assert { + condition = azurerm_storage_account.github_foundations_sa.account_tier == var.sa_tier + error_message = "The storage account tier is incorrect. Expected ${var.sa_tier} but got ${azurerm_storage_account.github_foundations_sa.account_tier}." + } + assert { + condition = azurerm_storage_account.github_foundations_sa.account_replication_type == var.sa_replication_type + error_message = "The storage account replication type is incorrect. Expected ${var.sa_replication_type} but got ${azurerm_storage_account.github_foundations_sa.account_replication_type}." + } + assert { + condition = azurerm_storage_account.github_foundations_sa.min_tls_version == "TLS1_2" + error_message = "The storage account min tls version is incorrect. Expected TLS1_2 but got ${azurerm_storage_account.github_foundations_sa.min_tls_version}." + } +} + +run "encryption_scope_test" { + + assert { + condition = azurerm_storage_container.github_foundations_tf_state_encrypted_container[0] != null + error_message = "The custom encryption scope is not set." + } + assert { + condition = length(azurerm_storage_container.github_foundations_tf_state_container) == 0 + error_message = "The default encryption scope should not be set." + } + assert { + condition = azurerm_storage_encryption_scope.encryption_scope[0].name == var.tf_state_container_default_encryption_scope.name + error_message = "The encryption scope name is incorrect. Expected ${var.tf_state_container_default_encryption_scope.name} but got ${azurerm_storage_encryption_scope.encryption_scope[0].name}." + } + assert { + condition = azurerm_storage_encryption_scope.encryption_scope[0].storage_account_id == azurerm_storage_account.github_foundations_sa.id + error_message = "The encryption scope storage account id is incorrect. Expected ${azurerm_storage_account.github_foundations_sa.id} but got ${azurerm_storage_encryption_scope.encryption_scope[0].storage_account_id}." + } + assert { + condition = azurerm_storage_encryption_scope.encryption_scope[0].source == var.tf_state_container_default_encryption_scope.source + error_message = "The encryption scope source is incorrect. Expected ${var.tf_state_container_default_encryption_scope.source} but got ${azurerm_storage_encryption_scope.encryption_scope[0].source}." + } + assert { + condition = azurerm_storage_encryption_scope.encryption_scope[0].key_vault_key_id == var.tf_state_container_default_encryption_scope.key_vault_key_id + error_message = "The encryption scope key vault key id is incorrect. Expected ${var.tf_state_container_default_encryption_scope.key_vault_key_id} but got ${azurerm_storage_encryption_scope.encryption_scope[0].key_vault_key_id}." + } + # Check the state container configuration + assert { + condition = azurerm_storage_container.github_foundations_tf_state_encrypted_container[0].name == var.tf_state_container + error_message = "The custom encryption scope container name is incorrect. Expected ${var.tf_state_container} but got ${azurerm_storage_container.github_foundations_tf_state_encrypted_container[0].name}." + } + assert { + condition = azurerm_storage_container.github_foundations_tf_state_encrypted_container[0].container_access_type == var.tf_state_container_anonymous_access_level + error_message = "The custom encryption scope container access type is incorrect. Expected ${var.tf_state_container_anonymous_access_level} but got ${azurerm_storage_container.github_foundations_tf_state_encrypted_container[0].container_access_type}." + } +} + +run "default_encryption_scope_test" { + variables { + tf_state_container_default_encryption_scope = { + name = "" + source = "" + key_vault_key_id = "" + } + } + + command = apply + plan_options { + refresh = true + } + assert { + condition = azurerm_storage_container.github_foundations_tf_state_container[0] != null + error_message = "The default encryption scope is not set." + } + assert { + condition = length(azurerm_storage_container.github_foundations_tf_state_encrypted_container) == 0 + error_message = "The custom encryption scope should not be set." + } + assert { + condition = length(azurerm_storage_encryption_scope.encryption_scope) == 0 + error_message = "The encryption scope should not be set." + } + assert { + condition = azurerm_storage_container.github_foundations_tf_state_container[0].name == var.tf_state_container + error_message = "The default encryption scope container name is incorrect. Expected ${var.tf_state_container} but got ${azurerm_storage_container.github_foundations_tf_state_container[0].name}." + } + assert { + condition = azurerm_storage_container.github_foundations_tf_state_container[0].container_access_type == var.tf_state_container_anonymous_access_level + error_message = "The default encryption scope container access type is incorrect. Expected ${var.tf_state_container_anonymous_access_level} but got ${azurerm_storage_container.github_foundations_tf_state_container[0].container_access_type}." + } +} + + +# run "default_encryption_scope_test" { +# override_data { +# target = data.azurerm_key_vault.key_vault[0] +# values = { +# id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/kvrg/providers/Microsoft.KeyVault/vaults/awesome-possum" +# } +# } +# } diff --git a/modules/github-azure-oidc/versions.tf b/modules/github-azure-oidc/versions.tf index 9a66b59..3add54a 100644 --- a/modules/github-azure-oidc/versions.tf +++ b/modules/github-azure-oidc/versions.tf @@ -3,7 +3,7 @@ terraform { required_providers { azurerm = { source = "hashicorp/azurerm" - version = ">=3.0.0" #tftest + version = ">=4.7.0" #tftest } random = { source = "hashicorp/random" diff --git a/modules/github-foundations/README.md b/modules/github-foundations/README.md index 65c6099..d484143 100644 --- a/modules/github-foundations/README.md +++ b/modules/github-foundations/README.md @@ -61,7 +61,7 @@ | [account\_type](#input\_account\_type) | The type of GitHub account being used. Should be one of either `Personal`, `Organization`, or `Enterprise`. | `string` | n/a | yes | | [bootstrap\_repository\_name](#input\_bootstrap\_repository\_name) | The name of the bootstrap repository. | `string` | `"bootstrap"` | no | | [foundation\_devs\_team\_name](#input\_foundation\_devs\_team\_name) | The name of the foundation developers team. | `string` | `"foundation-devs"` | no | -| [oidc\_configuration](#input\_oidc\_configuration) | n/a |
object({
gcp = optional(object({
workload_identity_provider_name_secret_name = optional(string)
workload_identity_provider_name = string

organization_workload_identity_sa_secret_name = optional(string)
organization_workload_identity_sa = string

gcp_secret_manager_project_id_variable_name = optional(string)
gcp_secret_manager_project_id = string

gcp_tf_state_bucket_project_id_variable_name = optional(string)
gcp_tf_state_bucket_project_id = string

bucket_name_variable_name = optional(string)
bucket_name = string

bucket_location_variable_name = optional(string)
bucket_location = string
}))
azure = optional(object({
bootstrap_client_id_variable_name = optional(string)
bootstrap_client_id = string

organization_client_id_variable_name = optional(string)
organization_client_id = string

tenant_id_variable_name = optional(string)
tenant_id = string

subscription_id_variable_name = optional(string)
subscription_id = string

resource_group_name_variable_name = optional(string)
resource_group_name = string

storage_account_name_variable_name = optional(string)
storage_account_name = string

container_name_variable_name = optional(string)
container_name = string

key_vault_id_variable_name = optional(string)
key_vault_id = string
}))
aws = optional(object({
s3_bucket_variable_name = optional(string)
s3_bucket = string

region_variable_name = optional(string)
region = string

organizations_role_variable_name = optional(string)
organizations_role = string

dynamodb_table_variable_name = optional(string)
dynamodb_table = string
}))
custom = optional(object({
organization_secrets = map(string)
organization_variables = map(string)
repository_secrets = map(map(string))
repository_variables = map(map(string))
}))
})
| n/a | yes | +| [oidc\_configuration](#input\_oidc\_configuration) | n/a |
object({
gcp = optional(object({
workload_identity_provider_name_secret_name = optional(string)
workload_identity_provider_name = string

organization_workload_identity_sa_secret_name = optional(string)
organization_workload_identity_sa = string

gcp_secret_manager_project_id_variable_name = optional(string)
gcp_secret_manager_project_id = string

gcp_tf_state_bucket_project_id_variable_name = optional(string)
gcp_tf_state_bucket_project_id = string

bucket_name_variable_name = optional(string)
bucket_name = string

bucket_location_variable_name = optional(string)
bucket_location = string
}))
azure = optional(object({
bootstrap_client_id_variable_name = optional(string)
bootstrap_client_id = string

organization_client_id_variable_name = optional(string)
organization_client_id = string

tenant_id_variable_name = optional(string)
tenant_id = string

subscription_id_variable_name = optional(string)
subscription_id = string

resource_group_name_variable_name = optional(string)
resource_group_name = string

storage_account_name_variable_name = optional(string)
storage_account_name = string

container_name_variable_name = optional(string)
container_name = string

key_vault_id_variable_name = optional(string)
key_vault_id = string
}))
aws = optional(object({
s3_bucket_variable_name = optional(string)
s3_bucket = string

region_variable_name = optional(string)
region = string

organizations_role_variable_name = optional(string)
organizations_role = string

dynamodb_table_variable_name = optional(string)
dynamodb_table = string
}))
custom = optional(object({
organization_secrets = map(string)
organization_variables = map(string)
repository_secrets = map(map(string))
repository_variables = map(map(string))
}))
})
| n/a | yes | | [organizations\_repository\_name](#input\_organizations\_repository\_name) | The name of the organizations repository. | `string` | `"organizations"` | no | | [readme\_path](#input\_readme\_path) | Local Path to the README file in your current codebase. Pushed to the github foundation repository. | `string` | `""` | no | diff --git a/modules/github-foundations/aws-oidc-variables.tftest.hcl b/modules/github-foundations/aws-oidc-variables.tftest.hcl new file mode 100644 index 0000000..eaf910d --- /dev/null +++ b/modules/github-foundations/aws-oidc-variables.tftest.hcl @@ -0,0 +1,107 @@ +mock_provider "github" {} + +variables { + # required variables + account_type = "Organization" + oidc_configuration = { + aws = { + s3_bucket_variable_name = "aws_s3_bucket_variable_name" + s3_bucket = "my-s3-bucket" + + region_variable_name = "aws_region_variable_name" + region = "us-west-2" + + organizations_role_variable_name = "aws_organizations_role_variable_name" + organizations_role = "my-organizations-role" + + dynamodb_table_variable_name = "aws_dynamodb_table_variable_name" + dynamodb_table = "my-dynamodb-table" + + + custom = { + organization_secrets = { + secret1 = "secret1", + secret2 = "secret2" + } + organization_variables = { + variable1 = "variable1", + variable2 = "variable2" + } + repository_secrets = { + repo1 = { + secret1 = "secret1", + secret2 = "secret2" + }, + repo2 = { + secret1 = "secret1", + secret2 = "secret2" + } + } + repository_variables = { + repo1 = { + variable1 = "variable1", + variable2 = "variable2" + }, + repo2 = { + variable1 = "variable1", + variable2 = "variable2" + } + } + } + } + } +} + +run "s3_bucket_test" { + command = apply + + assert { + condition = github_actions_organization_variable.s3_bucket[0].variable_name == var.oidc_configuration.aws.s3_bucket_variable_name + error_message = "s3_bucket variable name does not match. Expected: ${var.oidc_configuration.aws.s3_bucket_variable_name}, got: ${github_actions_organization_variable.s3_bucket[0].variable_name}" + } + assert { + condition = github_actions_organization_variable.s3_bucket[0].value == var.oidc_configuration.aws.s3_bucket + error_message = "s3_bucket value does not match. Expected: ${var.oidc_configuration.aws.s3_bucket}, got: ${github_actions_organization_variable.s3_bucket[0].value}" + } + assert { + condition = github_actions_organization_variable.s3_bucket[0].visibility == "selected" + error_message = "s3_bucket visibility does not match. Expected: selected, got: ${github_actions_organization_variable.s3_bucket[0].visibility}" + } +} + +run "region_test" { + assert { + condition = github_actions_organization_variable.region[0].variable_name == var.oidc_configuration.aws.region_variable_name + error_message = "region variable name does not match. Expected: ${var.oidc_configuration.aws.region_variable_name}, got: ${github_actions_organization_variable.region[0].variable_name}" + } + assert { + condition = github_actions_organization_variable.region[0].value == var.oidc_configuration.aws.region + error_message = "region value does not match. Expected: ${var.oidc_configuration.aws.region}, got: ${github_actions_organization_variable.region[0].value}" + } + assert { + condition = github_actions_organization_variable.region[0].visibility == "selected" + error_message = "region visibility does not match. Expected: selected, got: ${github_actions_organization_variable.region[0].visibility}" + } +} + +run "organizations_iam_role_test" { + assert { + condition = github_actions_secret.organizations_iam_role[0].secret_name == var.oidc_configuration.aws.organizations_role_variable_name + error_message = "organizations_iam_role secret name does not match. Expected: ${var.oidc_configuration.aws.organizations_role_variable_name}, got: ${github_actions_secret.organizations_iam_role[0].secret_name}" + } + assert { + condition = github_actions_secret.organizations_iam_role[0].plaintext_value == var.oidc_configuration.aws.organizations_role + error_message = "organizations_iam_role plaintext value does not match. Expected: ${var.oidc_configuration.aws.organizations_role}, got: ${nonsensitive(github_actions_secret.organizations_iam_role[0].plaintext_value)}" + } +} + +run "dynamodb_table_name_test" { + assert { + condition = github_actions_variable.dynamodb_table_name[0].variable_name == var.oidc_configuration.aws.dynamodb_table_variable_name + error_message = "dynamodb_table_name variable name does not match. Expected: ${var.oidc_configuration.aws.dynamodb_table_variable_name}, got: ${github_actions_variable.dynamodb_table_name[0].variable_name}" + } + assert { + condition = github_actions_variable.dynamodb_table_name[0].value == var.oidc_configuration.aws.dynamodb_table + error_message = "dynamodb_table_name value does not match. Expected: ${var.oidc_configuration.aws.dynamodb_table}, got: ${github_actions_variable.dynamodb_table_name[0].value}" + } +} diff --git a/modules/github-foundations/azure-oidc-variables.tftest.hcl b/modules/github-foundations/azure-oidc-variables.tftest.hcl new file mode 100644 index 0000000..f458aeb --- /dev/null +++ b/modules/github-foundations/azure-oidc-variables.tftest.hcl @@ -0,0 +1,114 @@ +mock_provider "github" {} + +variables { + # required variables + account_type = "Organization" + oidc_configuration = { + azure = { + bootstrap_client_id_variable_name = "bootstrap_client_id_variable_name" + bootstrap_client_id = "bootstrap_client_id" + + organization_client_id_variable_name = "organization_client_id_variable_name" + organization_client_id = "organization_client_id" + + tenant_id_variable_name = "tenant_id_variable_name" + tenant_id = "tenant_id" + + subscription_id_variable_name = "subscription_id_variable_name" + subscription_id = "subscription_id" + + resource_group_name_variable_name = "resource_group_name_variable_name" + resource_group_name = "resource_group_name" + + storage_account_name_variable_name = "storage_account_name_variable_name" + storage_account_name = "storage_account_name" + + container_name_variable_name = "container_name_variable_name" + container_name = "container_name" + + key_vault_id_variable_name = "key_vault_id_variable_name" + key_vault_id = "key_vault_id" + } + } +} + +run "organization_managed_identity_client_id_test" { + command = apply + + assert { + condition = github_actions_secret.organization_managed_identity_client_id[0].repository == "organizations" + error_message = "The org-managed client ID secret repository is incorrect. Expected: 'organizations', got: ${github_actions_secret.organization_managed_identity_client_id[0].repository}" + } + assert { + condition = github_actions_secret.organization_managed_identity_client_id[0].secret_name == var.oidc_configuration.azure.organization_client_id_variable_name + error_message = "The org-managed client ID secret name is incorrect. Expected: '${var.oidc_configuration.azure.organization_client_id_variable_name}', got: ${github_actions_secret.organization_managed_identity_client_id[0].secret_name}" + } + assert { + condition = github_actions_secret.organization_managed_identity_client_id[0].plaintext_value == var.oidc_configuration.azure.organization_client_id + error_message = "The org-managed client ID secret value is incorrect. Expected: '${var.oidc_configuration.azure.organization_client_id}', got: ${nonsensitive(github_actions_secret.organization_managed_identity_client_id[0].plaintext_value)}" + } +} + +run "bootstrap_managed_identity_client_id_test" { + + assert { + condition = github_actions_secret.bootstrap_managed_identity_client_id[0].repository == "bootstrap" + error_message = "The bootstrap-managed client ID secret repository is incorrect. Expected: 'bootstrap' got: ${github_actions_secret.bootstrap_managed_identity_client_id[0].repository}" + } + assert { + condition = github_actions_secret.bootstrap_managed_identity_client_id[0].secret_name == var.oidc_configuration.azure.bootstrap_client_id_variable_name + error_message = "The bootstrap-managed client ID secret name is incorrect. Expected: '${var.oidc_configuration.azure.bootstrap_client_id_variable_name}', got: ${github_actions_secret.bootstrap_managed_identity_client_id[0].secret_name}" + } + assert { + condition = github_actions_secret.bootstrap_managed_identity_client_id[0].plaintext_value == var.oidc_configuration.azure.bootstrap_client_id + error_message = "The bootstrap-managed client ID secret value is incorrect. Expected: '${var.oidc_configuration.azure.bootstrap_client_id}', got: ${nonsensitive(github_actions_secret.bootstrap_managed_identity_client_id[0].plaintext_value)}" + } +} + +run "tenant_id_test" { + + assert { + condition = github_actions_organization_secret.tenant_id[0].secret_name == var.oidc_configuration.azure.tenant_id_variable_name + error_message = "The tenant ID secret name is incorrect. Expected: '${var.oidc_configuration.azure.tenant_id_variable_name}', got: ${github_actions_organization_secret.tenant_id[0].secret_name}" + } + assert { + condition = github_actions_organization_secret.tenant_id[0].plaintext_value == var.oidc_configuration.azure.tenant_id + error_message = "The tenant ID secret value is incorrect. Expected: '${var.oidc_configuration.azure.tenant_id}', got: ${nonsensitive(github_actions_organization_secret.tenant_id[0].plaintext_value)}" + } +} + +run "subscription_id_test" { + + assert { + condition = github_actions_organization_variable.subscription_id[0].variable_name == var.oidc_configuration.azure.subscription_id_variable_name + error_message = "The subscription ID variable name is incorrect. Expected: '${var.oidc_configuration.azure.subscription_id_variable_name}', got: ${github_actions_organization_variable.subscription_id[0].variable_name}" + } + assert { + condition = github_actions_organization_variable.subscription_id[0].value == var.oidc_configuration.azure.subscription_id + error_message = "The subscription ID variable value is incorrect. Expected: '${var.oidc_configuration.azure.subscription_id}', got: ${nonsensitive(github_actions_organization_variable.subscription_id[0].value)}" + } +} + +run "resource_group_name_test" { + + assert { + condition = github_actions_organization_variable.resource_group_name[0].variable_name == var.oidc_configuration.azure.resource_group_name_variable_name + error_message = "The resource group name variable name is incorrect. Expected: '${var.oidc_configuration.azure.resource_group_name_variable_name}', got: ${github_actions_organization_variable.resource_group_name[0].variable_name}" + } + assert { + condition = github_actions_organization_variable.resource_group_name[0].value == var.oidc_configuration.azure.resource_group_name + error_message = "The resource group name variable value is incorrect. Expected: '${var.oidc_configuration.azure.resource_group_name}', got: ${nonsensitive(github_actions_organization_variable.resource_group_name[0].value)}" + } +} + +run "storage_account_name_test" { + + assert { + condition = github_actions_organization_variable.storage_account_name[0].variable_name == var.oidc_configuration.azure.storage_account_name_variable_name + error_message = "The storage account name variable name is incorrect. Expected: '${var.oidc_configuration.azure.storage_account_name_variable_name}', got: ${github_actions_organization_variable.storage_account_name[0].variable_name}" + } + assert { + condition = github_actions_organization_variable.storage_account_name[0].value == var.oidc_configuration.azure.storage_account_name + error_message = "The storage account name variable value is incorrect. Expected: '${var.oidc_configuration.azure.storage_account_name}', got: ${nonsensitive(github_actions_organization_variable.storage_account_name[0].value)}" + } +} diff --git a/modules/github-foundations/custom-oidc-variables.tf b/modules/github-foundations/custom-oidc-variables.tf index 26f219d..0e05e28 100644 --- a/modules/github-foundations/custom-oidc-variables.tf +++ b/modules/github-foundations/custom-oidc-variables.tf @@ -8,8 +8,8 @@ locals { repository = repo } } - ] - ), []) + ]... + ), {}) expanded_list_of_repo_variables = try(merge( [ @@ -20,8 +20,8 @@ locals { repository = repo } } - ] - ), []) + ]... + ), {}) } resource "github_actions_organization_secret" "custom_oidc_organization_secret" { @@ -49,7 +49,7 @@ resource "github_actions_organization_variable" "custom_oidc_organization_variab } resource "github_actions_secret" "repository_secret" { - for_each = toset(local.expanded_list_of_repo_secrets) + for_each = local.expanded_list_of_repo_secrets repository = each.value.repository secret_name = each.value.name @@ -57,7 +57,7 @@ resource "github_actions_secret" "repository_secret" { } resource "github_actions_variable" "repository_variable" { - for_each = toset(local.expanded_list_of_repo_variables) + for_each = local.expanded_list_of_repo_variables repository = each.value.repository variable_name = each.value.name diff --git a/modules/github-foundations/custom-oidc-variables.tftest.hcl b/modules/github-foundations/custom-oidc-variables.tftest.hcl new file mode 100644 index 0000000..bb7952c --- /dev/null +++ b/modules/github-foundations/custom-oidc-variables.tftest.hcl @@ -0,0 +1,120 @@ +mock_provider "github" {} + +variables { + # required variables + account_type = "Organization" + oidc_configuration = { + gcp = { + workload_identity_provider_name_secret_name = "workload_identity_provider_name_secret_name" + workload_identity_provider_name = "workload_identity_provider_name" + + organization_workload_identity_sa_secret_name = "organization_workload_identity_sa_secret_name" + organization_workload_identity_sa = "organization_workload_identity_sa" + + gcp_secret_manager_project_id_variable_name = "gcp_secret_manager_project_id_variable_name" + gcp_secret_manager_project_id = "gcp_secret_manager_project_id" + + gcp_tf_state_bucket_project_id_variable_name = "gcp_tf_state_bucket_project_id_variable_name" + gcp_tf_state_bucket_project_id = "gcp_tf_state_bucket_project_id" + + bucket_name_variable_name = "bucket_name_variable_name" + bucket_name = "bucket_name" + + bucket_location_variable_name = "bucket_location_variable_name" + bucket_location = "bucket_location" + } + + custom = { + organization_secrets = { + secret1 = "c2VjcmV0MQ==", # "secret1" base64 encoded + secret2 = "c2VjcmV0Mg==" # "secret2" base64 + } + organization_variables = { + variable1 = "variable1" + variable2 = "variable2" + } + repository_secrets = { + repo1 = { + secret1 = "c2VjcmV0MQ==", # "secret1" base64 encoded + secret2 = "c2VjcmV0Mg==" # "secret2" base64 + }, + repo2 = { + secret1 = "c2VjcmV0MQ==", # "secret1" base64 encoded + secret2 = "c2VjcmV0Mg==" # "secret2" base64 + } + } + repository_variables = { + repo1 = { + variable1 = "variable1", + variable2 = "variable2" + }, + repo2 = { + variable1 = "variable1", + variable2 = "variable2" + } + } + } + } +} + +run "custom_oidc_organization_secret_test" { + command = apply + + assert { + condition = length(github_actions_organization_secret.custom_oidc_organization_secret) == 2 + error_message = "The number of organization secrets is incorrect. Expected 2 but got ${length(github_actions_organization_secret.custom_oidc_organization_secret)}." + } +} + +run "custom_oidc_organization_variable_test" { + assert { + condition = length(github_actions_organization_variable.custom_oidc_organization_variable) == 2 + error_message = "The number of organization variables is incorrect. Expected 2 but got ${length(github_actions_organization_variable.custom_oidc_organization_variable)}." + } +} + +run "repository_secret_test" { + assert { + condition = length(github_actions_secret.repository_secret) == 4 + error_message = "The number of repository secrets is incorrect. Expected 4 but got ${length(github_actions_secret.repository_secret)}." + } + assert { + condition = github_actions_secret.repository_secret["repo1_secret1"].encrypted_value == var.oidc_configuration.custom.repository_secrets.repo1.secret1 + error_message = "Repository secret repo1_secret1 is incorrect. Expected '${var.oidc_configuration.custom.repository_secrets.repo1.secret1}' but got ${nonsensitive(github_actions_secret.repository_secret["repo1_secret1"].encrypted_value)}." + } + assert { + condition = github_actions_secret.repository_secret["repo1_secret2"].encrypted_value == var.oidc_configuration.custom.repository_secrets.repo1.secret2 + error_message = "Repository secret repo1_secret2 is incorrect. Expected '${var.oidc_configuration.custom.repository_secrets.repo1.secret2}' but got ${nonsensitive(github_actions_secret.repository_secret["repo1_secret2"].encrypted_value)}." + } + assert { + condition = github_actions_secret.repository_secret["repo2_secret1"].encrypted_value == var.oidc_configuration.custom.repository_secrets.repo2.secret1 + error_message = "Repository secret repo2_secret1 is incorrect. Expected '${var.oidc_configuration.custom.repository_secrets.repo2.secret1}' but got ${nonsensitive(github_actions_secret.repository_secret["repo2_secret1"].encrypted_value)}." + } + assert { + condition = github_actions_secret.repository_secret["repo2_secret2"].encrypted_value == var.oidc_configuration.custom.repository_secrets.repo2.secret2 + error_message = "Repository secret repo2_secret2 is incorrect. Expected '${var.oidc_configuration.custom.repository_secrets.repo2.secret2}' but got ${nonsensitive(github_actions_secret.repository_secret["repo2_secret2"].encrypted_value)}." + } +} + +run "repository_variable_test" { + assert { + condition = length(github_actions_variable.repository_variable) == 4 + error_message = "The number of repository variables is incorrect. Expected 4 but got ${length(github_actions_variable.repository_variable)}." + } + assert { + condition = github_actions_variable.repository_variable["repo1_variable1"].value == var.oidc_configuration.custom.repository_variables.repo1.variable1 + error_message = "Repository variable repo1_variable1 is incorrect. Expected '${var.oidc_configuration.custom.repository_variables.repo1.variable1}' but got ${github_actions_variable.repository_variable["repo1_variable1"].value}." + } + assert { + condition = github_actions_variable.repository_variable["repo1_variable2"].value == var.oidc_configuration.custom.repository_variables.repo1.variable2 + error_message = "Repository variable repo1_variable2 is incorrect. Expected '${var.oidc_configuration.custom.repository_variables.repo1.variable2}' but got ${github_actions_variable.repository_variable["repo1_variable2"].value}." + } + assert { + condition = github_actions_variable.repository_variable["repo2_variable1"].value == var.oidc_configuration.custom.repository_variables.repo2.variable1 + error_message = "Repository variable repo2_variable1 is incorrect. Expected '${var.oidc_configuration.custom.repository_variables.repo2.variable1}' but got ${github_actions_variable.repository_variable["repo2_variable1"].value}." + } + assert { + condition = github_actions_variable.repository_variable["repo2_variable2"].value == var.oidc_configuration.custom.repository_variables.repo2.variable2 + error_message = "Repository variable repo2_variable2 is incorrect. Expected '${var.oidc_configuration.custom.repository_variables.repo2.variable2}' but got ${github_actions_variable.repository_variable["repo2_variable2"].value}." + } +} diff --git a/modules/github-foundations/gcp-oidc-variables.tftest.hcl b/modules/github-foundations/gcp-oidc-variables.tftest.hcl new file mode 100644 index 0000000..d566c6f --- /dev/null +++ b/modules/github-foundations/gcp-oidc-variables.tftest.hcl @@ -0,0 +1,108 @@ +mock_provider "github" {} + +variables { + # required variables + account_type = "Organization" + oidc_configuration = { + gcp = { + workload_identity_provider_name_secret_name = "workload_identity_provider_name_secret_name" + workload_identity_provider_name = "workload_identity_provider_name" + + organization_workload_identity_sa_secret_name = "organization_workload_identity_sa_secret_name" + organization_workload_identity_sa = "organization_workload_identity_sa" + + gcp_secret_manager_project_id_variable_name = "gcp_secret_manager_project_id_variable_name" + gcp_secret_manager_project_id = "gcp_secret_manager_project_id" + + gcp_tf_state_bucket_project_id_variable_name = "gcp_tf_state_bucket_project_id_variable_name" + gcp_tf_state_bucket_project_id = "gcp_tf_state_bucket_project_id" + + bucket_name_variable_name = "bucket_name_variable_name" + bucket_name = "bucket_name" + + bucket_location_variable_name = "bucket_location_variable_name" + bucket_location = "bucket_location" + } + } +} + +run "organization_workload_identity_sa_test" { + + command = apply + + assert { + condition = github_actions_secret.organization_workload_identity_sa[0].repository == "organizations" + error_message = "The repository of the organization workload identity service account is incorrect. Expected 'organizations' but got '${github_actions_secret.organization_workload_identity_sa[0].repository}'." + } + assert { + condition = github_actions_secret.organization_workload_identity_sa[0].secret_name == var.oidc_configuration.gcp.organization_workload_identity_sa_secret_name + error_message = "The secret_name of the organization workload identity service account is incorrect. Expected '${var.oidc_configuration.gcp.organization_workload_identity_sa_secret_name}' but got '${github_actions_secret.organization_workload_identity_sa[0].secret_name}'." + } + assert { + condition = github_actions_secret.organization_workload_identity_sa[0].plaintext_value == var.oidc_configuration.gcp.organization_workload_identity_sa + error_message = "The plaintext_value of the organization workload identity service account is incorrect. Expected '${var.oidc_configuration.gcp.organization_workload_identity_sa}' but got '${nonsensitive(github_actions_secret.organization_workload_identity_sa[0].plaintext_value)}'." + } +} + +run "gcp_secret_manager_project_id_test" { + assert { + condition = github_actions_variable.gcp_secret_manager_project_id[0].repository == "organizations" + error_message = "The repository of the GCP Secret Manager project ID is incorrect. Expected 'organizations' but got '${github_actions_variable.gcp_secret_manager_project_id[0].repository}'." + } + assert { + condition = github_actions_variable.gcp_secret_manager_project_id[0].variable_name == var.oidc_configuration.gcp.gcp_secret_manager_project_id_variable_name + error_message = "The variable_name of the GCP Secret Manager project ID is incorrect. Expected '${var.oidc_configuration.gcp.gcp_secret_manager_project_id_variable_name}' but got '${github_actions_variable.gcp_secret_manager_project_id[0].variable_name}'." + } + assert { + condition = github_actions_variable.gcp_secret_manager_project_id[0].value == var.oidc_configuration.gcp.gcp_secret_manager_project_id + error_message = "The value of the GCP Secret Manager project ID is incorrect. Expected '${var.oidc_configuration.gcp.gcp_secret_manager_project_id}' but got '${github_actions_variable.gcp_secret_manager_project_id[0].value}'." + } +} + +run "workload_identity_provider_test" { + assert { + condition = github_actions_organization_secret.workload_identity_provider[0].secret_name == var.oidc_configuration.gcp.workload_identity_provider_name_secret_name + error_message = "The secret_name of the workload identity provider is incorrect. Expected '${var.oidc_configuration.gcp.workload_identity_provider_name_secret_name}' but got '${github_actions_organization_secret.workload_identity_provider[0].secret_name}'." + } + assert { + condition = github_actions_organization_secret.workload_identity_provider[0].plaintext_value == var.oidc_configuration.gcp.workload_identity_provider_name + error_message = "The plaintext_value of the workload identity provider is incorrect. Expected '${var.oidc_configuration.gcp.workload_identity_provider_name}' but got '${nonsensitive(github_actions_organization_secret.workload_identity_provider[0].plaintext_value)}'." + } +} + +run "tf_state_bucket_project_id_test" { + assert { + condition = github_actions_organization_variable.tf_state_bucket_project_id[0].variable_name == var.oidc_configuration.gcp.gcp_tf_state_bucket_project_id_variable_name + error_message = "The variable_name of the TF state bucket project ID is incorrect. Expected '${var.oidc_configuration.gcp.gcp_tf_state_bucket_project_id_variable_name}' but got '${github_actions_organization_variable.tf_state_bucket_project_id[0].variable_name}'." + } + assert { + condition = github_actions_organization_variable.tf_state_bucket_project_id[0].value == var.oidc_configuration.gcp.gcp_tf_state_bucket_project_id + error_message = "The value of the TF state bucket project ID is incorrect. Expected '${var.oidc_configuration.gcp.gcp_tf_state_bucket_project_id}' but got '${github_actions_organization_variable.tf_state_bucket_project_id[0].value}'." + } +} + +run "tf_state_bucket_name_test" { + assert { + condition = github_actions_organization_variable.tf_state_bucket_name[0].variable_name == var.oidc_configuration.gcp.bucket_name_variable_name + error_message = "The variable_name of the TF state bucket name is incorrect. Expected '${var.oidc_configuration.gcp.bucket_name_variable_name}' but got '${github_actions_organization_variable.tf_state_bucket_name[0].variable_name}'." + } + assert { + condition = github_actions_organization_variable.tf_state_bucket_name[0].value == var.oidc_configuration.gcp.bucket_name + error_message = "The value of the TF state bucket name is incorrect. Expected '${var.oidc_configuration.gcp.bucket_name}' but got '${github_actions_organization_variable.tf_state_bucket_name[0].value}'." + } +} + +run "tf_state_bucket_location_test" { + assert { + condition = github_actions_organization_variable.tf_state_bucket_location[0].variable_name == var.oidc_configuration.gcp.bucket_location_variable_name + error_message = "The variable_name of the TF state bucket location is incorrect. Expected '${var.oidc_configuration.gcp.bucket_location_variable_name}' but got '${github_actions_organization_variable.tf_state_bucket_location[0].variable_name}'." + } + assert { + condition = github_actions_organization_variable.tf_state_bucket_location[0].value == var.oidc_configuration.gcp.bucket_location + error_message = "The value of the TF state bucket location is incorrect. Expected '${var.oidc_configuration.gcp.bucket_location}' but got '${github_actions_organization_variable.tf_state_bucket_location[0].value}'." + } + assert { + condition = github_actions_organization_variable.tf_state_bucket_location[0].visibility == "selected" + error_message = "The visibility of the TF state bucket location is incorrect. Expected 'selected' but got '${github_actions_organization_variable.tf_state_bucket_location[0].visibility}'." + } +} diff --git a/modules/github-foundations/repo_readme.tftest.hcl b/modules/github-foundations/repo_readme.tftest.hcl new file mode 100644 index 0000000..f5911e0 --- /dev/null +++ b/modules/github-foundations/repo_readme.tftest.hcl @@ -0,0 +1,48 @@ +mock_provider "github" {} + +variables { + # required variables + account_type = "Organization" + oidc_configuration = { + gcp = { + workload_identity_provider_name_secret_name = "workload_identity_provider_name_secret_name" + workload_identity_provider_name = "workload_identity_provider_name" + + organization_workload_identity_sa_secret_name = "organization_workload_identity_sa_secret_name" + organization_workload_identity_sa = "organization_workload_identity_sa" + + gcp_secret_manager_project_id_variable_name = "gcp_secret_manager_project_id_variable_name" + gcp_secret_manager_project_id = "gcp_secret_manager_project_id" + + gcp_tf_state_bucket_project_id_variable_name = "gcp_tf_state_bucket_project_id_variable_name" + gcp_tf_state_bucket_project_id = "gcp_tf_state_bucket_project_id" + + bucket_name_variable_name = "bucket_name_variable_name" + bucket_name = "bucket_name" + + bucket_location_variable_name = "bucket_location_variable_name" + bucket_location = "bucket_location" + } + } + + # Variables for this test + readme_path = "../../README.md" +} + +run "repo_readme_test" { + + command = apply + + assert { + condition = github_repository_file.main_readme[0].repository == "organizations" + error_message = "The repository of the main readme file is incorrect. Expected 'organizations' but got '${github_repository_file.main_readme[0].repository}'." + } + assert { + condition = github_repository_file.main_readme[0].file == "README.md" + error_message = "The file of the main readme file is incorrect. Expected 'README.md' but got '${github_repository_file.main_readme[0].file}'." + } + assert { + condition = startswith(github_repository_file.main_readme[0].content, "# github-foundations-modules\n") + error_message = "The content of the main readme file is incorrect." + } +} diff --git a/modules/github-foundations/repositories.tf b/modules/github-foundations/repositories.tf index 7a3e95e..1c4bafc 100644 --- a/modules/github-foundations/repositories.tf +++ b/modules/github-foundations/repositories.tf @@ -73,7 +73,7 @@ resource "github_issue_labels" "drift_labels" { } label { - color = "ededed" name = "Drift" + color = "ededed" } } diff --git a/modules/github-foundations/repositories.tftest.hcl b/modules/github-foundations/repositories.tftest.hcl new file mode 100644 index 0000000..b0de1b9 --- /dev/null +++ b/modules/github-foundations/repositories.tftest.hcl @@ -0,0 +1,112 @@ +mock_provider "github" {} + +variables { + # required variables + account_type = "Organization" + oidc_configuration = { + gcp = { + workload_identity_provider_name_secret_name = "workload_identity_provider_name_secret_name" + workload_identity_provider_name = "workload_identity_provider_name" + + organization_workload_identity_sa_secret_name = "organization_workload_identity_sa_secret_name" + organization_workload_identity_sa = "organization_workload_identity_sa" + + gcp_secret_manager_project_id_variable_name = "gcp_secret_manager_project_id_variable_name" + gcp_secret_manager_project_id = "gcp_secret_manager_project_id" + + gcp_tf_state_bucket_project_id_variable_name = "gcp_tf_state_bucket_project_id_variable_name" + gcp_tf_state_bucket_project_id = "gcp_tf_state_bucket_project_id" + + bucket_name_variable_name = "bucket_name_variable_name" + bucket_name = "bucket_name" + + bucket_location_variable_name = "bucket_location_variable_name" + bucket_location = "bucket_location" + } + } + + # Variables for this test + bootstrap_repository_name = "bootstrap-repo" +} + +run "bootstrap_repo_test" { + + command = apply + + assert { + condition = github_repository.bootstrap_repo.name == var.bootstrap_repository_name + error_message = "The name of the bootstrap repository is incorrect. Expected '${var.bootstrap_repository_name}' but got '${github_repository.bootstrap_repo.name}'." + } + assert { + condition = github_repository.bootstrap_repo.visibility == "private" + error_message = "The visibility of the bootstrap repository is incorrect. Expected 'private' but got '${github_repository.bootstrap_repo.visibility}'." + } + assert { + condition = github_repository.bootstrap_repo.auto_init == true + error_message = "The auto_init of the bootstrap repository is incorrect. Expected 'true' but got '${github_repository.bootstrap_repo.auto_init}'." + } + assert { + condition = github_repository.bootstrap_repo.delete_branch_on_merge == true + error_message = "The delete_branch_on_merge of the bootstrap repository is incorrect. Expected 'true' but got '${github_repository.bootstrap_repo.delete_branch_on_merge}'." + } + assert { + condition = github_repository.bootstrap_repo.vulnerability_alerts == true + error_message = "The vulnerability_alerts of the bootstrap repository is incorrect. Expected 'true' but got '${github_repository.bootstrap_repo.vulnerability_alerts}'." + } +} + +run "bootstrap_repo_collaborators_test" { + assert { + condition = github_repository_collaborators.bootstrap_repo_collaborators.repository == github_repository.bootstrap_repo.name + error_message = "The repository of the bootstrap repository collaborators is incorrect. Expected '${github_repository.bootstrap_repo.name}' but got '${github_repository_collaborators.bootstrap_repo_collaborators.repository}'." + } + assert { + condition = github_repository_collaborators.bootstrap_repo_collaborators.team != null + error_message = "The permission of the bootstrap repository collaborators is incorrect. Got null." + } +} + +run "organizations_repo_test" { + assert { + condition = github_repository.organizations_repo.name == var.organizations_repository_name + error_message = "The name of the organizations repository is incorrect. Expected '${var.organizations_repository_name}' but got '${github_repository.organizations_repo.name}'." + } + assert { + condition = github_repository.organizations_repo.visibility == "private" + error_message = "The visibility of the organizations repository is incorrect. Expected 'private' but got '${github_repository.organizations_repo.visibility}'." + } + assert { + condition = github_repository.organizations_repo.auto_init == true + error_message = "The auto_init of the organizations repository is incorrect. Expected 'true' but got '${github_repository.organizations_repo.auto_init}'." + } + assert { + condition = github_repository.organizations_repo.delete_branch_on_merge == true + error_message = "The delete_branch_on_merge of the organizations repository is incorrect. Expected 'true' but got '${github_repository.organizations_repo.delete_branch_on_merge}'." + } + assert { + condition = github_repository.organizations_repo.vulnerability_alerts == true + error_message = "The vulnerability_alerts of the organizations repository is incorrect. Expected 'true' but got '${github_repository.organizations_repo.vulnerability_alerts}'." + } + assert { + condition = github_repository.organizations_repo.has_issues == true + error_message = "The has_issues of the organizations repository is incorrect. Expected 'true' but got '${github_repository.organizations_repo.has_issues}'." + } +} + +run "organization_repo_collaborators_test" { + assert { + condition = github_repository_collaborators.organization_repo_collaborators.repository == github_repository.organizations_repo.name + error_message = "The repository of the organizations repository collaborators is incorrect. Expected '${github_repository.organizations_repo.name}' but got '${github_repository_collaborators.organization_repo_collaborators.repository}'." + } + assert { + condition = github_repository_collaborators.organization_repo_collaborators.team != null + error_message = "The permission of the organizations repository collaborators is incorrect. Got null." + } +} + +run "drift_labels_test" { + assert { + condition = length(github_issue_labels.drift_labels[0]) == 3 + error_message = "The number of drift labels is incorrect. Expected '3' but got '${length(github_issue_labels.drift_labels[0])}'." + } +} diff --git a/modules/github-foundations/rulesets.tftest.hcl b/modules/github-foundations/rulesets.tftest.hcl new file mode 100644 index 0000000..431f04f --- /dev/null +++ b/modules/github-foundations/rulesets.tftest.hcl @@ -0,0 +1,36 @@ +mock_provider "github" {} + +variables { + # required variables + account_type = "Enterprise" + oidc_configuration = { + gcp = { + workload_identity_provider_name_secret_name = "workload_identity_provider_name_secret_name" + workload_identity_provider_name = "workload_identity_provider_name" + + organization_workload_identity_sa_secret_name = "organization_workload_identity_sa_secret_name" + organization_workload_identity_sa = "organization_workload_identity_sa" + + gcp_secret_manager_project_id_variable_name = "gcp_secret_manager_project_id_variable_name" + gcp_secret_manager_project_id = "gcp_secret_manager_project_id" + + gcp_tf_state_bucket_project_id_variable_name = "gcp_tf_state_bucket_project_id_variable_name" + gcp_tf_state_bucket_project_id = "gcp_tf_state_bucket_project_id" + + bucket_name_variable_name = "bucket_name_variable_name" + bucket_name = "bucket_name" + + bucket_location_variable_name = "bucket_location_variable_name" + bucket_location = "bucket_location" + } + } +} + +run "base_ruleset_module_test" { + command = apply + + assert { + condition = length(module.base_ruleset) == 1 + error_message = "The base ruleset module was not created." + } +} diff --git a/modules/github-foundations/teams.tftest.hcl b/modules/github-foundations/teams.tftest.hcl new file mode 100644 index 0000000..e0841c0 --- /dev/null +++ b/modules/github-foundations/teams.tftest.hcl @@ -0,0 +1,40 @@ +mock_provider "github" {} + +variables { + # required variables + account_type = "Organization" + oidc_configuration = { + gcp = { + workload_identity_provider_name_secret_name = "workload_identity_provider_name_secret_name" + workload_identity_provider_name = "workload_identity_provider_name" + + organization_workload_identity_sa_secret_name = "organization_workload_identity_sa_secret_name" + organization_workload_identity_sa = "organization_workload_identity_sa" + + gcp_secret_manager_project_id_variable_name = "gcp_secret_manager_project_id_variable_name" + gcp_secret_manager_project_id = "gcp_secret_manager_project_id" + + gcp_tf_state_bucket_project_id_variable_name = "gcp_tf_state_bucket_project_id_variable_name" + gcp_tf_state_bucket_project_id = "gcp_tf_state_bucket_project_id" + + bucket_name_variable_name = "bucket_name_variable_name" + bucket_name = "bucket_name" + + bucket_location_variable_name = "bucket_location_variable_name" + bucket_location = "bucket_location" + } + } + + # Variables for this test + foundation_devs_team_name = "foundation-devs" +} + +run "create_gcp_teams_test" { + + command = apply + + assert { + condition = github_team.foundation_devs.name == var.foundation_devs_team_name + error_message = "The name of the foundation developers team is incorrect. Expected '${var.foundation_devs_team_name}' but got '${github_team.foundation_devs.name}'." + } +} diff --git a/modules/github-gcloud-oidc/README.md b/modules/github-gcloud-oidc/README.md index 94ec762..0fe4c83 100644 --- a/modules/github-gcloud-oidc/README.md +++ b/modules/github-gcloud-oidc/README.md @@ -3,22 +3,22 @@ | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 1.6 | -| [google](#requirement\_google) | >= 3.77 | -| [google-beta](#requirement\_google-beta) | >= 3.77 | +| [google](#requirement\_google) | >= 6.12.0 | +| [google-beta](#requirement\_google-beta) | >= 6.12.0 | | [random](#requirement\_random) | >= 3.6 | ## Providers | Name | Version | |------|---------| -| [google](#provider\_google) | >= 3.77 | +| [google](#provider\_google) | >= 6.12.0 | | [random](#provider\_random) | >= 3.6 | ## Modules | Name | Source | Version | |------|--------|---------| -| [oidc](#module\_oidc) | terraform-google-modules/github-actions-runners/google//modules/gh-oidc | 3.1.2 | +| [oidc](#module\_oidc) | terraform-google-modules/github-actions-runners/google//modules/gh-oidc | 4.0.0 | ## Resources @@ -45,7 +45,7 @@ | [billing\_account](#input\_billing\_account) | Billing account id. | `string` | `null` | no | | [bootstrap\_repo\_name](#input\_bootstrap\_repo\_name) | The name of the github foundations bootstrap repository. Defaults to `bootstrap` | `string` | `"bootstrap"` | no | | [bucket\_name](#input\_bucket\_name) | Bucket name | `string` | n/a | yes | -| [cors](#input\_cors) | CORS configuration for the bucket. Defaults to null. |
object({
origin = optional(list(string))
method = optional(list(string))
response_header = optional(list(string))
max_age_seconds = optional(number)
})
| `null` | no | +| [cors](#input\_cors) | CORS configuration for the bucket. Defaults to null. |
object({
origin = optional(list(string))
method = optional(list(string))
response_header = optional(list(string))
max_age_seconds = optional(number)
})
| `null` | no | | [custom\_placement\_config](#input\_custom\_placement\_config) | The bucket's custom location configuration, which specifies the individual regions that comprise a dual-region bucket. If the bucket is designated as REGIONAL or MULTI\_REGIONAL, the parameters are empty. | `list(string)` | `null` | no | | [default\_event\_based\_hold](#input\_default\_event\_based\_hold) | Enable event based hold to new objects added to specific bucket, defaults to false. | `bool` | `null` | no | | [descriptive\_name](#input\_descriptive\_name) | Name of the project name. Used for project name instead of `project_name` variable. | `string` | `null` | no | @@ -56,9 +56,9 @@ | [github\_foundations\_organization\_name](#input\_github\_foundations\_organization\_name) | The name of the organization that the github foundation repos will be under. | `string` | n/a | yes | | [id](#input\_id) | Folder ID in case you use folder\_create=false. | `string` | `null` | no | | [labels](#input\_labels) | Resource labels. | `map(string)` | `{}` | no | -| [lifecycle\_rules](#input\_lifecycle\_rules) | Bucket lifecycle rule. |
map(object({
action = object({
type = string
storage_class = optional(string)
})
condition = object({
age = optional(number)
created_before = optional(string)
custom_time_before = optional(string)
days_since_custom_time = optional(number)
days_since_noncurrent_time = optional(number)
matches_prefix = optional(list(string))
matches_storage_class = optional(list(string)) # STANDARD, MULTI_REGIONAL, REGIONAL, NEARLINE, COLDLINE, ARCHIVE, DURABLE_REDUCED_AVAILABILITY
matches_suffix = optional(list(string))
noncurrent_time_before = optional(string)
num_newer_versions = optional(number)
with_state = optional(string) # "LIVE", "ARCHIVED", "ANY"
})
}))
| `{}` | no | +| [lifecycle\_rules](#input\_lifecycle\_rules) | Bucket lifecycle rule. |
map(object({
action = object({
type = string
storage_class = optional(string)
})
condition = object({
age = optional(number)
created_before = optional(string)
custom_time_before = optional(string)
days_since_custom_time = optional(number)
days_since_noncurrent_time = optional(number)
matches_prefix = optional(list(string))
matches_storage_class = optional(list(string)) # STANDARD, MULTI_REGIONAL, REGIONAL, NEARLINE, COLDLINE, ARCHIVE, DURABLE_REDUCED_AVAILABILITY
matches_suffix = optional(list(string))
noncurrent_time_before = optional(string)
num_newer_versions = optional(number)
with_state = optional(string) # "LIVE", "ARCHIVED", "ANY"
})
}))
| `{}` | no | | [location](#input\_location) | Bucket location. | `string` | n/a | yes | -| [logging\_config](#input\_logging\_config) | Bucket logging configuration. |
object({
log_bucket = string
log_object_prefix = optional(string)
})
| `null` | no | +| [logging\_config](#input\_logging\_config) | Bucket logging configuration. |
object({
log_bucket = string
log_object_prefix = optional(string)
})
| `null` | no | | [organizations\_repo\_name](#input\_organizations\_repo\_name) | The name of the github foundations organizations repository. Defaults to `organizations` | `string` | `"organizations"` | no | | [parent](#input\_parent) | Parent in folders/folder\_id or organizations/org\_id format. | `string` | `null` | no | | [prefix](#input\_prefix) | Optional prefix used to generate project id and name. | `string` | `null` | no | @@ -66,14 +66,13 @@ | [project\_name](#input\_project\_name) | Project name and id suffix. | `string` | n/a | yes | | [project\_parent](#input\_project\_parent) | Parent folder or organization in 'folders/folder\_id' or 'organizations/org\_id' format. | `string` | `null` | no | | [requester\_pays](#input\_requester\_pays) | Enables Requester Pays on a storage bucket. | `bool` | `null` | no | -| [retention\_policy](#input\_retention\_policy) | Bucket retention policy. |
object({
retention_period = number
is_locked = optional(bool)
})
| `null` | no | -| [service\_config](#input\_service\_config) | Configure service API activation. |
object({
disable_on_destroy = bool
disable_dependent_services = bool
})
|
{
"disable_dependent_services": false,
"disable_on_destroy": false
}
| no | +| [retention\_policy](#input\_retention\_policy) | Bucket retention policy. |
object({
retention_period = number
is_locked = optional(bool)
})
| `null` | no | +| [service\_config](#input\_service\_config) | Configure service API activation. |
object({
disable_on_destroy = bool
disable_dependent_services = bool
})
|
{
"disable_dependent_services": false,
"disable_on_destroy": false
}
| no | | [services](#input\_services) | Service APIs to enable. | `list(string)` | `[]` | no | -| [skip\_delete](#input\_skip\_delete) | Allows the underlying resources to be destroyed without destroying the project itself. | `bool` | `false` | no | | [storage\_class](#input\_storage\_class) | Bucket storage class. | `string` | `"STANDARD"` | no | | [uniform\_bucket\_level\_access](#input\_uniform\_bucket\_level\_access) | Allow using object ACLs (false) or not (true, this is the recommended behavior) , defaults to true (which is the recommended practice, but not the behavior of storage API). | `bool` | `true` | no | | [versioning](#input\_versioning) | Enable versioning, defaults to false. | `bool` | `false` | no | -| [website](#input\_website) | Bucket website. |
object({
main_page_suffix = optional(string)
not_found_page = optional(string)
})
| `null` | no | +| [website](#input\_website) | Bucket website. |
object({
main_page_suffix = optional(string)
not_found_page = optional(string)
})
| `null` | no | ## Outputs diff --git a/modules/github-gcloud-oidc/folder.tftest.hcl b/modules/github-gcloud-oidc/folder.tftest.hcl new file mode 100644 index 0000000..e473320 --- /dev/null +++ b/modules/github-gcloud-oidc/folder.tftest.hcl @@ -0,0 +1,41 @@ +mock_provider "github" {} +mock_provider "google" {} +mock_provider "google-beta" {} +mock_provider "github-actions-runners" {} +override_module { + target = module.oidc + outputs = { + gh-oidc = { + sa_name = "test-sa" + attribute = "attribute.repository/github-foundations/bootstrap" + } + } +} + +variables { + # required variables + github_foundations_organization_name = "github-foundations" + parent = "organizations/1234567890" + project_parent = "organizations/1234567890" + project_name = "test-project" + folder_name = "test-folder" + bucket_name = "test-bucket" + location = "US" + + # variables for this test + folder_create = true +} + +run "folder_test" { + command = apply + + assert { + condition = google_folder.folder[0].display_name == var.folder_name + error_message = "The folder name is incorrect. Expected ${var.folder_name} but got ${google_folder.folder[0].display_name}." + } + assert { + condition = google_folder.folder[0].parent == var.parent + error_message = "The folder parent is incorrect. Expected ${var.parent} but got ${google_folder.folder[0].parent}." + } + +} diff --git a/modules/github-gcloud-oidc/oidc.tf b/modules/github-gcloud-oidc/oidc.tf index e0462ab..16edca6 100644 --- a/modules/github-gcloud-oidc/oidc.tf +++ b/modules/github-gcloud-oidc/oidc.tf @@ -47,13 +47,15 @@ resource "google_project_iam_member" "organizations_member" { /* * oidc setup */ +#trivy:ignore:avd-gcp-0068 module "oidc" { source = "terraform-google-modules/github-actions-runners/google//modules/gh-oidc" - version = "3.1.2" + version = "4.0.0" depends_on = [google_project_service.project_services, google_service_account.bootstrap_sa, google_service_account.organizations_sa] project_id = google_project.project[0].project_id pool_id = local.pool_id provider_id = local.provider_id + sa_mapping = { (google_service_account.bootstrap_sa.account_id) = { sa_name = google_service_account.bootstrap_sa.name diff --git a/modules/github-gcloud-oidc/oidc.tftest.hcl b/modules/github-gcloud-oidc/oidc.tftest.hcl new file mode 100644 index 0000000..c2df5dd --- /dev/null +++ b/modules/github-gcloud-oidc/oidc.tftest.hcl @@ -0,0 +1,67 @@ +mock_provider "github" {} +mock_provider "google-beta" {} +mock_provider "github-actions-runners" {} +override_module { + target = module.oidc + outputs = { + gh-oidc = { + sa_name = "test-sa" + attribute = "attribute.repository/github-foundations/bootstrap" + } + } +} + +variables { + # required variables + github_foundations_organization_name = "github-foundations" + parent = "organizations/1234567890" + project_parent = "organizations/1234567890" + project_name = "test-project" + folder_name = "test-folder" + bucket_name = "test-bucket" + location = "US" + + # variables for this test + bootstrap_repo_name = "bootstrap-repo" + organizations_repo_name = "organizations-repo" +} + + +mock_provider "google" { + override_resource { + target = google_service_account.bootstrap_sa + values = { + name = "bootstrap-repo-sa" + } + } + override_resource { + target = google_service_account.organizations_sa + values = { + name = "organizations-repo-sa" + } + } +} + +run "bootstrap_sa_test" { + command = apply + + assert { + condition = google_service_account.bootstrap_sa.name == "${var.bootstrap_repo_name}-sa" + error_message = "The bootstrap service account name is incorrect. Expected ${var.bootstrap_repo_name}-sa but got ${google_service_account.bootstrap_sa.name}." + } + assert { + condition = google_service_account.bootstrap_sa.account_id == "${var.bootstrap_repo_name}-sa" + error_message = "The bootstrap service account account_id is incorrect. Expected ${var.bootstrap_repo_name} but got ${google_service_account.bootstrap_sa.account_id}." + } +} + +run "organizations_sa_test" { + assert { + condition = google_service_account.organizations_sa.name == "${var.organizations_repo_name}-sa" + error_message = "The organizations service account name is incorrect. Expected ${var.organizations_repo_name}-sa but got ${google_service_account.organizations_sa.name}." + } + assert { + condition = google_service_account.organizations_sa.account_id == "${var.organizations_repo_name}-sa" + error_message = "The organizations service account account_id is incorrect. Expected ${var.organizations_repo_name} but got ${google_service_account.organizations_sa.account_id}." + } +} diff --git a/modules/github-gcloud-oidc/outputs.tftest.hcl b/modules/github-gcloud-oidc/outputs.tftest.hcl new file mode 100644 index 0000000..5c85ae6 --- /dev/null +++ b/modules/github-gcloud-oidc/outputs.tftest.hcl @@ -0,0 +1,75 @@ +mock_provider "github" {} +mock_provider "google" {} +mock_provider "google-beta" {} +mock_provider "github-actions-runners" {} +override_module { + target = module.oidc + outputs = { + gh-oidc = { + sa_name = "test-sa" + attribute = "attribute.repository/github-foundations/bootstrap" + } + provider_name = "gh-oidc-provider" + } +} + +variables { + # required variables + github_foundations_organization_name = "github-foundations" + parent = "organizations/1234567890" + project_parent = "organizations/1234567890" + project_name = "test-project" + folder_name = "test-folder" + bucket_name = "test-bucket" + location = "US" + +} + +run "outputs_test" { + command = apply + + assert { + condition = output.folder != null + error_message = "The folder was not created." + } + assert { + condition = output.id == output.folder.name + error_message = "The folder id is incorrect. Expected ${output.folder.name} but got ${output.id}." + } + assert { + condition = startswith(output.project_id, var.project_name) + error_message = "The project id is incorrect. Expected ${var.project_name}#### but got ${output.project_id}." + } + assert { + condition = output.name == var.folder_name + error_message = "The folder name is incorrect. Expected ${var.folder_name} but got ${output.name}." + } + assert { + condition = output.provider_name == "gh-oidc-provider" + error_message = "The provider name is incorrect. Expected gh-oidc-provider but got ${output.provider_name}." + } + assert { + condition = output.bootstrap_sa != null + error_message = "The bootstrap service account was not created." + } + assert { + condition = output.bootstrap_sa_name != null + error_message = "The bootstrap service account name was not created." + } + assert { + condition = output.organizations_sa != null + error_message = "The organizations service account was not created." + } + assert { + condition = output.organizations_sa_name != null + error_message = "The organizations service account name was not created." + } + assert { + condition = output.bucket_name == var.bucket_name + error_message = "The bucket name is incorrect. Expected ${var.bucket_name} but got ${output.bucket_name}." + } + assert { + condition = output.bucket_location == var.location + error_message = "The bucket location is incorrect. Expected ${var.location} but got ${output.bucket_location}." + } +} diff --git a/modules/github-gcloud-oidc/project.tf b/modules/github-gcloud-oidc/project.tf index a52ccc9..415f112 100644 --- a/modules/github-gcloud-oidc/project.tf +++ b/modules/github-gcloud-oidc/project.tf @@ -55,7 +55,6 @@ resource "google_project" "project" { billing_account = var.billing_account auto_create_network = var.auto_create_network labels = var.labels - skip_delete = var.skip_delete depends_on = [google_folder.folder] } diff --git a/modules/github-gcloud-oidc/project.tftest.hcl b/modules/github-gcloud-oidc/project.tftest.hcl new file mode 100644 index 0000000..37e521f --- /dev/null +++ b/modules/github-gcloud-oidc/project.tftest.hcl @@ -0,0 +1,138 @@ +mock_provider "github" {} +mock_provider "google" {} +mock_provider "google-beta" {} +mock_provider "github-actions-runners" {} +override_module { + target = module.oidc + outputs = { + gh-oidc = { + sa_name = "test-sa" + attribute = "attribute.repository/github-foundations/bootstrap" + } + } +} + +variables { + # required variables + github_foundations_organization_name = "github-foundations" + parent = "organizations/1234567890" + project_parent = "organizations/1234567890" + project_name = "test-project" + folder_name = "test-folder" + bucket_name = "test-bucket" + location = "US" + + # variables for this test + project_create = true + billing_account = "billing-account" + auto_create_network = true + labels = { label1 = "value1", label2 = "value2" } + services = ["service1.googleapis.com", "service2.googleapis.com"] + service_config = { + disable_on_destroy = true + disable_dependent_services = true + } +} + + +# run "expect_project_errors_test" { +# variables { +# services = ["service1", "service2"] +# } +# command = apply +# plan_options { +# refresh = true +# } + +# expect_failures = [ google_project.project, google_project_service.project_services ] +# } + +run "google_project_test" { + command = apply + + assert { + condition = length(google_project.project) == 1 + error_message = "The project was not created." + } + assert { + condition = google_project.project[0].org_id == split("/", var.project_parent)[1] + error_message = "The org_id is incorrect. Expected ${split("/", var.project_parent)[1]} but got ${google_project.project[0].org_id}." + } + assert { + condition = google_project.project[0].folder_id == null + error_message = "The folder_id is incorrect. Expected null." + } + assert { + condition = startswith(google_project.project[0].project_id, var.project_name) + error_message = "The project name is incorrect. Expected '${var.project_name}####' but got '${google_project.project[0].project_id}'." + } + assert { + condition = startswith(google_project.project[0].name, var.project_name) + error_message = "The project name is incorrect. Expected '${var.project_name}####' but got '${google_project.project[0].name}'." + } + assert { + condition = google_project.project[0].billing_account == var.billing_account + error_message = "The billing account is incorrect. Expected '${var.billing_account}' but got '${google_project.project[0].billing_account}'." + } + assert { + condition = google_project.project[0].auto_create_network == var.auto_create_network + error_message = "The auto_create_network is incorrect. Expected '${var.auto_create_network}' but got '${google_project.project[0].auto_create_network}'." + } + assert { + condition = google_project.project[0].labels.label1 == var.labels.label1 + error_message = "The label1 is incorrect. Expected '${var.labels.label1}' but got '${google_project.project[0].labels.label1}'." + } + assert { + condition = google_project.project[0].labels.label2 == var.labels.label2 + error_message = "The label2 is incorrect. Expected '${var.labels.label2}' but got '${google_project.project[0].labels.label2}'." + } +} + +run "project_services_test" { + assert { + condition = length(google_project_service.project_services) == length(var.services) + error_message = "The number of services is incorrect. Expected ${length(var.services)} but got ${length(google_project_service.project_services)}." + } + # test the first service + assert { + condition = google_project_service.project_services["service1.googleapis.com"].project == google_project.project[0].project_id + error_message = "The service1 project is incorrect. Expected '${google_project.project[0].project_id}' but got '${google_project_service.project_services["service1.googleapis.com"].project}'." + } + assert { + condition = google_project_service.project_services["service1.googleapis.com"] != null + error_message = "The service1 was not created." + } + assert { + condition = google_project_service.project_services["service1.googleapis.com"].service == "service1.googleapis.com" + error_message = "The service1 name is incorrect. Expected 'service1.googleapis.com' but got '${google_project_service.project_services["service1.googleapis.com"].service}'." + } + assert { + condition = google_project_service.project_services["service1.googleapis.com"].disable_on_destroy == var.service_config.disable_on_destroy + error_message = "The service1 disable_on_destroy is incorrect. Expected '${var.service_config.disable_on_destroy}' but got '${google_project_service.project_services["service1.googleapis.com"].disable_on_destroy}'." + } + assert { + condition = google_project_service.project_services["service1.googleapis.com"].disable_dependent_services == var.service_config.disable_dependent_services + error_message = "The service1 disable_dependent_services is incorrect. Expected '${var.service_config.disable_dependent_services}' but got '${google_project_service.project_services["service1.googleapis.com"].disable_dependent_services}'." + } + # test the second service + assert { + condition = google_project_service.project_services["service2.googleapis.com"].project == google_project.project[0].project_id + error_message = "The service2 project is incorrect. Expected '${google_project.project[0].project_id}' but got '${google_project_service.project_services["service2.googleapis.com"].project}'." + } + assert { + condition = google_project_service.project_services["service2.googleapis.com"] != null + error_message = "The service2 was not created." + } + assert { + condition = google_project_service.project_services["service2.googleapis.com"].service == "service2.googleapis.com" + error_message = "The service2 name is incorrect. Expected 'service2.googleapis.com' but got '${google_project_service.project_services["service2.googleapis.com"].service}'." + } + assert { + condition = google_project_service.project_services["service2.googleapis.com"].disable_on_destroy == var.service_config.disable_on_destroy + error_message = "The service2 disable_on_destroy is incorrect. Expected '${var.service_config.disable_on_destroy}' but got '${google_project_service.project_services["service2.googleapis.com"].disable_on_destroy}'." + } + assert { + condition = google_project_service.project_services["service2.googleapis.com"].disable_dependent_services == var.service_config.disable_dependent_services + error_message = "The service2 disable_dependent_services is incorrect. Expected '${var.service_config.disable_dependent_services}' but got '${google_project_service.project_services["service2.googleapis.com"].disable_dependent_services}'." + } +} diff --git a/modules/github-gcloud-oidc/storage.tftest.hcl b/modules/github-gcloud-oidc/storage.tftest.hcl new file mode 100644 index 0000000..32477cc --- /dev/null +++ b/modules/github-gcloud-oidc/storage.tftest.hcl @@ -0,0 +1,188 @@ +mock_provider "github" {} +mock_provider "google" {} +mock_provider "google-beta" {} +mock_provider "github-actions-runners" {} +override_module { + target = module.oidc + outputs = { + gh-oidc = { + sa_name = "test-sa" + attribute = "attribute.repository/github-foundations/bootstrap" + } + } +} + +variables { + # required variables + github_foundations_organization_name = "github-foundations" + parent = "organizations/1234567890" + project_parent = "organizations/1234567890" + project_name = "test-project" + folder_name = "test-folder" + bucket_name = "test-bucket" + location = "US" + + # variables for this test + storage_class = "REGIONAL" + force_destroy = true + uniform_bucket_level_access = false + labels = { label1 = "value1", label2 = "value2" } + default_event_based_hold = true + requester_pays = true + versioning = true +} + +run "expect_parent_errors_test" { + variables { + parent = "invalid-parent" + } + command = plan + + expect_failures = [var.parent] +} + +run "expect_prefix_errors_test" { + variables { + prefix = "" + } + + command = plan + + expect_failures = [var.prefix] +} + +run "expect_project_parent_errors_test" { + variables { + project_parent = "invalid-parent" + } + command = plan + + expect_failures = [var.project_parent] +} + +run "expect_storage_class_errors_test" { + variables { + storage_class = "INVALID" + } + command = plan + + expect_failures = [var.storage_class] +} + +run "expect_lifecycle_rules_action_type_errors_test" { + variables { + lifecycle_rules = { + rule1 = { + action = { + type = "INVALID" + } + condition = { + age = 30 + } + } + } + } + command = plan + + expect_failures = [var.lifecycle_rules] +} + +run "expect_lifecycle_rules_storage_class_errors_test" { + variables { + lifecycle_rules = { + rule1 = { + action = { + type = "SetStorageClass" + } + condition = { + age = 30 + } + } + } + } + command = plan + + expect_failures = [var.lifecycle_rules] +} + +run "storage_bucket_test" { + + # Set some extra variables for this test + variables { + autoclass = true + website = { + main_page_suffix = "index.html" + not_found_page = "404.html" + } + encryption_key = "projects/${var.project_name}/locations/${var.location}/keyRings/${var.bucket_name}-keyring/cryptoKeys/${var.bucket_name}-key" + retention_policy = { + retention_period = 30 + is_locked = true + } + logging_config = { + log_bucket = "log-bucket" + log_object_prefix = "log-object-prefix" + } + cors = { + origin = ["*"] + method = ["GET", "POST"] + response_header = ["Content-Type"] + max_age_seconds = 3600 + } + lifecycle_rules = { + rule1 = { + action = { + type = "Delete" + storage_class = "NEARLINE" + } + condition = { + age = 30 + } + } + } + custom_placement_config = ["us-central1", "us-west1"] + } + + command = apply + + assert { + condition = google_storage_bucket.bucket.name == var.bucket_name + error_message = "The bucket name is incorrect. Expected ${var.bucket_name} but got ${google_storage_bucket.bucket.name}." + } + assert { + condition = startswith(google_storage_bucket.bucket.project, var.project_name) + error_message = "The project name is incorrect. Expected ${var.project_name}#### but got ${google_storage_bucket.bucket.project}." + } + assert { + condition = google_storage_bucket.bucket.location == var.location + error_message = "The location is incorrect. Expected ${var.location} but got ${google_storage_bucket.bucket.location}." + } + assert { + condition = google_storage_bucket.bucket.storage_class == var.storage_class + error_message = "The storage class is incorrect. Expected ${var.storage_class} but got ${google_storage_bucket.bucket.storage_class}." + } + assert { + condition = google_storage_bucket.bucket.force_destroy == var.force_destroy + error_message = "The force destroy is incorrect. Expected ${var.force_destroy} but got ${google_storage_bucket.bucket.force_destroy}." + } + assert { + condition = google_storage_bucket.bucket.uniform_bucket_level_access == var.uniform_bucket_level_access + error_message = "The uniform bucket level access is incorrect. Expected ${var.uniform_bucket_level_access} but got ${google_storage_bucket.bucket.uniform_bucket_level_access}." + } + assert { + condition = google_storage_bucket.bucket.labels.label1 == var.labels.label1 + error_message = "The label1 is incorrect. Expected ${var.labels.label1} but got ${google_storage_bucket.bucket.labels.label1}." + } + assert { + condition = google_storage_bucket.bucket.labels.label2 == var.labels.label2 + error_message = "The label2 is incorrect. Expected ${var.labels.label2} but got ${google_storage_bucket.bucket.labels.label2}." + } + assert { + condition = google_storage_bucket.bucket.default_event_based_hold == var.default_event_based_hold + error_message = "The default event based hold is incorrect. Expected ${var.default_event_based_hold} but got ${google_storage_bucket.bucket.default_event_based_hold}." + } + assert { + condition = google_storage_bucket.bucket.requester_pays == var.requester_pays + error_message = "The requester pays is incorrect. Expected ${var.requester_pays} but got ${google_storage_bucket.bucket.requester_pays}." + } +} diff --git a/modules/github-gcloud-oidc/variables.tf b/modules/github-gcloud-oidc/variables.tf index 64ad92a..5d7385b 100644 --- a/modules/github-gcloud-oidc/variables.tf +++ b/modules/github-gcloud-oidc/variables.tf @@ -76,12 +76,6 @@ variable "labels" { default = {} } -variable "skip_delete" { - description = "Allows the underlying resources to be destroyed without destroying the project itself." - type = bool - default = false -} - variable "services" { description = "Service APIs to enable." type = list(string) diff --git a/modules/github-gcloud-oidc/versions.tf b/modules/github-gcloud-oidc/versions.tf index 4658f90..821da76 100644 --- a/modules/github-gcloud-oidc/versions.tf +++ b/modules/github-gcloud-oidc/versions.tf @@ -3,11 +3,11 @@ terraform { required_providers { google = { source = "hashicorp/google" - version = ">= 3.77" # tftest + version = ">= 6.12.0" # tftest } google-beta = { source = "hashicorp/google-beta" - version = ">= 3.77" # tftest + version = ">= 6.12.0" # tftest } random = { source = "hashicorp/random" diff --git a/modules/organization/README.md b/modules/organization/README.md index 944d60f..7fa5253 100644 --- a/modules/organization/README.md +++ b/modules/organization/README.md @@ -43,11 +43,11 @@ | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| -| [actions\_secrets](#input\_actions\_secrets) | A map of organization-level GitHub Actions secrets to create. The key is the name of the secret and the value is an object describing how to create the secret. |
map(object({
encrypted_value = string
visibility = string
}))
| `{}` | no | -| [codespaces\_secrets](#input\_codespaces\_secrets) | A map of organization-level GitHub Codespaces secrets to create. The key is the name of the secret and the value is an object describing how to create the secret. |
map(object({
encrypted_value = string
visibility = string
}))
| `{}` | no | -| [custom\_repository\_roles](#input\_custom\_repository\_roles) | A map of custom repository roles to create. The key is the name of the role and the value is the role configurations. |
map(object({
description = string
base_role = string
permissions = list(string)
}))
| n/a | yes | -| [default\_branch\_protection\_rulesets](#input\_default\_branch\_protection\_rulesets) | n/a |
object({
base_protection = optional(object({
enforcement = string
}))
minimum_approvals = optional(object({
enforcement = string
approvals_required = number
}))
dismiss_stale_reviews = optional(object({
enforcement = string
}))
require_signatures = optional(object({
enforcement = string
}))
bypass_actors = optional(object({
repository_roles = optional(list(object({
role = string
always_bypass = optional(bool)
})))
teams = optional(list(object({
team = string
always_bypass = optional(bool)
})))
integrations = optional(list(object({
installation_id = number
always_bypass = optional(bool)
})))
organization_admins = optional(list(object({
user = string
always_bypass = optional(bool)
})))
}))
})
| `{}` | no | -| [dependabot\_secrets](#input\_dependabot\_secrets) | A map of organization-level Dependabot secrets to create. The key is the name of the secret and the value is an object describing how to create the secret. |
map(object({
encrypted_value = string
visibility = string
}))
| `{}` | no | +| [actions\_secrets](#input\_actions\_secrets) | A map of organization-level GitHub Actions secrets to create. The key is the name of the secret and the value is an object describing how to create the secret. |
map(object({
encrypted_value = string
visibility = string
}))
| `{}` | no | +| [codespaces\_secrets](#input\_codespaces\_secrets) | A map of organization-level GitHub Codespaces secrets to create. The key is the name of the secret and the value is an object describing how to create the secret. |
map(object({
encrypted_value = string
visibility = string
}))
| `{}` | no | +| [custom\_repository\_roles](#input\_custom\_repository\_roles) | A map of custom repository roles to create. The key is the name of the role and the value is the role configurations. |
map(object({
description = string
base_role = string
permissions = list(string)
}))
| n/a | yes | +| [default\_branch\_protection\_rulesets](#input\_default\_branch\_protection\_rulesets) | n/a |
object({
base_protection = optional(object({
enforcement = string
}))
minimum_approvals = optional(object({
enforcement = string
approvals_required = number
}))
dismiss_stale_reviews = optional(object({
enforcement = string
}))
require_signatures = optional(object({
enforcement = string
}))
bypass_actors = optional(object({
repository_roles = optional(list(object({
role = string
always_bypass = optional(bool)
})))
teams = optional(list(object({
team = string
always_bypass = optional(bool)
})))
integrations = optional(list(object({
installation_id = number
always_bypass = optional(bool)
})))
organization_admins = optional(list(object({
user = string
always_bypass = optional(bool)
})))
}))
})
| `{}` | no | +| [dependabot\_secrets](#input\_dependabot\_secrets) | A map of organization-level Dependabot secrets to create. The key is the name of the secret and the value is an object describing how to create the secret. |
map(object({
encrypted_value = string
visibility = string
}))
| `{}` | no | | [enable\_community\_manager\_role](#input\_enable\_community\_manager\_role) | If `true` will create a custom repository role for community managers. Defaults to `false`. If `true` the maximum number of `custom_repository_roles` that can be defined will be reduced by one. | `bool` | `false` | no | | [enable\_contractor\_role](#input\_enable\_contractor\_role) | If `true` will create a custom repository role for contractors. Defaults to `false`. If `true` the maximum number of `custom_repository_roles` that can be defined will be reduced by one. | `bool` | `false` | no | | [enable\_security\_engineer\_role](#input\_enable\_security\_engineer\_role) | If `true` will create a custom repository role for security engineers. Defaults to `false`. If `true` the maximum number of `custom_repository_roles` that can be defined will be reduced by one. | `bool` | `false` | no | @@ -63,10 +63,10 @@ | [github\_organization\_enable\_secret\_scanning\_push\_protection](#input\_github\_organization\_enable\_secret\_scanning\_push\_protection) | If set secret scanning push protection will be enabled for new repositories in the organization. Defaults to `true`. | `bool` | `true` | no | | [github\_organization\_location](#input\_github\_organization\_location) | Organization location. Defaults to `''`. | `string` | `""` | no | | [github\_organization\_members](#input\_github\_organization\_members) | A list of usernames to invite to the organization. Defaults to `[]`. | `list(string)` | `[]` | no | -| [github\_organization\_pages\_settings](#input\_github\_organization\_pages\_settings) | Settings for organization page creation. The default setting does not allow members to create public and private pages. |
object({
members_can_create_public = bool,
members_can_create_private = bool
})
|
{
"members_can_create_private": false,
"members_can_create_public": false
}
| no | -| [github\_organization\_repository\_settings](#input\_github\_organization\_repository\_settings) | Settings for organization repository creation. The default setting allows members to create internal and private repositories but not public. |
object({
members_can_create_public = bool,
members_can_create_internal = bool,
members_can_create_private = bool
})
|
{
"members_can_create_internal": true,
"members_can_create_private": true,
"members_can_create_public": false
}
| no | +| [github\_organization\_pages\_settings](#input\_github\_organization\_pages\_settings) | Settings for organization page creation. The default setting does not allow members to create public and private pages. |
object({
members_can_create_public = bool,
members_can_create_private = bool
})
|
{
"members_can_create_private": false,
"members_can_create_public": false
}
| no | +| [github\_organization\_repository\_settings](#input\_github\_organization\_repository\_settings) | Settings for organization repository creation. The default setting allows members to create internal and private repositories but not public. |
object({
members_can_create_public = bool,
members_can_create_internal = bool,
members_can_create_private = bool
})
|
{
"members_can_create_internal": true,
"members_can_create_private": true,
"members_can_create_public": false
}
| no | | [github\_organization\_requires\_web\_commit\_signing](#input\_github\_organization\_requires\_web\_commit\_signing) | If set commit signatures are required for commits to the organization. Defaults to `false`. | `bool` | `false` | no | -| [rulesets](#input\_rulesets) | n/a |
map(object({
bypass_actors = optional(object({
repository_roles = optional(list(object({
role = string
always_bypass = optional(bool)
})))
teams = optional(list(object({
team = string
always_bypass = optional(bool)
})))
integrations = optional(list(object({
installation_id = number
always_bypass = optional(bool)
})))
organization_admins = optional(list(object({
user = string
always_bypass = optional(bool)
})))
}))
conditions = optional(object({
ref_name = object({
include = list(string)
exclude = list(string)
})
repository_name = object({
include = list(string)
exclude = list(string)
})
}))
rules = object({
branch_name_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
tag_name_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
commit_author_email_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
commit_message_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
committer_email_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
creation = optional(bool)
deletion = optional(bool)
update = optional(bool)
non_fast_forward = optional(bool)
required_linear_history = optional(bool)
required_signatures = optional(bool)
pull_request = optional(object({
dismiss_stale_reviews_on_push = optional(bool)
require_code_owner_review = optional(bool)
require_last_push_approval = optional(bool)
required_approving_review_count = optional(number)
required_review_thread_resolution = optional(bool)
}))
required_status_checks = optional(object({
required_check = list(object({
context = string
integration_id = optional(number)
}))
strict_required_status_check_policy = optional(bool)
}))
required_workflows = optional(object({
required_workflows = list(object({
repository_id = number
path = string
ref = optional(string)
}))
}))
})
target = string
enforcement = string
}))
| `{}` | no | +| [rulesets](#input\_rulesets) | n/a |
map(object({
bypass_actors = optional(object({
repository_roles = optional(list(object({
role = string
always_bypass = optional(bool)
})))
teams = optional(list(object({
team = string
always_bypass = optional(bool)
})))
integrations = optional(list(object({
installation_id = number
always_bypass = optional(bool)
})))
organization_admins = optional(list(object({
user = string
always_bypass = optional(bool)
})))
}))
conditions = optional(object({
ref_name = object({
include = list(string)
exclude = list(string)
})
repository_name = object({
include = list(string)
exclude = list(string)
})
}))
rules = object({
branch_name_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
tag_name_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
commit_author_email_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
commit_message_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
committer_email_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
creation = optional(bool)
deletion = optional(bool)
update = optional(bool)
non_fast_forward = optional(bool)
required_linear_history = optional(bool)
required_signatures = optional(bool)
pull_request = optional(object({
dismiss_stale_reviews_on_push = optional(bool)
require_code_owner_review = optional(bool)
require_last_push_approval = optional(bool)
required_approving_review_count = optional(number)
required_review_thread_resolution = optional(bool)
}))
required_status_checks = optional(object({
required_check = list(object({
context = string
integration_id = optional(number)
}))
strict_required_status_check_policy = optional(bool)
}))
required_workflows = optional(object({
required_workflows = list(object({
repository_id = number
path = string
ref = optional(string)
}))
}))
})
target = string
enforcement = string
}))
| `{}` | no | ## Outputs diff --git a/modules/organization/block.tftest.hcl b/modules/organization/block.tftest.hcl new file mode 100644 index 0000000..597bf0d --- /dev/null +++ b/modules/organization/block.tftest.hcl @@ -0,0 +1,32 @@ +mock_provider "github" {} + +variables { + # required variables + github_organization_billing_email = "org_billing_email@focisolutions.com" + + custom_repository_roles = { + custom_role1 = { + description = "Custom role 1" + base_role = "read" + permissions = ["pull", "push"] + } + custom_role2 = { + description = "Custom role 2" + base_role = "write" + permissions = ["pull", "push", "delete"] + } + } + + # variables for this test + github_organization_blocked_users = ["user1", "user2", "user3", "user4"] +} + +run "blocked_user_test" { + + command = apply + + assert { + condition = length(github_organization_block.blocked_user) == length(var.github_organization_blocked_users) + error_message = "The number or blocked users is incorrect. Expected ${length(var.github_organization_blocked_users)} but got ${length(github_organization_block.blocked_user)}." + } +} diff --git a/modules/organization/members.tftest.hcl b/modules/organization/members.tftest.hcl new file mode 100644 index 0000000..0cfae05 --- /dev/null +++ b/modules/organization/members.tftest.hcl @@ -0,0 +1,32 @@ +mock_provider "github" {} + +variables { + # required variables + github_organization_billing_email = "org_billing_email@focisolutions.com" + + custom_repository_roles = { + custom_role1 = { + description = "Custom role 1" + base_role = "read" + permissions = ["pull", "push"] + } + custom_role2 = { + description = "Custom role 2" + base_role = "write" + permissions = ["pull", "push", "delete"] + } + } + + # variables for this test + github_organization_members = ["user1", "user2", "user3", "user4", "user5"] +} + +run "members_test" { + + command = apply + + assert { + condition = length(github_membership.membership_for_user) == length(var.github_organization_members) + error_message = "The number or members is incorrect. Expected ${length(var.github_organization_members)} but got ${length(github_membership.membership_for_user)}." + } +} diff --git a/modules/organization/organization.tftest.hcl b/modules/organization/organization.tftest.hcl new file mode 100644 index 0000000..d1f7483 --- /dev/null +++ b/modules/organization/organization.tftest.hcl @@ -0,0 +1,36 @@ +mock_provider "github" {} + +variables { + # required variables + github_organization_billing_email = "org_billing_email@focisolutions.com" + + custom_repository_roles = { + custom_role1 = { + description = "Custom role 1" + base_role = "read" + permissions = ["pull", "push"] + } + custom_role2 = { + description = "Custom role 2" + base_role = "write" + permissions = ["pull", "push", "delete"] + } + } + + # variables for this test + enable_security_engineer_role = true + enable_contractor_role = false + enable_community_manager_role = true + + github_organization_enable_ghas = false +} + +run "custom_repository_role_test" { + + command = apply + + assert { + condition = length(output.custom_role_ids) == length(var.custom_repository_roles) + error_message = "The number or roles is incorrect. Expected ${length(var.custom_repository_roles)} but got ${length(output.custom_role_ids)}." + } +} diff --git a/modules/organization/roles.tf b/modules/organization/roles.tf index f4427ac..169b1c5 100644 --- a/modules/organization/roles.tf +++ b/modules/organization/roles.tf @@ -8,7 +8,7 @@ resource "github_organization_custom_role" "custom_repository_role" { lifecycle { precondition { condition = length(var.custom_repository_roles) <= 5 - (var.enable_security_engineer_role ? 1 : 0) - (var.enable_contractor_role ? 1 : 0) - (var.enable_community_manager_role ? 1 : 0) - error_message = "To many custom repository roles defined, an orrganization's maximum is 5. This limit is reduced by one for each of the following variables that are set to true: `enable_security_engineer_role`, `enable_contractor_role`, `enable_community_manager_role`." + error_message = "To many custom repository roles defined, an organization's maximum is 5. This limit is reduced by one for each of the following variables that are set to true: `enable_security_engineer_role`, `enable_contractor_role`, `enable_community_manager_role`." } } } diff --git a/modules/organization/roles.tftest.hcl b/modules/organization/roles.tftest.hcl new file mode 100644 index 0000000..fb89f40 --- /dev/null +++ b/modules/organization/roles.tftest.hcl @@ -0,0 +1,121 @@ +mock_provider "github" {} + +variables { + # required variables + github_organization_billing_email = "org_billing_email@focisolutions.com" + + custom_repository_roles = { + custom_role1 = { + description = "Custom role 1" + base_role = "read" + permissions = ["pull", "push"] + } + custom_role2 = { + description = "Custom role 2" + base_role = "write" + permissions = ["pull", "push", "delete"] + } + } + + # variables for this test + enable_security_engineer_role = true + enable_contractor_role = true + enable_community_manager_role = true +} + +run "custom_repository_role_test" { + + command = apply + + assert { + condition = length(github_organization_custom_role.custom_repository_role) == length(var.custom_repository_roles) + error_message = "The number or roles is incorrect. Expected ${length(var.custom_repository_roles)} but got ${length(github_organization_custom_role.custom_repository_role)}." + } +} + +run "custom_repository_role_failure_test" { + + # too many custom roles - expect failure + variables { + custom_repository_roles = { + custom_role1 = { + description = "Custom role 1" + base_role = "read" + permissions = ["pull", "push"] + } + custom_role2 = { + description = "Custom role 2" + base_role = "write" + permissions = ["pull", "push", "delete"] + } + custom_role3 = { + description = "Custom role 3" + base_role = "maintain" + permissions = ["pull", "push", "delete", "admin"] + } + } + } + + command = plan + expect_failures = [github_organization_custom_role.custom_repository_role] +} + +run "security_engineer_role_test" { + + assert { + condition = github_organization_custom_role.security_engineer_role[0] != null + error_message = "The security engineer role was not created." + } + assert { + condition = github_organization_custom_role.security_engineer_role[0].name == "Security Engineer" + error_message = "The security engineer role name is incorrect. Expected 'Security Engineer' but got '${github_organization_custom_role.security_engineer_role[0].name}'." + } + assert { + condition = github_organization_custom_role.security_engineer_role[0].base_role == "maintain" + error_message = "The security engineer role base role is incorrect. Expected 'maintain' but got '${github_organization_custom_role.security_engineer_role[0].base_role}'." + } + assert { + condition = length(github_organization_custom_role.security_engineer_role[0].permissions) == 2 + error_message = "The security engineer role permissions are incorrect. Expected 2 but got ${length(github_organization_custom_role.security_engineer_role[0].permissions)}." + } +} + +run "contractor_role_test" { + + assert { + condition = github_organization_custom_role.contractor_role[0] != null + error_message = "The contractor role was not created." + } + assert { + condition = github_organization_custom_role.contractor_role[0].name == "Contractor" + error_message = "The contractor role name is incorrect. Expected 'Contractor' but got '${github_organization_custom_role.contractor_role[0].name}'." + } + assert { + condition = github_organization_custom_role.contractor_role[0].base_role == "write" + error_message = "The contractor role base role is incorrect. Expected 'write' but got '${github_organization_custom_role.contractor_role[0].base_role}'." + } + assert { + condition = length(github_organization_custom_role.contractor_role[0].permissions) == 1 + error_message = "The contractor role permissions are incorrect. Expected 1 but got ${length(github_organization_custom_role.contractor_role[0].permissions)}." + } +} + +run "community_manager_role_test" { + + assert { + condition = github_organization_custom_role.community_manager_role[0] != null + error_message = "The community manager role was not created." + } + assert { + condition = github_organization_custom_role.community_manager_role[0].name == "Community Manager" + error_message = "The community manager role name is incorrect. Expected 'Community Manager' but got '${github_organization_custom_role.community_manager_role[0].name}'." + } + assert { + condition = github_organization_custom_role.community_manager_role[0].base_role == "read" + error_message = "The community manager role base role is incorrect. Expected 'read' but got '${github_organization_custom_role.community_manager_role[0].base_role}'." + } + assert { + condition = length(github_organization_custom_role.community_manager_role[0].permissions) == 13 + error_message = "The community manager role permissions are incorrect. Expected 13 but got ${length(github_organization_custom_role.community_manager_role[0].permissions)}." + } +} diff --git a/modules/organization/rulesets.tftest.hcl b/modules/organization/rulesets.tftest.hcl new file mode 100644 index 0000000..41f02ec --- /dev/null +++ b/modules/organization/rulesets.tftest.hcl @@ -0,0 +1,186 @@ +mock_provider "github" {} + +# We need to mock some data sources in this teset +override_data { + target = data.github_team.branch_ruleset_bypasser["team1"] + values = { + id = 2 + } +} + +override_data { + target = data.github_user.branch_ruleset_bypasser["user5"] + values = { + id = 5 + } +} + +variables { + github_organization_billing_email = "org_billing_email@focisolutions.com" + + custom_repository_roles = { + custom_role1 = { + description = "Custom role 1" + base_role = "read" + permissions = ["pull", "push"] + } + } + + default_branch_protection_rulesets = { + base_protection = { + enforcement = "evaluate" + } + minimum_approvals = { + enforcement = "active" + approvals_required = 1 + } + dismiss_stale_reviews = { + enforcement = "disabled" + } + require_signatures = { + enforcement = "evaluate" + } + bypass_actors = { + repository_roles = [ + { + role = "maintain" + always_bypass = false + } + ] + teams = [ + { + team = "team1" + always_bypass = true + } + ] + integrations = [ + { + installation_id = 333333 + always_bypass = false + } + ] + organization_admins = [ + { + user = "user5" + always_bypass = true + } + ] + } + } + + rulesets = { + ruleset1 = { + target = "branch" + enforcement = "evaluate" + + bypass_actors = { + repository_roles = [ + { + role = "maintain" + always_bypass = false + } + ] + teams = [ + { + team = "team1" + always_bypass = true + } + ] + integrations = [ + { + installation_id = 333333 + always_bypass = false + } + ] + organization_admins = [ + { + user = "user5" + always_bypass = true + } + ] + } + conditions = { + ref_name = { + include = ["main"] + exclude = ["feature/*"] + } + repository_name = { + include = ["repo1"] + exclude = ["repo2"] + } + } + rules = { + branch_name_pattern = { + operator = "regex" + pattern = "main" + name = "branch_name_pattern" + negate = false + } + commit_author_email_pattern = { + operator = "regex" + pattern = "pattern" + name = "commit_author_email_pattern" + negate = false + } + commit_message_pattern = { + operator = "regex" + pattern = "pattern" + name = "commit_message_pattern" + negate = false + } + committer_email_pattern = { + operator = "regex" + pattern = "pattern" + name = "committer_email_pattern" + negate = false + } + creation = true + deletion = false + update = true + non_fast_forward = false + required_linear_history = true + required_signatures = false + update_allows_fetch_and_merge = true + } + } + } +} + +run "ruleset_test" { + command = apply + + assert { + condition = module.ruleset != null + error_message = "The ruleset is null" + } +} + +# This is crashing Terraform (v.1.9.8) +# run "base_default_branch_protection_ruleset_test" { + +# assert { +# condition = module.base_default_branch_protection != null +# error_message = "The base_default_branch_protection is null" +# } +# } + +# run "minimum_approvals_test" { +# assert { +# condition = module.minimum_approvals != null +# error_message = "The minimum_approvals is null" +# } +# } + +# run "dismiss_stale_reviews_test" { +# assert { +# condition = module.dismiss_stale_reviews != null +# error_message = "The dismiss_stale_reviews is null" +# } +# } + +# run "require_signatures_test" { +# assert { +# condition = module.require_signatures != null +# error_message = "The require_signatures is null" +# } +# } diff --git a/modules/organization/secrets.tftest.hcl b/modules/organization/secrets.tftest.hcl new file mode 100644 index 0000000..acfaa83 --- /dev/null +++ b/modules/organization/secrets.tftest.hcl @@ -0,0 +1,157 @@ +mock_provider "github" {} + +variables { + github_organization_billing_email = "org_billing_email@focisolutions.com" + + custom_repository_roles = { + custom_role1 = { + description = "Custom role 1" + base_role = "read" + permissions = ["pull", "push"] + } + } + + actions_secrets = { + action_secret1 = { + encrypted_value = "dmFsdWUxCg==" # base64 encoded + visibility = "all" + }, + action_secret2 = { + encrypted_value = "dmFsdWUyCg==" # base64 encoded + visibility = "private" + } + + } + + codespaces_secrets = { + codespace_secret1 = { + encrypted_value = "dmFsdWUxCg==" # base64 encoded + visibility = "all" + } + } + + dependabot_secrets = { + dependabot_secret1 = { + encrypted_value = "dmFsdWUxCg==" # base64 encoded + visibility = "all" + }, + dependabot_secret2 = { + encrypted_value = "dmFsdWUyCg==" # base64 encoded + visibility = "private" + }, + dependabot_secret3 = { + encrypted_value = "dmFsdWUzCg==" # base64 encoded + visibility = "private" + } + } +} + +run "action_secret_test" { + command = apply + + assert { + condition = github_actions_organization_secret.action_secret["action_secret1"].secret_name == "action_secret1" + error_message = "The action secret name is incorrect. Expected `action_secret1`, got `${github_actions_organization_secret.action_secret["action_secret1"].secret_name}`" + } + assert { + condition = github_actions_organization_secret.action_secret["action_secret1"].encrypted_value == "dmFsdWUxCg==" + error_message = "The action secret encrypted value is incorrect. Expected `dmFsdWUxCg==`, got `${nonsensitive(github_actions_organization_secret.action_secret["action_secret1"].encrypted_value)}`" + } + assert { + condition = github_actions_organization_secret.action_secret["action_secret1"].visibility == "all" + error_message = "The action secret visibility is incorrect. Expected `all`, got `${github_actions_organization_secret.action_secret["action_secret1"].visibility}`" + } + assert { + condition = length(github_actions_organization_secret.action_secret["action_secret1"].selected_repository_ids) == 0 + error_message = "The action secret selected repository ids is incorrect. Expected `0`, got `${length(github_actions_organization_secret.action_secret["action_secret1"].selected_repository_ids)}`" + } + assert { + condition = github_actions_organization_secret.action_secret["action_secret2"].secret_name == "action_secret2" + error_message = "The action secret name is incorrect. Expected `action_secret2`, got `${github_actions_organization_secret.action_secret["action_secret2"].secret_name}`" + } + assert { + condition = github_actions_organization_secret.action_secret["action_secret2"].encrypted_value == "dmFsdWUyCg==" + error_message = "The action secret encrypted value is incorrect. Expected `dmFsdWUyCg==`, got `${nonsensitive(github_actions_organization_secret.action_secret["action_secret2"].encrypted_value)}`" + } + assert { + condition = github_actions_organization_secret.action_secret["action_secret2"].visibility == "private" + error_message = "The action secret visibility is incorrect. Expected `private`, got `${github_actions_organization_secret.action_secret["action_secret2"].visibility}`" + } + assert { + condition = length(github_actions_organization_secret.action_secret["action_secret2"].selected_repository_ids) == 0 + error_message = "The action secret selected repository ids is incorrect. Expected `0`, got `${length(github_actions_organization_secret.action_secret["action_secret2"].selected_repository_ids)}`" + } +} + +run "codespace_secret_test" { + + assert { + condition = github_codespaces_organization_secret.codespace_secret["codespace_secret1"].secret_name == "codespace_secret1" + error_message = "The codespace secret name is incorrect. Expected `codespace_secret1`, got `${github_codespaces_organization_secret.codespace_secret["codespace_secret1"].secret_name}`" + } + assert { + condition = github_codespaces_organization_secret.codespace_secret["codespace_secret1"].encrypted_value == "dmFsdWUxCg==" + error_message = "The codespace secret encrypted value is incorrect. Expected `dmFsdWUxCg==`, got `${nonsensitive(github_codespaces_organization_secret.codespace_secret["codespace_secret1"].encrypted_value)}`" + } + assert { + condition = github_codespaces_organization_secret.codespace_secret["codespace_secret1"].visibility == "all" + error_message = "The codespace secret visibility is incorrect. Expected `all`, got `${github_codespaces_organization_secret.codespace_secret["codespace_secret1"].visibility}`" + } + assert { + condition = length(github_codespaces_organization_secret.codespace_secret["codespace_secret1"].selected_repository_ids) == 0 + error_message = "The codespace secret selected repository ids is incorrect. Expected `0`, got `${length(github_codespaces_organization_secret.codespace_secret["codespace_secret1"].selected_repository_ids)}`" + } +} + +run "dependabot_secret_test" { + assert { + condition = github_dependabot_organization_secret.dependabot_secret["dependabot_secret1"].secret_name == "dependabot_secret1" + error_message = "The dependabot secret name is incorrect. Expected `dependabot_secret1`, got `${github_dependabot_organization_secret.dependabot_secret["dependabot_secret1"].secret_name}`" + } + assert { + condition = github_dependabot_organization_secret.dependabot_secret["dependabot_secret1"].encrypted_value == "dmFsdWUxCg==" + error_message = "The dependabot secret encrypted value is incorrect. Expected `dmFsdWUxCg==`, got `${nonsensitive(github_dependabot_organization_secret.dependabot_secret["dependabot_secret1"].encrypted_value)}`" + } + assert { + condition = github_dependabot_organization_secret.dependabot_secret["dependabot_secret1"].visibility == "all" + error_message = "The dependabot secret visibility is incorrect. Expected `all`, got `${github_dependabot_organization_secret.dependabot_secret["dependabot_secret1"].visibility}`" + } + assert { + condition = length(github_dependabot_organization_secret.dependabot_secret["dependabot_secret1"].selected_repository_ids) == 0 + error_message = "The dependabot secret selected repository ids is incorrect. Expected `0`, got `${length(github_dependabot_organization_secret.dependabot_secret["dependabot_secret1"].selected_repository_ids)}`" + } + # test the second secret + assert { + condition = github_dependabot_organization_secret.dependabot_secret["dependabot_secret2"].secret_name == "dependabot_secret2" + error_message = "The dependabot secret name is incorrect. Expected `dependabot_secret2`, got `${github_dependabot_organization_secret.dependabot_secret["dependabot_secret2"].secret_name}`" + } + assert { + condition = github_dependabot_organization_secret.dependabot_secret["dependabot_secret2"].encrypted_value == "dmFsdWUyCg==" + error_message = "The dependabot secret encrypted value is incorrect. Expected `dmFsdWUyCg==`, got `${nonsensitive(github_dependabot_organization_secret.dependabot_secret["dependabot_secret2"].encrypted_value)}`" + } + assert { + condition = github_dependabot_organization_secret.dependabot_secret["dependabot_secret2"].visibility == "private" + error_message = "The dependabot secret visibility is incorrect. Expected `private`, got `${github_dependabot_organization_secret.dependabot_secret["dependabot_secret2"].visibility}`" + } + assert { + condition = length(github_dependabot_organization_secret.dependabot_secret["dependabot_secret2"].selected_repository_ids) == 0 + error_message = "The dependabot secret selected repository ids is incorrect. Expected `0`, got `${length(github_dependabot_organization_secret.dependabot_secret["dependabot_secret2"].selected_repository_ids)}`" + } + # test the third secret + assert { + condition = github_dependabot_organization_secret.dependabot_secret["dependabot_secret3"].secret_name == "dependabot_secret3" + error_message = "The dependabot secret name is incorrect. Expected `dependabot_secret3`, got `${github_dependabot_organization_secret.dependabot_secret["dependabot_secret3"].secret_name}`" + } + assert { + condition = github_dependabot_organization_secret.dependabot_secret["dependabot_secret3"].encrypted_value == "dmFsdWUzCg==" + error_message = "The dependabot secret encrypted value is incorrect. Expected `dmFsdWUzCg==`, got `${nonsensitive(github_dependabot_organization_secret.dependabot_secret["dependabot_secret3"].encrypted_value)}`" + } + assert { + condition = github_dependabot_organization_secret.dependabot_secret["dependabot_secret3"].visibility == "private" + error_message = "The dependabot secret visibility is incorrect. Expected `private`, got `${github_dependabot_organization_secret.dependabot_secret["dependabot_secret3"].visibility}`" + } + assert { + condition = length(github_dependabot_organization_secret.dependabot_secret["dependabot_secret3"].selected_repository_ids) == 0 + error_message = "The dependabot secret selected repository ids is incorrect. Expected `0`, got `${length(github_dependabot_organization_secret.dependabot_secret["dependabot_secret3"].selected_repository_ids)}`" + } +} diff --git a/modules/organization/settings.tf b/modules/organization/settings.tf index ebd9bc0..74370c0 100644 --- a/modules/organization/settings.tf +++ b/modules/organization/settings.tf @@ -25,7 +25,7 @@ resource "github_organization_settings" "organization_settings" { members_can_create_public_pages = var.github_organization_pages_settings.members_can_create_public members_can_create_private_pages = var.github_organization_pages_settings.members_can_create_private - #Oranization Repository settings + #Organization Repository settings members_can_create_repositories = local.members_can_create_repositories members_can_create_public_repositories = var.github_organization_repository_settings.members_can_create_public members_can_create_internal_repositories = var.github_organization_repository_settings.members_can_create_internal diff --git a/modules/organization/settings.tftest.hcl b/modules/organization/settings.tftest.hcl new file mode 100644 index 0000000..4c079b7 --- /dev/null +++ b/modules/organization/settings.tftest.hcl @@ -0,0 +1,134 @@ +mock_provider "github" {} + +variables { + github_organization_billing_email = "org_billing_email@focisolutions.com" + github_organization_email = "org_email@focisolutions.com" + github_organization_blog = "org_blog" + github_organization_location = "org_location" + github_organization_requires_web_commit_signing = false + + github_organization_enable_ghas = true + github_organization_enable_dependabot_alerts = true + github_organization_enable_dependabot_updates = true + github_organization_enable_dependancy_graph = true + github_organization_enable_secret_scanning = true + github_organization_enable_secret_scanning_push_protection = true + + github_organization_pages_settings = { + members_can_create_public = true + members_can_create_private = true + } + + github_organization_repository_settings = { + members_can_create_public = true + members_can_create_internal = true + members_can_create_private = true + } + + custom_repository_roles = { + custom_role1 = { + description = "Custom role 1" + base_role = "read" + permissions = ["pull", "push"] + } + } +} + +run "organization_settings_test" { + command = apply + + assert { + condition = github_organization_settings.organization_settings.billing_email == var.github_organization_billing_email + error_message = "The billing email is not set correctly. Expected: ${var.github_organization_billing_email}, got: ${github_organization_settings.organization_settings.billing_email}" + } + assert { + condition = github_organization_settings.organization_settings.email == var.github_organization_email + error_message = "The email is not set correctly. Expected: ${var.github_organization_email}, got: ${github_organization_settings.organization_settings.email}" + } + assert { + condition = github_organization_settings.organization_settings.blog == var.github_organization_blog + error_message = "The blog is not set correctly. Expected: ${var.github_organization_blog}, got: ${github_organization_settings.organization_settings.blog}" + } + assert { + condition = github_organization_settings.organization_settings.location == var.github_organization_location + error_message = "The location is not set correctly. Expected: ${var.github_organization_location}, got: ${github_organization_settings.organization_settings.location}" + } + assert { + condition = github_organization_settings.organization_settings.web_commit_signoff_required == var.github_organization_requires_web_commit_signing + error_message = "The web commit signoff required is not set correctly. Expected: ${var.github_organization_requires_web_commit_signing}, got: ${github_organization_settings.organization_settings.web_commit_signoff_required}" + } + assert { + condition = github_organization_settings.organization_settings.has_organization_projects == true + error_message = "The organization projects are not enabled. Expected: true, got: ${github_organization_settings.organization_settings.has_organization_projects}" + } + assert { + condition = github_organization_settings.organization_settings.has_repository_projects == true + error_message = "The repository projects are not enabled. Expected: true, got: ${github_organization_settings.organization_settings.has_repository_projects}" + } + + # Github advance security, dependabot, and secret scanning + assert { + condition = github_organization_settings.organization_settings.advanced_security_enabled_for_new_repositories == var.github_organization_enable_ghas + error_message = "The advance security is not set correctly. Expected: ${var.github_organization_enable_ghas}, got: ${github_organization_settings.organization_settings.advanced_security_enabled_for_new_repositories}" + } + assert { + condition = github_organization_settings.organization_settings.dependabot_alerts_enabled_for_new_repositories == var.github_organization_enable_dependabot_alerts + error_message = "The dependabot alerts are not enabled. Expected: ${var.github_organization_enable_dependabot_alerts}, got: ${github_organization_settings.organization_settings.dependabot_alerts_enabled_for_new_repositories}" + } + assert { + condition = github_organization_settings.organization_settings.dependabot_security_updates_enabled_for_new_repositories == var.github_organization_enable_dependabot_updates + error_message = "The dependabot security updates are not enabled. Expected: ${var.github_organization_enable_dependabot_updates}, got: ${github_organization_settings.organization_settings.dependabot_security_updates_enabled_for_new_repositories}" + } + assert { + condition = github_organization_settings.organization_settings.dependency_graph_enabled_for_new_repositories == var.github_organization_enable_dependancy_graph + error_message = "The dependency graph is not enabled. Expected: ${var.github_organization_enable_dependancy_graph}, got: ${github_organization_settings.organization_settings.dependency_graph_enabled_for_new_repositories}" + } + assert { + condition = github_organization_settings.organization_settings.secret_scanning_enabled_for_new_repositories == var.github_organization_enable_secret_scanning + error_message = "The secret scanning is not enabled. Expected: ${var.github_organization_enable_secret_scanning}, got: ${github_organization_settings.organization_settings.secret_scanning_enabled_for_new_repositories}" + } + assert { + condition = github_organization_settings.organization_settings.secret_scanning_push_protection_enabled_for_new_repositories == var.github_organization_enable_secret_scanning_push_protection + error_message = "The secret scanning push protection is not enabled. Expected: ${var.github_organization_enable_secret_scanning_push_protection}, got: ${github_organization_settings.organization_settings.secret_scanning_push_protection_enabled_for_new_repositories}" + } + + #Organization pages + assert { + condition = github_organization_settings.organization_settings.members_can_create_pages == true + error_message = "The members can create pages is not enabled. Expected: true, got: ${github_organization_settings.organization_settings.members_can_create_pages}" + } + assert { + condition = github_organization_settings.organization_settings.members_can_create_public_pages == var.github_organization_pages_settings.members_can_create_public + error_message = "The members can create public pages is not enabled. Expected: ${var.github_organization_pages_settings.members_can_create_public}, got: ${github_organization_settings.organization_settings.members_can_create_public_pages}" + } + assert { + condition = github_organization_settings.organization_settings.members_can_create_private_pages == var.github_organization_pages_settings.members_can_create_private + error_message = "The members can create private pages is not enabled. Expected: ${var.github_organization_pages_settings.members_can_create_private}, got: ${github_organization_settings.organization_settings.members_can_create_private_pages}" + } + + #Organization Repository settings + assert { + condition = github_organization_settings.organization_settings.members_can_create_repositories == true + error_message = "The members can create repositories is not enabled. Expected: true, got: ${github_organization_settings.organization_settings.members_can_create_repositories}" + } + assert { + condition = github_organization_settings.organization_settings.members_can_create_public_repositories == var.github_organization_repository_settings.members_can_create_public + error_message = "The members can create public repositories is not enabled. Expected: ${var.github_organization_repository_settings.members_can_create_public}, got: ${github_organization_settings.organization_settings.members_can_create_public_repositories}" + } + assert { + condition = github_organization_settings.organization_settings.members_can_create_internal_repositories == var.github_organization_repository_settings.members_can_create_internal + error_message = "The members can create internal repositories is not enabled. Expected: ${var.github_organization_repository_settings.members_can_create_internal}, got: ${github_organization_settings.organization_settings.members_can_create_internal_repositories}" + } + assert { + condition = github_organization_settings.organization_settings.members_can_create_private_repositories == var.github_organization_repository_settings.members_can_create_private + error_message = "The members can create private repositories is not enabled. Expected: ${var.github_organization_repository_settings.members_can_create_private}, got: ${github_organization_settings.organization_settings.members_can_create_private_repositories}" + } + assert { + condition = github_organization_settings.organization_settings.default_repository_permission == "none" + error_message = "The default repository permission is not set correctly. Expected: none, got: ${github_organization_settings.organization_settings.default_repository_permission}" + } + assert { + condition = github_organization_settings.organization_settings.members_can_fork_private_repositories == false + error_message = "The members can fork private repositories is not set correctly. Expected: false, got: ${github_organization_settings.organization_settings.members_can_fork_private_repositories}" + } +} diff --git a/modules/private_repository/README.md b/modules/private_repository/README.md index bec2428..dd33f99 100644 --- a/modules/private_repository/README.md +++ b/modules/private_repository/README.md @@ -35,22 +35,22 @@ No resources. | [dependabot\_secrets](#input\_dependabot\_secrets) | An (Optional) map of Dependabot secrets to create for this repository. The key is the name of the secret and the value is the encrypted value. | `map(string)` | `{}` | no | | [dependabot\_security\_updates](#input\_dependabot\_security\_updates) | Enables dependabot security updates. Only works when `has_vulnerability_alerts` is set because that is required to enable dependabot for the repository. | `bool` | `true` | no | | [description](#input\_description) | The description to give to the repository. Defaults to `""` | `string` | `""` | no | -| [environments](#input\_environments) | Environments to create for the repository. |
map(object({
wait_timer = optional(number)
can_admins_bypass = optional(bool)
prevent_self_review = optional(bool)
action_secrets = optional(map(string))
reviewers = optional(object({
teams = optional(list(string))
users = optional(list(string))
}))
deployment_branch_policy = optional(object({
protected_branches = bool
custom_branch_policies = bool
branch_patterns = list(string)
}))
}))
| `{}` | no | +| [environments](#input\_environments) | Environments to create for the repository. |
map(object({
wait_timer = optional(number)
can_admins_bypass = optional(bool)
prevent_self_review = optional(bool)
action_secrets = optional(map(string))
reviewers = optional(object({
teams = optional(list(string))
users = optional(list(string))
}))
deployment_branch_policy = optional(object({
protected_branches = bool
custom_branch_policies = bool
branch_patterns = list(string)
}))
}))
| `{}` | no | | [has\_ghas\_license](#input\_has\_ghas\_license) | If the organization owning the repository has a GitHub Advanced Security license or not. Defaults to false. | `bool` | `false` | no | | [homepage](#input\_homepage) | The homepage for the repository | `string` | `""` | no | | [license\_template](#input\_license\_template) | The (Optional) license template to use for the repository | `string` | `null` | no | | [merge\_commit\_message](#input\_merge\_commit\_message) | (Optional) Can be `PR_BODY`, `PR_TITLE`, or `BLANK` for a default merge commit message. Applicable only if allow\_merge\_commit is `true`. | `string` | `"PR_TITLE"` | no | | [merge\_commit\_title](#input\_merge\_commit\_title) | (Optional) Can be `PR_TITLE` or `MERGE_MESSAGE` for a default merge commit title. Applicable only if allow\_merge\_commit is `true`. | `string` | `"MERGE_MESSAGE"` | no | | [name](#input\_name) | The name of the repository to create/import. | `string` | n/a | yes | -| [pages](#input\_pages) | The (Optional) configuration for GitHub Pages for the repository |
object({
source = optional(object({
branch = string
path = optional(string)
}))
build_type = optional(string)
cname = optional(string)
})
| `null` | no | -| [protected\_branches](#input\_protected\_branches) | A list of ref names or patterns that should be protected. Defaults `["main"]` | `list(string)` |
[
"main"
]
| no | +| [pages](#input\_pages) | The (Optional) configuration for GitHub Pages for the repository |
object({
source = optional(object({
branch = string
path = optional(string)
}))
build_type = optional(string)
cname = optional(string)
})
| `null` | no | +| [protected\_branches](#input\_protected\_branches) | A list of ref names or patterns that should be protected. Defaults `["main"]` | `list(string)` |
[
"main"
]
| no | | [repository\_team\_permissions](#input\_repository\_team\_permissions) | A map where the keys are github team slugs and the value is the permissions the team should have in the repository | `map(string)` | n/a | yes | | [repository\_user\_permissions](#input\_repository\_user\_permissions) | A map where the keys are github usernames and the value is the permissions the user should have in the repository | `map(string)` | n/a | yes | | [requires\_web\_commit\_signing](#input\_requires\_web\_commit\_signing) | If set commit signatures are required for commits to the organization. Defaults to `false`. | `bool` | `false` | no | -| [rulesets](#input\_rulesets) | n/a |
map(object({
bypass_actors = optional(object({
repository_roles = optional(list(object({
role = string
always_bypass = optional(bool)
})))
teams = optional(list(object({
team = string
always_bypass = optional(bool)
})))
integrations = optional(list(object({
installation_id = number
always_bypass = optional(bool)
})))
organization_admins = optional(list(object({
user = string
always_bypass = optional(bool)
})))
}))
conditions = optional(object({
ref_name = object({
include = list(string)
exclude = list(string)
})
}))
rules = object({
branch_name_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
tag_name_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
commit_author_email_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
commit_message_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
committer_email_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
creation = optional(bool)
deletion = optional(bool)
update = optional(bool)
non_fast_forward = optional(bool)
required_linear_history = optional(bool)
required_signatures = optional(bool)
update_allows_fetch_and_merge = optional(bool)
pull_request = optional(object({
dismiss_stale_reviews_on_push = optional(bool)
require_code_owner_review = optional(bool)
require_last_push_approval = optional(bool)
required_approving_review_count = optional(number)
required_review_thread_resolution = optional(bool)
}))
required_status_checks = optional(object({
required_check = list(object({
context = string
integration_id = optional(number)
}))
strict_required_status_check_policy = optional(bool)
}))
required_deployment_environments = optional(list(string))
})
target = string
enforcement = string
}))
| `{}` | no | +| [rulesets](#input\_rulesets) | n/a |
map(object({
bypass_actors = optional(object({
repository_roles = optional(list(object({
role = string
always_bypass = optional(bool)
})))
teams = optional(list(object({
team = string
always_bypass = optional(bool)
})))
integrations = optional(list(object({
installation_id = number
always_bypass = optional(bool)
})))
organization_admins = optional(list(object({
user = string
always_bypass = optional(bool)
})))
}))
conditions = optional(object({
ref_name = object({
include = list(string)
exclude = list(string)
})
}))
rules = object({
branch_name_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
tag_name_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
commit_author_email_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
commit_message_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
committer_email_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
creation = optional(bool)
deletion = optional(bool)
update = optional(bool)
non_fast_forward = optional(bool)
required_linear_history = optional(bool)
required_signatures = optional(bool)
update_allows_fetch_and_merge = optional(bool)
pull_request = optional(object({
dismiss_stale_reviews_on_push = optional(bool)
require_code_owner_review = optional(bool)
require_last_push_approval = optional(bool)
required_approving_review_count = optional(number)
required_review_thread_resolution = optional(bool)
}))
required_status_checks = optional(object({
required_check = list(object({
context = string
integration_id = optional(number)
}))
strict_required_status_check_policy = optional(bool)
}))
required_deployment_environments = optional(list(string))
})
target = string
enforcement = string
}))
| `{}` | no | | [squash\_merge\_commit\_message](#input\_squash\_merge\_commit\_message) | (Optional) Can be `PR_BODY`, `COMMIT_MESSAGES`, or `BLANK` for a default squash merge commit message. Applicable only if allow\_squash\_merge is `true`. | `string` | `"PR_BODY"` | no | | [squash\_merge\_commit\_title](#input\_squash\_merge\_commit\_title) | (Optional) Can be `PR_TITLE` or `COMMIT_OR_PR_TITLE` for a default squash merge commit title. Applicable only if allow\_squash\_merge is `true`. | `string` | `"PR_TITLE"` | no | -| [template\_repository](#input\_template\_repository) | A (Optional) list of template repositories to use for the repository |
object({
owner = string
repository = string
include_all_branches = bool
})
| `null` | no | +| [template\_repository](#input\_template\_repository) | A (Optional) list of template repositories to use for the repository |
object({
owner = string
repository = string
include_all_branches = bool
})
| `null` | no | | [topics](#input\_topics) | The topics to apply to the repository | `list(string)` | `[]` | no | ## Outputs diff --git a/modules/private_repository/repository.tftest.hcl b/modules/private_repository/repository.tftest.hcl new file mode 100644 index 0000000..ef1650c --- /dev/null +++ b/modules/private_repository/repository.tftest.hcl @@ -0,0 +1,65 @@ +mock_provider "github" {} + +variables { + name = "github-foundations-modules" + description = "A collection of terraform modules used in the Github Foundations framework." + visibility = "public" + has_downloads = true + has_issues = true + has_projects = true + has_wiki = true + has_discussions = true + has_vulnerability_alerts = true + topics = ["terraform", "github", "foundations"] + homepage = "myhomepage" + delete_head_on_merge = false + allow_auto_merge = true + allow_squash_merge = false + squash_merge_commit_message = "COMMIT_MESSAGES" + squash_merge_commit_title = "COMMIT_OR_PR_TITLE" + allow_merge_commit = false + merge_commit_message = "PR_BODY" + merge_commit_title = "PR_TITLE" + allow_rebase_merge = true + requires_web_commit_signing = false + license_template = "mit" + dependabot_security_updates = true + advance_security = true + secret_scanning = true + secret_scanning_on_push = true + + default_branch = "main" + protected_branches = ["main", "develop"] + + template_repository = { + owner = "owner" + repository = "template_repository" + include_all_branches = true + } + + pages = { + source = { + branch = "main" + path = "path" + } + cname = "cname" + } + + repository_team_permissions = { + repo_team1 = "push" + repo_team2 = "admin" + } + repository_user_permissions = { + user1 = "push" + user2 = "admin" + } +} + +run "create_test" { + command = apply + + assert { + condition = module.repository_base.id != null + error_message = "The repository was not created" + } +} diff --git a/modules/public_repository/README.md b/modules/public_repository/README.md index 1c4157a..5fc5ef1 100644 --- a/modules/public_repository/README.md +++ b/modules/public_repository/README.md @@ -35,21 +35,21 @@ No resources. | [dependabot\_secrets](#input\_dependabot\_secrets) | An (Optional) map of Dependabot secrets to create for this repository. The key is the name of the secret and the value is the encrypted value. | `map(string)` | `{}` | no | | [dependabot\_security\_updates](#input\_dependabot\_security\_updates) | Enables dependabot security updates. Only works when `has_vulnerability_alerts` is set because that is required to enable dependabot for the repository. | `bool` | `true` | no | | [description](#input\_description) | The description to give to the repository. Defaults to `""` | `string` | `""` | no | -| [environments](#input\_environments) | Environments to create for the repository. |
map(object({
wait_timer = optional(number)
can_admins_bypass = optional(bool)
prevent_self_review = optional(bool)
action_secrets = optional(map(string))
reviewers = optional(object({
teams = optional(list(string))
users = optional(list(string))
}))
deployment_branch_policy = optional(object({
protected_branches = bool
custom_branch_policies = bool
branch_patterns = list(string)
}))
}))
| `{}` | no | +| [environments](#input\_environments) | Environments to create for the repository. |
map(object({
wait_timer = optional(number)
can_admins_bypass = optional(bool)
prevent_self_review = optional(bool)
action_secrets = optional(map(string))
reviewers = optional(object({
teams = optional(list(string))
users = optional(list(string))
}))
deployment_branch_policy = optional(object({
protected_branches = bool
custom_branch_policies = bool
branch_patterns = list(string)
}))
}))
| `{}` | no | | [homepage](#input\_homepage) | The homepage for the repository | `string` | `""` | no | | [license\_template](#input\_license\_template) | The (Optional) license template to apply to the repository | `string` | `null` | no | | [merge\_commit\_message](#input\_merge\_commit\_message) | (Optional) Can be `PR_BODY`, `PR_TITLE`, or `BLANK` for a default merge commit message. Applicable only if allow\_merge\_commit is `true`. | `string` | `"PR_TITLE"` | no | | [merge\_commit\_title](#input\_merge\_commit\_title) | (Optional) Can be `PR_TITLE` or `MERGE_MESSAGE` for a default merge commit title. Applicable only if allow\_merge\_commit is `true`. | `string` | `"MERGE_MESSAGE"` | no | | [name](#input\_name) | The name of the repository to create/import. | `string` | n/a | yes | -| [pages](#input\_pages) | The (Optional) configuration for GitHub Pages for the repository |
object({
source = optional(object({
branch = string
path = optional(string)
}))
build_type = optional(string)
cname = optional(string)
})
| `null` | no | -| [protected\_branches](#input\_protected\_branches) | A list of ref names or patterns that should be protected. Defaults `["main"]` | `list(string)` |
[
"main"
]
| no | +| [pages](#input\_pages) | The (Optional) configuration for GitHub Pages for the repository |
object({
source = optional(object({
branch = string
path = optional(string)
}))
build_type = optional(string)
cname = optional(string)
})
| `null` | no | +| [protected\_branches](#input\_protected\_branches) | A list of ref names or patterns that should be protected. Defaults `["main"]` | `list(string)` |
[
"main"
]
| no | | [repository\_team\_permissions](#input\_repository\_team\_permissions) | A map where the keys are github team slugs and the value is the permissions the team should have in the repository | `map(string)` | n/a | yes | | [repository\_user\_permissions](#input\_repository\_user\_permissions) | A map where the keys are github usernames and the value is the permissions the user should have in the repository | `map(string)` | n/a | yes | | [requires\_web\_commit\_signing](#input\_requires\_web\_commit\_signing) | If set commit signatures are required for commits to the organization. Defaults to `false`. | `bool` | `false` | no | -| [rulesets](#input\_rulesets) | n/a |
map(object({
bypass_actors = optional(object({
repository_roles = optional(list(object({
role = string
always_bypass = optional(bool)
})))
teams = optional(list(object({
team = string
always_bypass = optional(bool)
})))
integrations = optional(list(object({
installation_id = number
always_bypass = optional(bool)
})))
organization_admins = optional(list(object({
user = string
always_bypass = optional(bool)
})))
}))
conditions = optional(object({
ref_name = object({
include = list(string)
exclude = list(string)
})
}))
rules = object({
branch_name_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
tag_name_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
commit_author_email_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
commit_message_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
committer_email_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
creation = optional(bool)
deletion = optional(bool)
update = optional(bool)
non_fast_forward = optional(bool)
required_linear_history = optional(bool)
required_signatures = optional(bool)
update_allows_fetch_and_merge = optional(bool)
pull_request = optional(object({
dismiss_stale_reviews_on_push = optional(bool)
require_code_owner_review = optional(bool)
require_last_push_approval = optional(bool)
required_approving_review_count = optional(number)
required_review_thread_resolution = optional(bool)
}))
required_status_checks = optional(object({
required_check = list(object({
context = string
integration_id = optional(number)
}))
strict_required_status_check_policy = optional(bool)
}))
required_deployment_environments = optional(list(string))
})
target = string
enforcement = string
}))
| `{}` | no | +| [rulesets](#input\_rulesets) | n/a |
map(object({
bypass_actors = optional(object({
repository_roles = optional(list(object({
role = string
always_bypass = optional(bool)
})))
teams = optional(list(object({
team = string
always_bypass = optional(bool)
})))
integrations = optional(list(object({
installation_id = number
always_bypass = optional(bool)
})))
organization_admins = optional(list(object({
user = string
always_bypass = optional(bool)
})))
}))
conditions = optional(object({
ref_name = object({
include = list(string)
exclude = list(string)
})
}))
rules = object({
branch_name_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
tag_name_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
commit_author_email_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
commit_message_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
committer_email_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
creation = optional(bool)
deletion = optional(bool)
update = optional(bool)
non_fast_forward = optional(bool)
required_linear_history = optional(bool)
required_signatures = optional(bool)
update_allows_fetch_and_merge = optional(bool)
pull_request = optional(object({
dismiss_stale_reviews_on_push = optional(bool)
require_code_owner_review = optional(bool)
require_last_push_approval = optional(bool)
required_approving_review_count = optional(number)
required_review_thread_resolution = optional(bool)
}))
required_status_checks = optional(object({
required_check = list(object({
context = string
integration_id = optional(number)
}))
strict_required_status_check_policy = optional(bool)
}))
required_deployment_environments = optional(list(string))
})
target = string
enforcement = string
}))
| `{}` | no | | [squash\_merge\_commit\_message](#input\_squash\_merge\_commit\_message) | (Optional) Can be `PR_BODY`, `COMMIT_MESSAGES`, or `BLANK` for a default squash merge commit message. Applicable only if allow\_squash\_merge is `true`. | `string` | `"PR_BODY"` | no | | [squash\_merge\_commit\_title](#input\_squash\_merge\_commit\_title) | (Optional) Can be `PR_TITLE` or `COMMIT_OR_PR_TITLE` for a default squash merge commit title. Applicable only if allow\_squash\_merge is `true`. | `string` | `"PR_TITLE"` | no | -| [template\_repository](#input\_template\_repository) | A (Optional) list of template repositories to use for the repository |
object({
owner = string
repository = string
include_all_branches = bool
})
| `null` | no | +| [template\_repository](#input\_template\_repository) | A (Optional) list of template repositories to use for the repository |
object({
owner = string
repository = string
include_all_branches = bool
})
| `null` | no | | [topics](#input\_topics) | The topics to apply to the repository | `list(string)` | `[]` | no | ## Outputs diff --git a/modules/public_repository/repository.tftest.hcl b/modules/public_repository/repository.tftest.hcl new file mode 100644 index 0000000..7ea88a3 --- /dev/null +++ b/modules/public_repository/repository.tftest.hcl @@ -0,0 +1,65 @@ +mock_provider "github" {} + +variables { + name = "github-foundations-modules" + description = "A collection of terraform modules used in the Github Foundations framework." + visibility = "public" + has_downloads = true + has_issues = true + has_projects = true + has_wiki = true + has_discussions = true + has_vulnerability_alerts = true + topics = ["terraform", "github", "foundations"] + homepage = "myhomepage" + delete_head_on_merge = false + allow_auto_merge = true + allow_squash_merge = false + squash_merge_commit_message = "COMMIT_MESSAGES" + squash_merge_commit_title = "COMMIT_OR_PR_TITLE" + allow_merge_commit = false + merge_commit_message = "PR_BODY" + merge_commit_title = "PR_TITLE" + allow_rebase_merge = true + requires_web_commit_signing = false + license_template = "mit" + dependabot_security_updates = true + advance_security = true + secret_scanning = true + secret_scanning_on_push = true + + default_branch = "main" + protected_branches = ["main", "develop"] + + template_repository = { + owner = "owner" + repository = "template_repository" + include_all_branches = true + } + + pages = { + source = { + branch = "main" + path = "path" + } + cname = "cname" + } + + repository_team_permissions = { + "repo_team1" = "push" + "repo_team2" = "admin" + } + repository_user_permissions = { + "user1" = "push" + "user2" = "admin" + } +} + +run "create_test" { + command = apply + + assert { + condition = module.repository_base.id != null + error_message = "The repository was not created" + } +} diff --git a/modules/repository_base/README.md b/modules/repository_base/README.md index 3f039aa..494de8d 100644 --- a/modules/repository_base/README.md +++ b/modules/repository_base/README.md @@ -52,7 +52,7 @@ | [dependabot\_secrets](#input\_dependabot\_secrets) | An (Optional) map of Dependabot secrets to create for this repository. The key is the name of the secret and the value is the encrypted value. | `map(string)` | `{}` | no | | [dependabot\_security\_updates](#input\_dependabot\_security\_updates) | Enables dependabot security updates. Only works when `has_vulnerability_alerts` is set because that is required to enable dependabot for the repository. | `bool` | `true` | no | | [description](#input\_description) | The description to give to the repository. Defaults to `""` | `string` | `""` | no | -| [environments](#input\_environments) | An (Optional) map of environments to create for the repository. The key is the name of the environment and the value is the environment configuration. |
map(object({
wait_timer = optional(number)
can_admins_bypass = optional(bool)
prevent_self_review = optional(bool)
action_secrets = optional(map(string))
reviewers = optional(object({
teams = optional(list(string))
users = optional(list(string))
}))
deployment_branch_policy = optional(object({
protected_branches = bool
custom_branch_policies = bool
branch_patterns = list(string)
}))
}))
| `{}` | no | +| [environments](#input\_environments) | An (Optional) map of environments to create for the repository. The key is the name of the environment and the value is the environment configuration. |
map(object({
wait_timer = optional(number)
can_admins_bypass = optional(bool)
prevent_self_review = optional(bool)
action_secrets = optional(map(string))
reviewers = optional(object({
teams = optional(list(string))
users = optional(list(string))
}))
deployment_branch_policy = optional(object({
protected_branches = bool
custom_branch_policies = bool
branch_patterns = list(string)
}))
}))
| `{}` | no | | [has\_discussions](#input\_has\_discussions) | Enables Github Discussions. | `bool` | `true` | no | | [has\_downloads](#input\_has\_downloads) | Enables downloads for the repository | `bool` | `false` | no | | [has\_issues](#input\_has\_issues) | Enables Github Issues for the repository | `bool` | `true` | no | @@ -64,17 +64,17 @@ | [merge\_commit\_message](#input\_merge\_commit\_message) | (Optional) Can be `PR_BODY`, `PR_TITLE`, or `BLANK` for a default merge commit message. Applicable only if allow\_merge\_commit is `true`. | `string` | `"PR_TITLE"` | no | | [merge\_commit\_title](#input\_merge\_commit\_title) | (Optional) Can be `PR_TITLE` or `MERGE_MESSAGE` for a default merge commit title. Applicable only if allow\_merge\_commit is `true`. | `string` | `"MERGE_MESSAGE"` | no | | [name](#input\_name) | The name of the repository to create/import. | `string` | n/a | yes | -| [pages](#input\_pages) | The (Optional) configuration for GitHub Pages for the repository |
object({
source = optional(object({
branch = string
path = optional(string)
}))
build_type = optional(string)
cname = optional(string)
})
| `null` | no | -| [protected\_branches](#input\_protected\_branches) | A list of ref names or patterns that should be protected. Setting to `[]` means no protection. Defaults `["~DEFAULT_BRANCH"]` | `list(string)` |
[
"~DEFAULT_BRANCH"
]
| no | +| [pages](#input\_pages) | The (Optional) configuration for GitHub Pages for the repository |
object({
source = optional(object({
branch = string
path = optional(string)
}))
build_type = optional(string)
cname = optional(string)
})
| `null` | no | +| [protected\_branches](#input\_protected\_branches) | A list of ref names or patterns that should be protected. Setting to `[]` means no protection. Defaults `["~DEFAULT_BRANCH"]` | `list(string)` |
[
"~DEFAULT_BRANCH"
]
| no | | [repository\_team\_permissions](#input\_repository\_team\_permissions) | A map where the keys are github team slugs and the value is the permissions the team should have in the repository | `map(string)` | n/a | yes | | [repository\_user\_permissions](#input\_repository\_user\_permissions) | A map where the keys are github usernames and the value is the permissions the user should have in the repository | `map(string)` | n/a | yes | | [requires\_web\_commit\_signing](#input\_requires\_web\_commit\_signing) | If set commit signatures are required for commits to the organization. Defaults to `false`. | `bool` | `false` | no | -| [rulesets](#input\_rulesets) | n/a |
map(object({
bypass_actors = optional(object({
repository_roles = optional(list(object({
role = string
always_bypass = optional(bool)
})))
teams = optional(list(object({
team = string
always_bypass = optional(bool)
})))
integrations = optional(list(object({
installation_id = number
always_bypass = optional(bool)
})))
organization_admins = optional(list(object({
user = string
always_bypass = optional(bool)
})))
}))
conditions = optional(object({
ref_name = object({
include = list(string)
exclude = list(string)
})
}))
rules = object({
branch_name_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
tag_name_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
commit_author_email_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
commit_message_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
committer_email_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
creation = optional(bool)
deletion = optional(bool)
update = optional(bool)
non_fast_forward = optional(bool)
required_linear_history = optional(bool)
required_signatures = optional(bool)
update_allows_fetch_and_merge = optional(bool)
pull_request = optional(object({
dismiss_stale_reviews_on_push = optional(bool)
require_code_owner_review = optional(bool)
require_last_push_approval = optional(bool)
required_approving_review_count = optional(number)
required_review_thread_resolution = optional(bool)
}))
required_status_checks = optional(object({
required_check = list(object({
context = string
integration_id = optional(number)
}))
strict_required_status_check_policy = optional(bool)
}))
required_deployment_environments = optional(list(string))
})
target = string
enforcement = string
}))
| `{}` | no | +| [rulesets](#input\_rulesets) | n/a |
map(object({
bypass_actors = optional(object({
repository_roles = optional(list(object({
role = string
always_bypass = optional(bool)
})))
teams = optional(list(object({
team = string
always_bypass = optional(bool)
})))
integrations = optional(list(object({
installation_id = number
always_bypass = optional(bool)
})))
organization_admins = optional(list(object({
user = string
always_bypass = optional(bool)
})))
}))
conditions = optional(object({
ref_name = object({
include = list(string)
exclude = list(string)
})
}))
rules = object({
branch_name_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
tag_name_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
commit_author_email_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
commit_message_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
committer_email_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
creation = optional(bool)
deletion = optional(bool)
update = optional(bool)
non_fast_forward = optional(bool)
required_linear_history = optional(bool)
required_signatures = optional(bool)
update_allows_fetch_and_merge = optional(bool)
pull_request = optional(object({
dismiss_stale_reviews_on_push = optional(bool)
require_code_owner_review = optional(bool)
require_last_push_approval = optional(bool)
required_approving_review_count = optional(number)
required_review_thread_resolution = optional(bool)
}))
required_status_checks = optional(object({
required_check = list(object({
context = string
integration_id = optional(number)
}))
strict_required_status_check_policy = optional(bool)
}))
required_deployment_environments = optional(list(string))
})
target = string
enforcement = string
}))
| `{}` | no | | [secret\_scanning](#input\_secret\_scanning) | Enables secret scanning for the repository. If repository is private `advance_security` must also be enabled. | `bool` | `true` | no | | [secret\_scanning\_on\_push](#input\_secret\_scanning\_on\_push) | Enables secret scanning push protection for the repository. If repository is private `advance_security` must also be enabled. | `bool` | `true` | no | | [squash\_merge\_commit\_message](#input\_squash\_merge\_commit\_message) | (Optional) Can be `PR_BODY`, `COMMIT_MESSAGES`, or `BLANK` for a default squash merge commit message. Applicable only if allow\_squash\_merge is `true`. | `string` | `"PR_BODY"` | no | | [squash\_merge\_commit\_title](#input\_squash\_merge\_commit\_title) | (Optional) Can be `PR_TITLE` or `COMMIT_OR_PR_TITLE` for a default squash merge commit title. Applicable only if allow\_squash\_merge is `true`. | `string` | `"PR_TITLE"` | no | -| [template\_repository](#input\_template\_repository) | A (Optional) list of template repositories to use for the repository |
object({
owner = string
repository = string
include_all_branches = bool
})
| `null` | no | +| [template\_repository](#input\_template\_repository) | A (Optional) list of template repositories to use for the repository |
object({
owner = string
repository = string
include_all_branches = bool
})
| `null` | no | | [topics](#input\_topics) | The topics to apply to the repository | `list(string)` | `[]` | no | | [visibility](#input\_visibility) | Sets the visibility property of a repository. Defaults to "private" | `string` | `"private"` | no | diff --git a/modules/repository_base/collaborators.tftest.hcl b/modules/repository_base/collaborators.tftest.hcl new file mode 100644 index 0000000..43f2886 --- /dev/null +++ b/modules/repository_base/collaborators.tftest.hcl @@ -0,0 +1,32 @@ +mock_provider "github" {} + +variables { + name = "github-foundations-modules" + description = "A collection of terraform modules used in the Github Foundations framework." + + repository_team_permissions = { + "repo_team1" = "push" + "repo_team2" = "admin" + } + repository_user_permissions = { + "user1" = "push" + "user2" = "admin" + } +} + +run "collaborators_test" { + command = apply + + assert { + condition = github_repository_collaborators.collaborators.repository == var.name + error_message = "The repository id value is incorrect. Expected ${var.name}, got ${github_repository_collaborators.collaborators.repository}" + } + assert { + condition = length(github_repository_collaborators.collaborators.team) == length(var.repository_team_permissions) + error_message = "The number of teams is incorrect. Expected ${length(var.repository_team_permissions)}, got ${length(github_repository_collaborators.collaborators.team)}" + } + assert { + condition = length(github_repository_collaborators.collaborators.user) == length(var.repository_user_permissions) + error_message = "The number of users is incorrect. Expected ${length(var.repository_user_permissions)}, got ${length(github_repository_collaborators.collaborators.user)}" + } +} diff --git a/modules/repository_base/environments.tftest.hcl b/modules/repository_base/environments.tftest.hcl new file mode 100644 index 0000000..e60edf5 --- /dev/null +++ b/modules/repository_base/environments.tftest.hcl @@ -0,0 +1,95 @@ +mock_provider "github" {} + +variables { + name = "github-foundations-modules" + description = "A collection of terraform modules used in the Github Foundations framework." + + repository_team_permissions = { + "repo_team1" = "push" + "repo_team2" = "admin" + } + repository_user_permissions = { + "user1" = "push" + "user2" = "admin" + } + environments = { + "env1" = { + wait_timer = 10 + can_admins_bypass = true + prevent_self_review = false + action_secrets = { + "action_secret1" = "dmFsdWUxCg==" # base64 encoded + } + reviewers = { + teams = [111, 222] + users = [55, 66] + } + deployment_branch_policy = { + protected_branches = true + custom_branch_policies = false + branch_patterns = ["main"] + } + } + } + +} + +run "create_environment_test" { + command = apply + + assert { + condition = github_repository_environment.environment["env1"].repository == var.name + error_message = "The repository id value is incorrect. Expected ${var.name}, got ${github_repository_environment.environment["env1"].repository}" + } + assert { + condition = github_repository_environment.environment["env1"].environment == "env1" + error_message = "The environment name value is incorrect. Expected env1, got ${github_repository_environment.environment["env1"].environment}" + } + assert { + condition = github_repository_environment.environment["env1"].wait_timer == var.environments["env1"].wait_timer + error_message = "The wait timer value is incorrect. Expected ${var.environments["env1"].wait_timer}, got ${github_repository_environment.environment["env1"].wait_timer}" + } + assert { + condition = github_repository_environment.environment["env1"].can_admins_bypass == var.environments["env1"].can_admins_bypass + error_message = "The can admins bypass value is incorrect. Expected ${var.environments["env1"].can_admins_bypass}, got ${github_repository_environment.environment["env1"].can_admins_bypass}" + } + assert { + condition = github_repository_environment.environment["env1"].prevent_self_review == var.environments["env1"].prevent_self_review + error_message = "The prevent self review value is incorrect. Expected ${var.environments["env1"].prevent_self_review}, got ${github_repository_environment.environment["env1"].prevent_self_review}" + } + assert { + condition = length(github_repository_environment.environment["env1"].reviewers["0"].teams) == length(var.environments["env1"].reviewers.teams) + error_message = "The reviewers teams value is incorrect. Expected ${length(var.environments["env1"].reviewers.teams)}, got ${length(github_repository_environment.environment["env1"].reviewers["0"].teams)}" + } + assert { + condition = length(github_repository_environment.environment["env1"].reviewers["0"].users) == length(var.environments["env1"].reviewers.users) + error_message = "The reviewers users value is incorrect. Expected ${length(var.environments["env1"].reviewers.users)}, got ${length(github_repository_environment.environment["env1"].reviewers["0"].users)}" + } + assert { + condition = github_repository_environment.environment["env1"].deployment_branch_policy["0"].protected_branches == var.environments["env1"].deployment_branch_policy.protected_branches + error_message = "The deployment branch policy protected branches value is incorrect. Expected ${var.environments["env1"].deployment_branch_policy.protected_branches}, got ${github_repository_environment.environment["env1"].deployment_branch_policy["0"].protected_branches}" + } + assert { + condition = github_repository_environment.environment["env1"].deployment_branch_policy["0"].custom_branch_policies == var.environments["env1"].deployment_branch_policy.custom_branch_policies + error_message = "The deployment branch policy custom branch policies value is incorrect. Expected ${var.environments["env1"].deployment_branch_policy.custom_branch_policies}, got ${github_repository_environment.environment["env1"].deployment_branch_policy["0"].custom_branch_policies}" + } + assert { + condition = length(github_repository_environment_deployment_policy.deployment_policy) == length(var.environments["env1"].deployment_branch_policy.branch_patterns) + error_message = "The deployment policy length is incorrect. Expected ${length(var.environments["env1"].deployment_branch_policy.branch_patterns)}, got ${length(github_repository_environment_deployment_policy.deployment_policy)}" + } +} + +run "deployment_policy_test" { + assert { + condition = github_repository_environment_deployment_policy.deployment_policy["env1:main"].repository == var.name + error_message = "The repository id value is incorrect. Expected ${var.name}, got ${github_repository_environment_deployment_policy.deployment_policy["env1:main"].repository}" + } + assert { + condition = github_repository_environment_deployment_policy.deployment_policy["env1:main"].environment == "env1" + error_message = "The environment name value is incorrect. Expected env1, got ${github_repository_environment_deployment_policy.deployment_policy["env1:main"].environment}" + } + assert { + condition = github_repository_environment_deployment_policy.deployment_policy["env1:main"].branch_pattern == "main" + error_message = "The branch pattern value is incorrect. Expected main, got ${github_repository_environment_deployment_policy.deployment_policy["env1:main"].branch_pattern}" + } +} diff --git a/modules/repository_base/repository.tftest.hcl b/modules/repository_base/repository.tftest.hcl new file mode 100644 index 0000000..8c77e5b --- /dev/null +++ b/modules/repository_base/repository.tftest.hcl @@ -0,0 +1,280 @@ +mock_provider "github" {} + +variables { + name = "github-foundations-modules" + description = "A collection of terraform modules used in the Github Foundations framework." + visibility = "public" + has_downloads = true + has_issues = true + has_projects = true + has_wiki = true + has_discussions = true + has_vulnerability_alerts = true + topics = ["terraform", "github", "foundations"] + homepage = "myhomepage" + delete_head_on_merge = false + allow_auto_merge = true + allow_squash_merge = false + squash_merge_commit_message = "COMMIT_MESSAGES" + squash_merge_commit_title = "COMMIT_OR_PR_TITLE" + allow_merge_commit = false + merge_commit_message = "PR_BODY" + merge_commit_title = "PR_TITLE" + allow_rebase_merge = true + requires_web_commit_signing = false + license_template = "mit" + dependabot_security_updates = true + advance_security = true + secret_scanning = true + secret_scanning_on_push = true + + default_branch = "main" + protected_branches = ["main", "develop"] + + template_repository = { + owner = "owner" + repository = "template_repository" + include_all_branches = true + } + + pages = { + source = { + branch = "main" + path = "path" + } + cname = "cname" + } + + repository_team_permissions = { + "repo_team1" = "push" + "repo_team2" = "admin" + } + repository_user_permissions = { + "user1" = "push" + "user2" = "admin" + } +} + +run "repository_test" { + assert { + condition = github_repository.repository.name == var.name + error_message = "Repository name does not match. Expected: ${var.name}, Actual: ${github_repository.repository.name}" + } + assert { + condition = github_repository.repository.description == var.description + error_message = "Repository description does not match. Expected: ${var.description}, Actual: ${github_repository.repository.description}" + } + assert { + condition = github_repository.repository.visibility == var.visibility + error_message = "Repository visibility does not match. Expected: ${var.visibility}, Actual: ${github_repository.repository.visibility}" + } + assert { + condition = github_repository.repository.auto_init == true + error_message = "Repository auto_init does not match. Expected: true, Actual: ${github_repository.repository.auto_init}" + } + assert { + condition = github_repository.repository.archive_on_destroy == false + error_message = "Repository archive_on_destroy does not match. Expected: false, Actual: ${github_repository.repository.archive_on_destroy}" + } + assert { + condition = github_repository.repository.has_downloads == var.has_downloads + error_message = "Repository has_downloads does not match. Expected: ${var.has_downloads}, Actual: ${github_repository.repository.has_downloads}" + } + assert { + condition = github_repository.repository.has_issues == var.has_issues + error_message = "Repository has_issues does not match. Expected: ${var.has_issues}, Actual: ${github_repository.repository.has_issues}" + } + assert { + condition = github_repository.repository.has_projects == var.has_projects + error_message = "Repository has_projects does not match. Expected: ${var.has_projects}, Actual: ${github_repository.repository.has_projects}" + } + assert { + condition = github_repository.repository.has_wiki == var.has_wiki + error_message = "Repository has_wiki does not match. Expected: ${var.has_wiki}, Actual: ${github_repository.repository.has_wiki}" + } + assert { + condition = github_repository.repository.has_discussions == var.has_discussions + error_message = "Repository has_discussions does not match. Expected: ${var.has_discussions}, Actual: ${github_repository.repository.has_discussions}" + } + assert { + condition = github_repository.repository.vulnerability_alerts == var.has_vulnerability_alerts + error_message = "Repository vulnerability_alerts does not match. Expected: ${var.has_vulnerability_alerts}, Actual: ${github_repository.repository.vulnerability_alerts}" + } + assert { + condition = length(github_repository.repository.topics) == length(var.topics) + error_message = "Repository topics length does not match. Expected: ${length(var.topics)}, Actual: ${length(github_repository.repository.topics)}" + } + assert { + condition = github_repository.repository.homepage_url == var.homepage + error_message = "Repository homepage does not match. Expected: ${var.homepage}, Actual: ${github_repository.repository.homepage_url}" + } + assert { + condition = github_repository.repository.delete_branch_on_merge == var.delete_head_on_merge + error_message = "Repository delete_branch_on_merge does not match. Expected: ${var.delete_head_on_merge}, Actual: ${github_repository.repository.delete_branch_on_merge}" + } + assert { + condition = github_repository.repository.allow_auto_merge == var.allow_auto_merge + error_message = "Repository allow_auto_merge does not match. Expected: ${var.allow_auto_merge}, Actual: ${github_repository.repository.allow_auto_merge}" + } + assert { + condition = github_repository.repository.allow_squash_merge == var.allow_squash_merge + error_message = "Repository allow_squash_merge does not match. Expected: ${var.allow_squash_merge}, Actual: ${github_repository.repository.allow_squash_merge}" + } + assert { + condition = github_repository.repository.squash_merge_commit_message == var.squash_merge_commit_message + error_message = "Repository squash_merge_commit_message does not match. Expected: ${var.squash_merge_commit_message}, Actual: ${github_repository.repository.squash_merge_commit_message}" + } + assert { + condition = github_repository.repository.squash_merge_commit_title == var.squash_merge_commit_title + error_message = "Repository squash_merge_commit_title does not match. Expected: ${var.squash_merge_commit_title}, Actual: ${github_repository.repository.squash_merge_commit_title}" + } + assert { + condition = github_repository.repository.allow_merge_commit == var.allow_merge_commit + error_message = "Repository allow_merge_commit does not match. Expected: ${var.allow_merge_commit}, Actual: ${github_repository.repository.allow_merge_commit}" + } + assert { + condition = github_repository.repository.merge_commit_message == var.merge_commit_message + error_message = "Repository merge_commit_message does not match. Expected: ${var.merge_commit_message}, Actual: ${github_repository.repository.merge_commit_message}" + } + assert { + condition = github_repository.repository.merge_commit_title == var.merge_commit_title + error_message = "Repository merge_commit_title does not match. Expected: ${var.merge_commit_title}, Actual: ${github_repository.repository.merge_commit_title}" + } + assert { + condition = github_repository.repository.allow_rebase_merge == var.allow_rebase_merge + error_message = "Repository allow_rebase_merge does not match. Expected: ${var.allow_rebase_merge}, Actual: ${github_repository.repository.allow_rebase_merge}" + } + assert { + condition = github_repository.repository.web_commit_signoff_required == var.requires_web_commit_signing + error_message = "Repository web_commit_signoff_required does not match. Expected: ${var.requires_web_commit_signing}, Actual: ${github_repository.repository.web_commit_signoff_required}" + } + assert { + condition = github_repository.repository.license_template == var.license_template + error_message = "Repository license_template does not match. Expected: ${var.license_template}, Actual: ${github_repository.repository.license_template}" + } + assert { + condition = github_repository.repository.security_and_analysis[0].advanced_security[0].status == "enabled" + error_message = "Repository advanced_security status does not match. Expected: enabled, Actual: ${github_repository.repository.security_and_analysis[0].advanced_security[0].status}" + } + assert { + condition = github_repository.repository.security_and_analysis[0].secret_scanning[0].status == "enabled" + error_message = "Repository secret_scanning status does not match. Expected: enabled, Actual: ${github_repository.repository.security_and_analysis[0].secret_scanning[0].status}" + } + assert { + condition = github_repository.repository.security_and_analysis[0].secret_scanning_push_protection[0].status == "enabled" + error_message = "Repository secret_scanning_push_protection status does not match. Expected: enabled, Actual: ${github_repository.repository.security_and_analysis[0].secret_scanning_push_protection[0].status}" + } + assert { + condition = github_repository.repository.template[0].owner == var.template_repository.owner + error_message = "Repository template owner does not match. Expected: ${var.template_repository.owner}, Actual: ${github_repository.repository.template[0].owner}" + } + assert { + condition = github_repository.repository.template[0].repository == var.template_repository.repository + error_message = "Repository template repository does not match. Expected: ${var.template_repository.repository}, Actual: ${github_repository.repository.template[0].repository}" + } + assert { + condition = github_repository.repository.template[0].include_all_branches == var.template_repository.include_all_branches + error_message = "Repository template include_all_branches does not match. Expected: ${var.template_repository.include_all_branches}, Actual: ${github_repository.repository.template[0].include_all_branches}" + } + assert { + condition = github_repository.repository.pages[0].source[0].branch == var.pages.source.branch + error_message = "Repository pages source branch does not match. Expected: ${var.pages.source.branch}, Actual: ${github_repository.repository.pages[0].source[0].branch}" + } + assert { + condition = github_repository.repository.pages[0].source[0].path == var.pages.source.path + error_message = "Repository pages source path does not match. Expected: ${var.pages.source.path}, Actual: ${github_repository.repository.pages[0].source[0].path}" + } + assert { + condition = github_repository.repository.pages[0].cname == var.pages.cname + error_message = "Repository pages cname does not match. Expected: ${var.pages.cname}, Actual: ${github_repository.repository.pages[0].cname}" + } +} + +run "automated_security_fixes_test" { + assert { + condition = length(github_repository_dependabot_security_updates.automated_security_fixes) == 1 + error_message = "Repository automated_security_fixes count does not match. Expected: 1, Actual: ${length(github_repository_dependabot_security_updates.automated_security_fixes)}" + } + assert { + condition = github_repository_dependabot_security_updates.automated_security_fixes[0].repository == var.name + error_message = "Repository automated_security_fixes repository does not match. Expected: ${var.name}, Actual: ${github_repository_dependabot_security_updates.automated_security_fixes[0].repository}" + } + assert { + condition = github_repository_dependabot_security_updates.automated_security_fixes[0].enabled == var.dependabot_security_updates + error_message = "Repository automated_security_fixes enabled does not match. Expected: ${var.dependabot_security_updates}, Actual: ${github_repository_dependabot_security_updates.automated_security_fixes[0].enabled}" + } +} + +run "default_branch_test" { + assert { + condition = github_branch_default.default_branch.repository == var.name + error_message = "Repository default_branch repository does not match. Expected: ${var.name}, Actual: ${github_branch_default.default_branch.repository}" + } + assert { + condition = github_branch_default.default_branch.branch == var.default_branch + error_message = "Repository default_branch branch does not match. Expected: ${var.default_branch}, Actual: ${github_branch_default.default_branch.branch}" + } +} + +run "protected_branch_base_rules_test" { + assert { + condition = github_repository_ruleset.protected_branch_base_rules[0].name == "protected_branch_base_ruleset" + error_message = "Repository protected_branch_base_rules name does not match. Expected: protected_branch_base_ruleset, Actual: ${github_repository_ruleset.protected_branch_base_rules[0].name}" + } + assert { + condition = github_repository_ruleset.protected_branch_base_rules[0].repository == var.name + error_message = "Repository protected_branch_base_rules repository does not match. Expected: ${var.name}, Actual: ${github_repository_ruleset.protected_branch_base_rules[0].repository}" + } + assert { + condition = github_repository_ruleset.protected_branch_base_rules[0].target == "branch" + error_message = "Repository protected_branch_base_rules target does not match. Expected: branch, Actual: ${github_repository_ruleset.protected_branch_base_rules[0].target}" + } + assert { + condition = github_repository_ruleset.protected_branch_base_rules[0].enforcement == "active" + error_message = "Repository protected_branch_base_rules enforcement does not match. Expected: active, Actual: ${github_repository_ruleset.protected_branch_base_rules[0].enforcement}" + } + # test the rules + assert { + condition = github_repository_ruleset.protected_branch_base_rules[0].rules[0].deletion == true + error_message = "Repository protected_branch_base_rules rules deletion does not match. Expected: true, Actual: ${github_repository_ruleset.protected_branch_base_rules[0].rules[0].deletion}" + } + assert { + condition = github_repository_ruleset.protected_branch_base_rules[0].rules[0].creation == false + error_message = "Repository protected_branch_base_rules rules creation does not match. Expected: false, Actual: ${github_repository_ruleset.protected_branch_base_rules[0].rules[0].creation}" + } + assert { + condition = github_repository_ruleset.protected_branch_base_rules[0].rules[0].update == false + error_message = "Repository protected_branch_base_rules rules update does not match. Expected: false, Actual: ${github_repository_ruleset.protected_branch_base_rules[0].rules[0].update}" + } + # Rule Pull Request + assert { + condition = github_repository_ruleset.protected_branch_base_rules[0].rules[0].pull_request[0].dismiss_stale_reviews_on_push == true + error_message = "Repository protected_branch_base_rules rules pull_request.dismiss_stale_reviews_on_push does not match. Expected: true, Actual: ${github_repository_ruleset.protected_branch_base_rules[0].rules[0].pull_request[0].dismiss_stale_reviews_on_push}" + } + assert { + condition = github_repository_ruleset.protected_branch_base_rules[0].rules[0].pull_request[0].require_last_push_approval == true + error_message = "Repository protected_branch_base_rules rules pull_request.require_last_push_approval does not match. Expected: true, Actual: ${github_repository_ruleset.protected_branch_base_rules[0].rules[0].pull_request[0].require_last_push_approval}" + } + assert { + condition = github_repository_ruleset.protected_branch_base_rules[0].rules[0].pull_request[0].required_approving_review_count == 1 + error_message = "Repository protected_branch_base_rules rules pull_request.required_approving_review_count does not match. Expected: 1, Actual: ${github_repository_ruleset.protected_branch_base_rules[0].rules[0].pull_request[0].required_approving_review_count}" + } + + assert { + condition = github_repository_ruleset.protected_branch_base_rules[0].rules[0].non_fast_forward == true + error_message = "Repository protected_branch_base_rules rules non_fast_forward does not match. Expected: true, Actual: ${github_repository_ruleset.protected_branch_base_rules[0].rules[0].non_fast_forward}" + } + # Test conditions + assert { + condition = length(github_repository_ruleset.protected_branch_base_rules[0].conditions[0].ref_name[0].exclude) == 0 + error_message = "Repository protected_branch_base_rules conditions ref_name.exclude length does not match. Expected: 0, Actual: ${length(github_repository_ruleset.protected_branch_base_rules[0].conditions[0].ref_name[0].exclude)}" + } + assert { + condition = github_repository_ruleset.protected_branch_base_rules[0].conditions[0].ref_name[0].include[0] == "refs/heads/${var.protected_branches[1]}" + error_message = "Repository protected_branch_base_rules conditions ref_name.include does not match. Expected: refs/heads/${var.protected_branches[1]}, Actual: ${github_repository_ruleset.protected_branch_base_rules[0].conditions[0].ref_name[0].include[0]}" + } + assert { + condition = github_repository_ruleset.protected_branch_base_rules[0].conditions[0].ref_name[0].include[1] == "refs/heads/${var.protected_branches[0]}" + error_message = "Repository protected_branch_base_rules conditions ref_name.include does not match. Expected: refs/heads/${var.protected_branches[0]}, Actual: ${github_repository_ruleset.protected_branch_base_rules[0].conditions[0].ref_name[0].include[1]}" + } +} diff --git a/modules/repository_base/rulesets.tftest.hcl b/modules/repository_base/rulesets.tftest.hcl new file mode 100644 index 0000000..cc6d6d8 --- /dev/null +++ b/modules/repository_base/rulesets.tftest.hcl @@ -0,0 +1,70 @@ +mock_provider "github" {} + +variables { + name = "github-foundations-modules" + description = "A collection of terraform modules used in the Github Foundations framework." + + repository_team_permissions = { + "repo_team1" = "push" + "repo_team2" = "admin" + } + repository_user_permissions = { + "user1" = "push" + "user2" = "admin" + } + + rulesets = { + ruleset1 = { + target = "branch" + enforcement = "evaluate" + conditions = { + ref_name = { + include = ["main"] + exclude = ["feature/*"] + } + } + rules = { + branch_name_pattern = { + operator = "regex" + pattern = "main" + name = "branch_name_pattern" + negate = false + } + commit_author_email_pattern = { + operator = "regex" + pattern = "pattern" + name = "commit_author_email_pattern" + negate = false + } + commit_message_pattern = { + operator = "regex" + pattern = "pattern" + name = "commit_message_pattern" + negate = false + } + committer_email_pattern = { + operator = "regex" + pattern = "pattern" + name = "committer_email_pattern" + negate = false + } + creation = true + deletion = false + update = true + non_fast_forward = false + required_linear_history = true + required_signatures = false + update_allows_fetch_and_merge = true + } + } + } +} + +run "rulesets_test" { + command = apply + + assert { + condition = module.ruleset.ruleset1 != null + error_message = "The ruleset name is null" + } +} diff --git a/modules/repository_base/secrets.tftest.hcl b/modules/repository_base/secrets.tftest.hcl new file mode 100644 index 0000000..1d34046 --- /dev/null +++ b/modules/repository_base/secrets.tftest.hcl @@ -0,0 +1,65 @@ +mock_provider "github" {} + +variables { + name = "github-foundations-modules" + description = "A collection of terraform modules used in the Github Foundations framework." + + repository_team_permissions = { + "repo_team1" = "push" + "repo_team2" = "admin" + } + repository_user_permissions = { + "user1" = "push" + "user2" = "admin" + } + + action_secrets = { + "action_secret1" = "dmFsdWUxCg==" + } + + codespace_secrets = { + "codespace_secret1" = "dmFsdWUxCg==" + } + + dependabot_secrets = { + "dependabot_secret1" = "dmFsdWUxCg==" + } +} + +run "actions_secret_test" { + command = apply + + assert { + condition = github_actions_secret.actions_secret["action_secret1"].secret_name == "action_secret1" + error_message = "The secret name value is incorrect. Expected \"action_secret1\", got ${github_actions_secret.actions_secret["action_secret1"].secret_name}" + } + assert { + condition = github_actions_secret.actions_secret["action_secret1"].encrypted_value == "dmFsdWUxCg==" # "value1" base64 encoded + error_message = "The encrypted value is incorrect. Expected \"dmFsdWUxCg==\", got ${nonsensitive(github_actions_secret.actions_secret["action_secret1"].encrypted_value)}" + } + +} + +run "namespaced_secrets_test" { + + assert { + condition = github_codespaces_secret.codespaces_secret["codespace_secret1"].secret_name == "codespace_secret1" + error_message = "The secret name value is incorrect. Expected \"codespace_secret1\", got ${github_codespaces_secret.codespaces_secret["codespace_secret1"].secret_name}" + } + assert { + condition = github_codespaces_secret.codespaces_secret["codespace_secret1"].encrypted_value == "dmFsdWUxCg==" + error_message = "The encrypted value is incorrect. Expected \"dmFsdWUxCg==\", got ${nonsensitive(github_codespaces_secret.codespaces_secret["codespace_secret1"].encrypted_value)}" + } +} + +run "dependabot_secrets_test" { + + assert { + condition = github_dependabot_secret.dependabot_secret["dependabot_secret1"].secret_name == "dependabot_secret1" + error_message = "The secret name value is incorrect. Expected \"dependabot_secret1\", got ${github_dependabot_secret.dependabot_secret["dependabot_secret1"].secret_name}" + } + assert { + condition = github_dependabot_secret.dependabot_secret["dependabot_secret1"].encrypted_value == "dmFsdWUxCg==" + error_message = "The encrypted value is incorrect. Expected \"dmFsdWUxCg==\", got ${nonsensitive(github_dependabot_secret.dependabot_secret["dependabot_secret1"].encrypted_value)}" + } +} diff --git a/modules/repository_set/README.md b/modules/repository_set/README.md index 5a74929..13eacbe 100644 --- a/modules/repository_set/README.md +++ b/modules/repository_set/README.md @@ -32,9 +32,9 @@ |------|-------------|------|---------|:--------:| | [default\_repository\_team\_permissions](#input\_default\_repository\_team\_permissions) | A map where the keys are github team slugs and the value is the permissions the team should have by default for every repository. If an entry exists in `repository_team_permissions_override` for a repository then that will take precedence over this default. Defaults to `{}` giving no team access to the repositories. | `map(string)` | `{}` | no | | [has\_ghas\_license](#input\_has\_ghas\_license) | If the organization owning the repositories has a GitHub Advanced Security license or not. Defaults to false. | `bool` | `false` | no | -| [private\_repositories](#input\_private\_repositories) | A map of private repositories where the key is the repository name and the value is the configuration |
map(object({
description = string
default_branch = string
protected_branches = list(string)
advance_security = bool
has_vulnerability_alerts = bool
topics = list(string)
homepage = string
delete_head_on_merge = bool
requires_web_commit_signing = bool
dependabot_security_updates = bool
allow_auto_merge = optional(bool)
allow_squash_merge = optional(bool)
allow_rebase_merge = optional(bool)
allow_merge_commit = optional(bool)
squash_merge_commit_title = optional(string)
squash_merge_commit_message = optional(string)
merge_commit_title = optional(string)
merge_commit_message = optional(string)
repository_team_permissions_override = optional(map(string))
user_permissions = optional(map(string))
organization_action_secrets = optional(list(string))
organization_codespace_secrets = optional(list(string))
organization_dependabot_secrets = optional(list(string))
action_secrets = optional(map(string))
codespace_secrets = optional(map(string))
dependabot_secrets = optional(map(string))
environments = optional(map(object({
wait_timer = optional(number)
can_admins_bypass = optional(bool)
prevent_self_review = optional(bool)
action_secrets = optional(map(string))
reviewers = optional(object({
teams = optional(list(string))
users = optional(list(string))
}))
deployment_branch_policy = optional(object({
protected_branches = bool
custom_branch_policies = bool
branch_patterns = list(string)
}))
})))
template_repository = optional(object({
owner = string
repository = string
include_all_branches = bool
}))
license_template = optional(string)
pages = optional(object({
source = optional(object({
branch = string
path = optional(string)
}))
build_type = optional(string)
cname = optional(string)
}))
}))
| n/a | yes | -| [public\_repositories](#input\_public\_repositories) | A map of public repositories where the key is the repository name and the value is the configuration |
map(object({
description = string
default_branch = string
protected_branches = list(string)
advance_security = bool
topics = list(string)
homepage = string
delete_head_on_merge = bool
dependabot_security_updates = bool
requires_web_commit_signing = bool
allow_auto_merge = optional(bool)
allow_squash_merge = optional(bool)
allow_rebase_merge = optional(bool)
allow_merge_commit = optional(bool)
squash_merge_commit_title = optional(string)
squash_merge_commit_message = optional(string)
merge_commit_title = optional(string)
merge_commit_message = optional(string)
repository_team_permissions_override = optional(map(string))
user_permissions = optional(map(string))
organization_action_secrets = optional(list(string))
organization_codespace_secrets = optional(list(string))
organization_dependabot_secrets = optional(list(string))
action_secrets = optional(map(string))
codespace_secrets = optional(map(string))
dependabot_secrets = optional(map(string))
environments = optional(map(object({
wait_timer = optional(number)
can_admins_bypass = optional(bool)
prevent_self_review = optional(bool)
action_secrets = optional(map(string))
reviewers = optional(object({
teams = optional(list(string))
users = optional(list(string))
}))
deployment_branch_policy = optional(object({
protected_branches = bool
custom_branch_policies = bool
branch_patterns = list(string)
}))
})))
template_repository = optional(object({
owner = string
repository = string
include_all_branches = bool
}))
license_template = optional(string)
pages = optional(object({
source = optional(object({
branch = string
path = optional(string)
}))
build_type = optional(string)
cname = optional(string)
}))
}))
| n/a | yes | -| [rulesets](#input\_rulesets) | n/a |
map(object({
bypass_actors = optional(object({
repository_roles = optional(list(object({
role = string
always_bypass = optional(bool)
})))
teams = optional(list(object({
team = string
always_bypass = optional(bool)
})))
integrations = optional(list(object({
installation_id = number
always_bypass = optional(bool)
})))
organization_admins = optional(list(object({
user = string
always_bypass = optional(bool)
})))
}))
conditions = optional(object({
ref_name = object({
include = list(string)
exclude = list(string)
})
}))
rules = object({
branch_name_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
tag_name_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
commit_author_email_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
commit_message_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
committer_email_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
creation = optional(bool)
deletion = optional(bool)
update = optional(bool)
non_fast_forward = optional(bool)
required_linear_history = optional(bool)
required_signatures = optional(bool)
update_allows_fetch_and_merge = optional(bool)
pull_request = optional(object({
dismiss_stale_reviews_on_push = optional(bool)
require_code_owner_review = optional(bool)
require_last_push_approval = optional(bool)
required_approving_review_count = optional(number)
required_review_thread_resolution = optional(bool)
}))
required_status_checks = optional(object({
required_check = list(object({
context = string
integration_id = optional(number)
}))
strict_required_status_check_policy = optional(bool)
}))
required_deployment_environments = optional(list(string))
})
target = string
enforcement = string
repositories = list(string)
}))
| `{}` | no | +| [private\_repositories](#input\_private\_repositories) | A map of private repositories where the key is the repository name and the value is the configuration |
map(object({
description = string
default_branch = string
protected_branches = list(string)
advance_security = bool
has_vulnerability_alerts = bool
topics = list(string)
homepage = string
delete_head_on_merge = bool
requires_web_commit_signing = bool
dependabot_security_updates = bool
allow_auto_merge = optional(bool)
allow_squash_merge = optional(bool)
allow_rebase_merge = optional(bool)
allow_merge_commit = optional(bool)
squash_merge_commit_title = optional(string)
squash_merge_commit_message = optional(string)
merge_commit_title = optional(string)
merge_commit_message = optional(string)
repository_team_permissions_override = optional(map(string))
user_permissions = optional(map(string))
organization_action_secrets = optional(list(string))
organization_codespace_secrets = optional(list(string))
organization_dependabot_secrets = optional(list(string))
action_secrets = optional(map(string))
codespace_secrets = optional(map(string))
dependabot_secrets = optional(map(string))
environments = optional(map(object({
wait_timer = optional(number)
can_admins_bypass = optional(bool)
prevent_self_review = optional(bool)
action_secrets = optional(map(string))
reviewers = optional(object({
teams = optional(list(string))
users = optional(list(string))
}))
deployment_branch_policy = optional(object({
protected_branches = bool
custom_branch_policies = bool
branch_patterns = list(string)
}))
})))
template_repository = optional(object({
owner = string
repository = string
include_all_branches = bool
}))
license_template = optional(string)
pages = optional(object({
source = optional(object({
branch = string
path = optional(string)
}))
build_type = optional(string)
cname = optional(string)
}))
}))
| n/a | yes | +| [public\_repositories](#input\_public\_repositories) | A map of public repositories where the key is the repository name and the value is the configuration |
map(object({
description = string
default_branch = string
protected_branches = list(string)
advance_security = bool
topics = list(string)
homepage = string
delete_head_on_merge = bool
dependabot_security_updates = bool
requires_web_commit_signing = bool
allow_auto_merge = optional(bool)
allow_squash_merge = optional(bool)
allow_rebase_merge = optional(bool)
allow_merge_commit = optional(bool)
squash_merge_commit_title = optional(string)
squash_merge_commit_message = optional(string)
merge_commit_title = optional(string)
merge_commit_message = optional(string)
repository_team_permissions_override = optional(map(string))
user_permissions = optional(map(string))
organization_action_secrets = optional(list(string))
organization_codespace_secrets = optional(list(string))
organization_dependabot_secrets = optional(list(string))
action_secrets = optional(map(string))
codespace_secrets = optional(map(string))
dependabot_secrets = optional(map(string))
environments = optional(map(object({
wait_timer = optional(number)
can_admins_bypass = optional(bool)
prevent_self_review = optional(bool)
action_secrets = optional(map(string))
reviewers = optional(object({
teams = optional(list(string))
users = optional(list(string))
}))
deployment_branch_policy = optional(object({
protected_branches = bool
custom_branch_policies = bool
branch_patterns = list(string)
}))
})))
template_repository = optional(object({
owner = string
repository = string
include_all_branches = bool
}))
license_template = optional(string)
pages = optional(object({
source = optional(object({
branch = string
path = optional(string)
}))
build_type = optional(string)
cname = optional(string)
}))
}))
| n/a | yes | +| [rulesets](#input\_rulesets) | n/a |
map(object({
bypass_actors = optional(object({
repository_roles = optional(list(object({
role = string
always_bypass = optional(bool)
})))
teams = optional(list(object({
team = string
always_bypass = optional(bool)
})))
integrations = optional(list(object({
installation_id = number
always_bypass = optional(bool)
})))
organization_admins = optional(list(object({
user = string
always_bypass = optional(bool)
})))
}))
conditions = optional(object({
ref_name = object({
include = list(string)
exclude = list(string)
})
}))
rules = object({
branch_name_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
tag_name_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
commit_author_email_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
commit_message_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
committer_email_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
creation = optional(bool)
deletion = optional(bool)
update = optional(bool)
non_fast_forward = optional(bool)
required_linear_history = optional(bool)
required_signatures = optional(bool)
update_allows_fetch_and_merge = optional(bool)
pull_request = optional(object({
dismiss_stale_reviews_on_push = optional(bool)
require_code_owner_review = optional(bool)
require_last_push_approval = optional(bool)
required_approving_review_count = optional(number)
required_review_thread_resolution = optional(bool)
}))
required_status_checks = optional(object({
required_check = list(object({
context = string
integration_id = optional(number)
}))
strict_required_status_check_policy = optional(bool)
}))
required_deployment_environments = optional(list(string))
})
target = string
enforcement = string
repositories = list(string)
}))
| `{}` | no | ## Outputs diff --git a/modules/repository_set/organization_secrets.tftest.hcl b/modules/repository_set/organization_secrets.tftest.hcl new file mode 100644 index 0000000..f9d8495 --- /dev/null +++ b/modules/repository_set/organization_secrets.tftest.hcl @@ -0,0 +1,75 @@ +mock_provider "github" {} + +variables { + public_repositories = { + + "github-foundations-modules" = { + # required + description = "A collection of terraform modules used in the Github Foundations framework." + default_branch = "main" + protected_branches = ["main"] + advance_security = false + has_vulnerability_alerts = true + topics = ["terraform", "github", "foundations"] + homepage = "myhomepage" + delete_head_on_merge = false + dependabot_security_updates = true + requires_web_commit_signing = false + + # secrets + organization_action_secrets = ["org_secret1", "org_secret2"] + organization_codespace_secrets = ["org_codespace_secret1", "org_codespace_secret2"] + organization_dependabot_secrets = ["org_dependabot_secret1", "org_dependabot_secret2"] + + } + } + + private_repositories = {} +} + +run "organization_actions_secrets_test" { + command = apply + + assert { + condition = github_actions_organization_secret_repositories.org__action_secret_repo_access["org_secret1"].secret_name == var.public_repositories["github-foundations-modules"].organization_action_secrets[0] + error_message = "The repository id value is incorrect. Expected ${var.public_repositories["github-foundations-modules"].organization_action_secrets[0]}, got ${github_actions_organization_secret_repositories.org__action_secret_repo_access["org_secret1"].secret_name}" + } + assert { + condition = github_actions_organization_secret_repositories.org__action_secret_repo_access["org_secret2"].secret_name == var.public_repositories["github-foundations-modules"].organization_action_secrets[1] + error_message = "The repository id value is incorrect. Expected ${var.public_repositories["github-foundations-modules"].organization_action_secrets[1]}, got ${github_actions_organization_secret_repositories.org__action_secret_repo_access["org_secret2"].secret_name}" + } + assert { + condition = tolist(github_actions_organization_secret_repositories.org__action_secret_repo_access["org_secret1"].selected_repository_ids)[0] == 0 + error_message = "The repository id value is incorrect. Expected [0], got ${tolist(github_actions_organization_secret_repositories.org__action_secret_repo_access["org_secret1"].selected_repository_ids)[0]}" + } +} + +run "organization_codespaces_secrets_test" { + assert { + condition = github_codespaces_organization_secret_repositories.org__codespace_secret_repo_access["org_codespace_secret1"].secret_name == var.public_repositories["github-foundations-modules"].organization_codespace_secrets[0] + error_message = "The repository id value is incorrect. Expected ${var.public_repositories["github-foundations-modules"].organization_codespace_secrets[0]}, got ${github_codespaces_organization_secret_repositories.org__codespace_secret_repo_access["org_codespace_secret1"].secret_name}" + } + assert { + condition = github_codespaces_organization_secret_repositories.org__codespace_secret_repo_access["org_codespace_secret2"].secret_name == var.public_repositories["github-foundations-modules"].organization_codespace_secrets[1] + error_message = "The repository id value is incorrect. Expected ${var.public_repositories["github-foundations-modules"].organization_codespace_secrets[1]}, got ${github_codespaces_organization_secret_repositories.org__codespace_secret_repo_access["org_codespace_secret2"].secret_name}" + } + assert { + condition = tolist(github_codespaces_organization_secret_repositories.org__codespace_secret_repo_access["org_codespace_secret1"].selected_repository_ids)[0] == 0 + error_message = "The repository id value is incorrect. Expected [0], got ${tolist(github_codespaces_organization_secret_repositories.org__codespace_secret_repo_access["org_codespace_secret1"].selected_repository_ids)[0]}" + } +} + +run "organization_dependabot_secrets_test" { + assert { + condition = github_dependabot_organization_secret_repositories.org__dependabot_secret_repo_access["org_dependabot_secret1"].secret_name == var.public_repositories["github-foundations-modules"].organization_dependabot_secrets[0] + error_message = "The repository id value is incorrect. Expected ${var.public_repositories["github-foundations-modules"].organization_dependabot_secrets[0]}, got ${github_dependabot_organization_secret_repositories.org__dependabot_secret_repo_access["org_dependabot_secret1"].secret_name}" + } + assert { + condition = github_dependabot_organization_secret_repositories.org__dependabot_secret_repo_access["org_dependabot_secret2"].secret_name == var.public_repositories["github-foundations-modules"].organization_dependabot_secrets[1] + error_message = "The repository id value is incorrect. Expected ${var.public_repositories["github-foundations-modules"].organization_dependabot_secrets[1]}, got ${github_dependabot_organization_secret_repositories.org__dependabot_secret_repo_access["org_dependabot_secret2"].secret_name}" + } + assert { + condition = tolist(github_dependabot_organization_secret_repositories.org__dependabot_secret_repo_access["org_dependabot_secret1"].selected_repository_ids)[0] == 0 + error_message = "The repository id value is incorrect. Expected [0], got ${tolist(github_dependabot_organization_secret_repositories.org__dependabot_secret_repo_access["org_dependabot_secret1"].selected_repository_ids)[0]}" + } +} diff --git a/modules/repository_set/repositories.tftest.hcl b/modules/repository_set/repositories.tftest.hcl new file mode 100644 index 0000000..d0ff2de --- /dev/null +++ b/modules/repository_set/repositories.tftest.hcl @@ -0,0 +1,190 @@ +mock_provider "github" {} + +variables { + public_repositories = { + + "github-foundations-modules" = { + description = "A collection of terraform modules used in the Github Foundations framework." + default_branch = "main" + protected_branches = ["main"] + advance_security = false + has_vulnerability_alerts = true + topics = ["terraform", "github", "foundations"] + homepage = "myhomepage" + delete_head_on_merge = false + dependabot_security_updates = true + requires_web_commit_signing = false + allow_auto_merge = true + allow_squash_merge = false + allow_rebase_merge = true + allow_merge_commit = false + squash_merge_commit_title = "COMMIT_OR_PR_TITLE" + squash_merge_commit_message = "COMMIT_MESSAGES" + merge_commit_title = "PR_TITLE" + merge_commit_message = "PR_BODY" + repository_team_permissions_override = { + "repo_team1" = "push" + "repo_team2" = "admin" + } + user_permissions = { + "user1" = "push" + "user2" = "admin" + } + + + action_secrets = { + "action_secret1" = "value1" + } + + codespace_secrets = { + "codespace_secret1" = "value1" + } + + dependabot_secrets = { + "dependabot_secret1" = "value1" + } + + environments = { + "env1" = { + wait_timer = 10 + can_admins_bypass = true + prevent_self_review = false + action_secrets = { + "action_secret1" = "dmFsdWUxCg==" # base64 encoded + } + reviewers = { + teams = [111, 222] + users = [55, 66] + } + deployment_branch_policy = { + protected_branches = true + custom_branch_policies = false + branch_patterns = ["main"] + } + } + } + + template_repository = { + owner = "owner" + repository = "template_repository" + include_all_branches = true + } + + license_template = "mit" + + pages = { + source = { + branch = "main" + path = "path" + } + # build_type = "build_type" + cname = "cname" + } + } + } + + private_repositories = { + private-github-foundations-modules = { + description = "A collection of terraform modules used in the Github Foundations framework." + default_branch = "main" + protected_branches = ["main"] + advance_security = false + has_vulnerability_alerts = true + topics = ["terraform", "github", "foundations"] + homepage = "myhomepage" + delete_head_on_merge = false + dependabot_security_updates = true + requires_web_commit_signing = false + allow_auto_merge = true + allow_squash_merge = false + allow_rebase_merge = true + allow_merge_commit = false + squash_merge_commit_title = "COMMIT_OR_PR_TITLE" + squash_merge_commit_message = "COMMIT_MESSAGES" + merge_commit_title = "PR_TITLE" + merge_commit_message = "PR_BODY" + repository_team_permissions_override = { + "repo_team1" = "push" + "repo_team2" = "admin" + } + user_permissions = { + "user1" = "push" + "user2" = "admin" + } + + + action_secrets = { + "action_secret1" = "value1" + } + + codespace_secrets = { + "codespace_secret1" = "value1" + } + + dependabot_secrets = { + "dependabot_secret1" = "value1" + } + + environments = { + "env1" = { + wait_timer = 10 + can_admins_bypass = true + prevent_self_review = false + action_secrets = { + "action_secret1" = "dmFsdWUxCg==" # base64 encoded + } + reviewers = { + teams = [111, 222] + users = [55, 66] + } + deployment_branch_policy = { + protected_branches = true + custom_branch_policies = false + branch_patterns = ["main"] + } + } + } + + template_repository = { + owner = "owner" + repository = "template_repository" + include_all_branches = true + } + + license_template = "mit" + + pages = { + source = { + branch = "main" + path = "path" + } + cname = "cname" + } + } + } + + has_ghas_license = true + + default_repository_team_permissions = { + GhFoundationsAdmins = "admin" + GhFoundationsDevelopers = "push" + } +} + +run "apply" { + command = apply +} + +run "public_repository_test" { + assert { + condition = module.public_repositories["github-foundations-modules"].id != null + error_message = "The repository id value is incorrect. Expected not null, got ${module.public_repositories["github-foundations-modules"].id}" + } +} + +run "private_repository_test" { + assert { + condition = module.private_repositories["private-github-foundations-modules"].id != null + error_message = "The repository id value is incorrect. Expected not null, got ${module.private_repositories["private-github-foundations-modules"].id}" + } +} diff --git a/modules/ruleset/README.md b/modules/ruleset/README.md index a50dc9d..21f8990 100644 --- a/modules/ruleset/README.md +++ b/modules/ruleset/README.md @@ -26,14 +26,14 @@ No modules. | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| -| [bypass\_actors](#input\_bypass\_actors) | An object containing fields for role, team, organization admin, and integration bypass actors. Defaults to `{}` |
object({
repository_roles = optional(list(object({
role_id = string
always_bypass = optional(bool)
})))
teams = optional(list(object({
team_id = string
always_bypass = optional(bool)
})))
integrations = optional(list(object({
installation_id = number
always_bypass = optional(bool)
})))
organization_admins = optional(list(object({
user_id = string
always_bypass = optional(bool)
})))
})
| `{}` | no | +| [bypass\_actors](#input\_bypass\_actors) | An object containing fields for role, team, organization admin, and integration bypass actors. Defaults to `{}` |
object({
repository_roles = optional(list(object({
role_id = string
always_bypass = optional(bool)
})))
teams = optional(list(object({
team_id = string
always_bypass = optional(bool)
})))
integrations = optional(list(object({
installation_id = number
always_bypass = optional(bool)
})))
organization_admins = optional(list(object({
user_id = string
always_bypass = optional(bool)
})))
})
| `{}` | no | | [enforcement](#input\_enforcement) | The enforcement level of the ruleset. Should be one of either `active`, `evaluate` or `disabled`. Defaults to `active` | `string` | `"active"` | no | | [name](#input\_name) | The name of the ruleset. | `string` | n/a | yes | | [ref\_name\_exclusions](#input\_ref\_name\_exclusions) | A list of ref names or patterns to exclude. Defaults to an empty list. If set and `ruleset_type` is set to `organization` then either `repository_name_inclusions` or `repository_name_exclusions` must be set to a list of atleast 1 string. | `list(string)` | `[]` | no | | [ref\_name\_inclusions](#input\_ref\_name\_inclusions) | A list of ref names or patterns to include. Defaults to an empty list. If set and `ruleset_type` is set to `organization` then either `repository_name_inclusions` or `repository_name_exclusions` must be set to a list of atleast 1 string. | `list(string)` | `[]` | no | | [repository\_name\_exclusions](#input\_repository\_name\_exclusions) | A list of repository names or patterns to exclude. If `ruleset_type` is set to `repository` then this field is ignored. | `list(string)` | `[]` | no | | [repository\_name\_inclusions](#input\_repository\_name\_inclusions) | A list of repository names or patterns to include. If `ruleset_type` is set to `repository` then this field is ignored. | `list(string)` | `[]` | no | -| [rules](#input\_rules) | An object containing fields for all the rule definitions the ruleset should enforce. |
object({
branch_name_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
tag_name_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
commit_author_email_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
commit_message_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
committer_email_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
creation = optional(bool)
deletion = optional(bool)
update = optional(bool)
non_fast_forward = optional(bool)
required_linear_history = optional(bool)
required_signatures = optional(bool)
update_allows_fetch_and_merge = optional(bool)
pull_request = optional(object({
dismiss_stale_reviews_on_push = optional(bool)
require_code_owner_review = optional(bool)
require_last_push_approval = optional(bool)
required_approving_review_count = optional(number)
required_review_thread_resolution = optional(bool)
}))
required_status_checks = optional(object({
required_check = list(object({
context = string
integration_id = optional(number)
}))
strict_required_status_check_policy = optional(bool)
}))
required_workflows = optional(object({
required_workflows = list(object({
repository_id = number
path = string
ref = optional(string)
}))
}))
required_deployment_environments = optional(list(string))
})
| n/a | yes | +| [rules](#input\_rules) | An object containing fields for all the rule definitions the ruleset should enforce. |
object({
branch_name_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
tag_name_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
commit_author_email_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
commit_message_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
committer_email_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
creation = optional(bool)
deletion = optional(bool)
update = optional(bool)
non_fast_forward = optional(bool)
required_linear_history = optional(bool)
required_signatures = optional(bool)
update_allows_fetch_and_merge = optional(bool)
pull_request = optional(object({
dismiss_stale_reviews_on_push = optional(bool)
require_code_owner_review = optional(bool)
require_last_push_approval = optional(bool)
required_approving_review_count = optional(number)
required_review_thread_resolution = optional(bool)
}))
required_status_checks = optional(object({
required_check = list(object({
context = string
integration_id = optional(number)
}))
strict_required_status_check_policy = optional(bool)
}))
required_workflows = optional(object({
required_workflows = list(object({
repository_id = number
path = string
ref = optional(string)
}))
}))
required_deployment_environments = optional(list(string))
})
| n/a | yes | | [ruleset\_type](#input\_ruleset\_type) | The type of rulset to make. Should be one of ether `organization` or `repository`. | `string` | n/a | yes | | [target](#input\_target) | The target of the ruleset. Should be one of either `branch` or `tag`. | `string` | n/a | yes | diff --git a/modules/ruleset/organization_ruleset.tftest.hcl b/modules/ruleset/organization_ruleset.tftest.hcl new file mode 100644 index 0000000..a066e93 --- /dev/null +++ b/modules/ruleset/organization_ruleset.tftest.hcl @@ -0,0 +1,360 @@ +mock_provider "github" {} + + +variables { + + name = "ruleset_name" + ruleset_type = "organization" + target = "tag" + enforcement = "active" + + ref_name_inclusions = ["main"] + ref_name_exclusions = ["feature/*"] + + repository_name_inclusions = ["repo1"] + repository_name_exclusions = ["repo2"] + + rules = { + creation = true + update = false + deletion = true + non_fast_forward = false + required_linear_history = true + required_signatures = false + update_allows_fetch_and_merge = true + + tag_name_pattern = { + operator = "regex" + pattern = "pattern" + name = "tag_name" + negate = false + } + commit_author_email_pattern = { + operator = "regex" + pattern = "pattern" + name = "commit_author_email" + negate = false + } + commit_message_pattern = { + operator = "regex" + pattern = "pattern" + name = "commit_message" + negate = false + } + committer_email_pattern = { + operator = "regex" + pattern = "pattern" + name = "committer_email" + negate = false + } + pull_request = { + dismiss_stale_reviews_on_push = true + require_code_owner_review = false + require_last_push_approval = true + required_approving_review_count = 2 + required_review_thread_resolution = false + } + required_status_checks = { + required_check = [ + { + context = "context" + integration_id = 555555 + }, + { + context = "context2" + integration_id = 666666 + } + ] + strict_required_status_check_policy = true + } + required_workflows = { + required_workflows = [{ + repository_id = 777777 + path = "path" + ref = "main" + }, + { + repository_id = 888888 + path = "path2" + ref = "main" + }] + } + } + + bypass_actors = { + repository_roles = [ + { + role_id = 111111 + always_bypass = false + } + ] + teams = [ + { + team_id = 222222 + always_bypass = true + } + ] + integrations = [ + { + installation_id = 333333 + always_bypass = false + } + ] + organization_admins = [ + { + user_id = 444444 + always_bypass = true + } + ] + } +} + +# Test the ruleset creation +run "create_ruleset_test" { + command = apply + + assert { + condition = github_organization_ruleset.ruleset[0].name == var.name + error_message = "The ruleset name is incorrect." + } + assert { + condition = github_organization_ruleset.ruleset[0].target == var.target + error_message = "The target type is incorrect." + } + assert { + condition = github_organization_ruleset.ruleset[0].enforcement == var.enforcement + error_message = "The enforcement mode is incorrect." + } +} + +# Test the ruleset ref name conditions +run "ruleset_ref_name_conditions_test" { + assert { + condition = github_organization_ruleset.ruleset[0].conditions[0].ref_name[0].include[0] == var.ref_name_inclusions[0] + error_message = "The ref name inclusion is incorrect." + } + assert { + condition = github_organization_ruleset.ruleset[0].conditions[0].ref_name[0].exclude[0] == var.ref_name_exclusions[0] + error_message = "The ref name exclusion is incorrect." + } +} + +# Test the ruleset repository name conditions +run "ruleset_repository_conditions_test" { + assert { + condition = github_organization_ruleset.ruleset[0].conditions[0].repository_name[0].include[0] == var.repository_name_inclusions[0] + error_message = "The repository name inclusion is incorrect." + } + assert { + condition = github_organization_ruleset.ruleset[0].conditions[0].repository_name[0].exclude[0] == var.repository_name_exclusions[0] + error_message = "The repository name exclusion is incorrect." + } +} +# Test the ruleset rules +run "ruleset_rules_test" { + assert { + condition = github_organization_ruleset.ruleset[0].rules[0].creation == var.rules.creation + error_message = "The creation rule is incorrect." + } + assert { + condition = github_organization_ruleset.ruleset[0].rules[0].update == var.rules.update + error_message = "The update rule is incorrect." + } + assert { + condition = github_organization_ruleset.ruleset[0].rules[0].deletion == var.rules.deletion + error_message = "The deletion rule is incorrect." + } + assert { + condition = github_organization_ruleset.ruleset[0].rules[0].non_fast_forward == var.rules.non_fast_forward + error_message = "The non-fast-forward rule is incorrect." + } + assert { + condition = github_organization_ruleset.ruleset[0].rules[0].required_linear_history == var.rules.required_linear_history + error_message = "The required linear history rule is incorrect." + } + assert { + condition = github_organization_ruleset.ruleset[0].rules[0].required_signatures == var.rules.required_signatures + error_message = "The required signatures rule is incorrect." + } +} + +# Test the tag name pattern rule +run "ruleset_tag_name_pattern_test" { + assert { + condition = github_organization_ruleset.ruleset[0].rules[0].tag_name_pattern[0].operator == var.rules.tag_name_pattern.operator + error_message = "The tag name pattern operator is incorrect." + } + assert { + condition = github_organization_ruleset.ruleset[0].rules[0].tag_name_pattern[0].pattern == var.rules.tag_name_pattern.pattern + error_message = "The tag name pattern is incorrect." + } + assert { + condition = github_organization_ruleset.ruleset[0].rules[0].tag_name_pattern[0].name == var.rules.tag_name_pattern.name + error_message = "The tag name pattern name is incorrect." + } + assert { + condition = github_organization_ruleset.ruleset[0].rules[0].tag_name_pattern[0].negate == var.rules.tag_name_pattern.negate + error_message = "The tag name pattern negate is incorrect." + } +} + +# Test the commit author email pattern rule +run "ruleset_commit_author_email_pattern_test" { + assert { + condition = github_organization_ruleset.ruleset[0].rules[0].commit_author_email_pattern[0].operator == var.rules.commit_author_email_pattern.operator + error_message = "The commit author email pattern operator is incorrect." + } + assert { + condition = github_organization_ruleset.ruleset[0].rules[0].commit_author_email_pattern[0].pattern == var.rules.commit_author_email_pattern.pattern + error_message = "The commit author email pattern is incorrect." + } + assert { + condition = github_organization_ruleset.ruleset[0].rules[0].commit_author_email_pattern[0].name == var.rules.commit_author_email_pattern.name + error_message = "The commit author email pattern name is incorrect." + } + assert { + condition = github_organization_ruleset.ruleset[0].rules[0].commit_author_email_pattern[0].negate == var.rules.commit_author_email_pattern.negate + error_message = "The commit author email pattern negate is incorrect." + } +} + +# Test the commit message pattern rule +run "ruleset_commit_message_pattern_test" { + assert { + condition = github_organization_ruleset.ruleset[0].rules[0].commit_message_pattern[0].operator == var.rules.commit_message_pattern.operator + error_message = "The commit message pattern operator is incorrect." + } + assert { + condition = github_organization_ruleset.ruleset[0].rules[0].commit_message_pattern[0].pattern == var.rules.commit_message_pattern.pattern + error_message = "The commit message pattern is incorrect." + } + assert { + condition = github_organization_ruleset.ruleset[0].rules[0].commit_message_pattern[0].name == var.rules.commit_message_pattern.name + error_message = "The commit message pattern name is incorrect." + } + assert { + condition = github_organization_ruleset.ruleset[0].rules[0].commit_message_pattern[0].negate == var.rules.commit_message_pattern.negate + error_message = "The commit message pattern negate is incorrect." + } +} + +# Test the committer email pattern rule +run "ruleset_committer_email_pattern_test" { + assert { + condition = github_organization_ruleset.ruleset[0].rules[0].committer_email_pattern[0].operator == var.rules.committer_email_pattern.operator + error_message = "The committer email pattern operator is incorrect." + } + assert { + condition = github_organization_ruleset.ruleset[0].rules[0].committer_email_pattern[0].pattern == var.rules.committer_email_pattern.pattern + error_message = "The committer email pattern is incorrect." + } + assert { + condition = github_organization_ruleset.ruleset[0].rules[0].committer_email_pattern[0].name == var.rules.committer_email_pattern.name + error_message = "The committer email pattern name is incorrect." + } + assert { + condition = github_organization_ruleset.ruleset[0].rules[0].committer_email_pattern[0].negate == var.rules.committer_email_pattern.negate + error_message = "The committer email pattern negate is incorrect." + } +} + +# Test the pull request rule +run "ruleset_pull_request_test" { + assert { + condition = github_organization_ruleset.ruleset[0].rules[0].pull_request[0].dismiss_stale_reviews_on_push == var.rules.pull_request.dismiss_stale_reviews_on_push + error_message = "The pull request dismiss stale reviews on push rule is incorrect." + } + assert { + condition = github_organization_ruleset.ruleset[0].rules[0].pull_request[0].require_code_owner_review == var.rules.pull_request.require_code_owner_review + error_message = "The pull request require code owner reviews rule is incorrect." + } + assert { + condition = github_organization_ruleset.ruleset[0].rules[0].pull_request[0].require_last_push_approval == var.rules.pull_request.require_last_push_approval + error_message = "The pull request require last push approval rule is incorrect." + } + assert { + condition = github_organization_ruleset.ruleset[0].rules[0].pull_request[0].required_approving_review_count == var.rules.pull_request.required_approving_review_count + error_message = "The pull request required approving review count rule is incorrect." + } + assert { + condition = github_organization_ruleset.ruleset[0].rules[0].pull_request[0].required_review_thread_resolution == var.rules.pull_request.required_review_thread_resolution + error_message = "The pull request required review thread resolution rule is incorrect." + } +} + +# Test the required status checks rule +run "ruleset_required_status_checks_test" { + # Can't test the required checks because the tf test framework doesn't + # support lists of objects yet. + assert { + condition = github_organization_ruleset.ruleset[0].rules[0].required_status_checks[0].strict_required_status_checks_policy == var.rules.required_status_checks.strict_required_status_check_policy + error_message = "The required status checks context is incorrect." + } +} + +# Tests for the required workflows rule not currently supported by the +# tf test framework, as it doesn't support lists of objects yet. + +# Test the repository bypass actors +run "bypass_actor_repository_roles_test" { + assert { + condition = github_organization_ruleset.ruleset[0].bypass_actors[0].actor_id == tonumber(var.bypass_actors.repository_roles[0].role_id) + error_message = "The bypass actor role id is incorrect." + } + assert { + condition = github_organization_ruleset.ruleset[0].bypass_actors[0].bypass_mode == "pull_request" + error_message = "The bypass actor role bypass mode is incorrect." + } + assert { + condition = github_organization_ruleset.ruleset[0].bypass_actors[0].actor_type == "RepositoryRole" + error_message = "The bypass actor type is incorrect." + } +} + +# Test the Team bypass actors +run "bypass_actor_teams_test" { + assert { + condition = github_organization_ruleset.ruleset[0].bypass_actors[1].actor_id == tonumber(var.bypass_actors.teams[0].team_id) + error_message = "The bypass actor team id is incorrect." + } + assert { + condition = github_organization_ruleset.ruleset[0].bypass_actors[1].bypass_mode == "always" + error_message = "The bypass actor team bypass mode is incorrect." + } + assert { + condition = github_organization_ruleset.ruleset[0].bypass_actors[1].actor_type == "Team" + error_message = "The bypass actor type is incorrect." + } +} + +# Test the Integration bypass actors +run "bypass_actor_integrations_test" { + assert { + condition = github_organization_ruleset.ruleset[0].bypass_actors[2].actor_id == tonumber(var.bypass_actors.integrations[0].installation_id) + error_message = "The bypass actor integration id is incorrect." + } + assert { + condition = github_organization_ruleset.ruleset[0].bypass_actors[2].bypass_mode == "pull_request" + error_message = "The bypass actor integration bypass mode is incorrect." + } + assert { + condition = github_organization_ruleset.ruleset[0].bypass_actors[2].actor_type == "Integration" + error_message = "The bypass actor type is incorrect." + } +} + +# Test the Organization Admin bypass actors +run "bypass_actor_organization_admins_test" { + assert { + condition = github_organization_ruleset.ruleset[0].bypass_actors[3].actor_id == tonumber(var.bypass_actors.organization_admins[0].user_id) + error_message = "The bypass actor organization admin id is incorrect." + } + assert { + condition = github_organization_ruleset.ruleset[0].bypass_actors[3].bypass_mode == "always" + error_message = "The bypass actor organization admin bypass mode is incorrect." + } + assert { + condition = github_organization_ruleset.ruleset[0].bypass_actors[3].actor_type == "OrganizationAdmin" + error_message = "The bypass actor type is incorrect." + } +} diff --git a/modules/ruleset/repository_ruleset.tftest.hcl b/modules/ruleset/repository_ruleset.tftest.hcl new file mode 100644 index 0000000..77e7197 --- /dev/null +++ b/modules/ruleset/repository_ruleset.tftest.hcl @@ -0,0 +1,348 @@ +mock_provider "github" {} + + +variables { + + name = "ruleset_name" + ruleset_type = "repository" + target = "tag" + enforcement = "disabled" + + ref_name_inclusions = ["main"] + ref_name_exclusions = ["feature/*"] + + rules = { + creation = true + update = false + deletion = true + non_fast_forward = false + required_linear_history = true + required_signatures = false + update_allows_fetch_and_merge = true + + branch_name_pattern = { + operator = "regex" + pattern = "pattern" + name = "branch_name" + negate = false + } + commit_author_email_pattern = { + operator = "regex" + pattern = "pattern" + name = "commit_author_email" + negate = false + } + commit_message_pattern = { + operator = "regex" + pattern = "pattern" + name = "commit_message" + negate = false + } + committer_email_pattern = { + operator = "regex" + pattern = "pattern" + name = "committer_email" + negate = false + } + pull_request = { + dismiss_stale_reviews_on_push = true + require_code_owner_review = false + require_last_push_approval = true + required_approving_review_count = 2 + required_review_thread_resolution = false + } + required_status_checks = { + required_check = [ + { + context = "context" + integration_id = 555555 + }, + { + context = "context2" + integration_id = 666666 + } + ] + strict_required_status_check_policy = true + } + required_deployment_environments = ["env1", "env2"] + } + + bypass_actors = { + repository_roles = [ + { + role_id = 111111 + bypass_mode = false + } + ] + teams = [ + { + team_id = 222222 + always_bypass = true + } + ] + integrations = [ + { + installation_id = 333333 + always_bypass = false + } + ] + organization_admins = [ + { + user_id = 444444 + always_bypass = true + } + ] + } +} + +# Test the ruleset creation +run "create_ruleset_test" { + command = apply + + assert { + condition = github_repository_ruleset.ruleset[0].name == var.name + error_message = "The ruleset name is incorrect." + } + assert { + condition = github_repository_ruleset.ruleset[0].target == var.target + error_message = "The target type is incorrect." + } + assert { + condition = github_repository_ruleset.ruleset[0].enforcement == var.enforcement + error_message = "The enforcement mode is incorrect." + } +} + +# Test the ruleset conditions +run "ruleset_conditions_test" { + assert { + condition = github_repository_ruleset.ruleset[0].conditions[0].ref_name[0].include[0] == var.ref_name_inclusions[0] + error_message = "The ref name inclusion is incorrect." + } + assert { + condition = github_repository_ruleset.ruleset[0].conditions[0].ref_name[0].exclude[0] == var.ref_name_exclusions[0] + error_message = "The ref name exclusion is incorrect." + } +} + +# Test the ruleset rules +run "ruleset_rules_test" { + assert { + condition = github_repository_ruleset.ruleset[0].rules[0].creation == var.rules.creation + error_message = "The creation rule is incorrect." + } + assert { + condition = github_repository_ruleset.ruleset[0].rules[0].update == var.rules.update + error_message = "The update rule is incorrect." + } + assert { + condition = github_repository_ruleset.ruleset[0].rules[0].deletion == var.rules.deletion + error_message = "The deletion rule is incorrect." + } + assert { + condition = github_repository_ruleset.ruleset[0].rules[0].non_fast_forward == var.rules.non_fast_forward + error_message = "The non-fast-forward rule is incorrect." + } + assert { + condition = github_repository_ruleset.ruleset[0].rules[0].required_linear_history == var.rules.required_linear_history + error_message = "The required linear history rule is incorrect." + } + assert { + condition = github_repository_ruleset.ruleset[0].rules[0].required_signatures == var.rules.required_signatures + error_message = "The required signatures rule is incorrect." + } + assert { + condition = github_repository_ruleset.ruleset[0].rules[0].update_allows_fetch_and_merge == var.rules.update_allows_fetch_and_merge + error_message = "The update allows fetch and merge rule is incorrect." + } +} + +# Test the branch name pattern rule +run "ruleset_branch_name_pattern_test" { + assert { + condition = github_repository_ruleset.ruleset[0].rules[0].branch_name_pattern[0].operator == var.rules.branch_name_pattern.operator + error_message = "The branch name pattern operator is incorrect." + } + assert { + condition = github_repository_ruleset.ruleset[0].rules[0].branch_name_pattern[0].pattern == var.rules.branch_name_pattern.pattern + error_message = "The branch name pattern is incorrect." + } + assert { + condition = github_repository_ruleset.ruleset[0].rules[0].branch_name_pattern[0].name == var.rules.branch_name_pattern.name + error_message = "The branch name pattern name is incorrect." + } + assert { + condition = github_repository_ruleset.ruleset[0].rules[0].branch_name_pattern[0].negate == var.rules.branch_name_pattern.negate + error_message = "The branch name pattern negate is incorrect." + } +} + +# Test the commit author email pattern rule +run "ruleset_commit_author_email_pattern_test" { + assert { + condition = github_repository_ruleset.ruleset[0].rules[0].commit_author_email_pattern[0].operator == var.rules.commit_author_email_pattern.operator + error_message = "The commit author email pattern operator is incorrect." + } + assert { + condition = github_repository_ruleset.ruleset[0].rules[0].commit_author_email_pattern[0].pattern == var.rules.commit_author_email_pattern.pattern + error_message = "The commit author email pattern is incorrect." + } + assert { + condition = github_repository_ruleset.ruleset[0].rules[0].commit_author_email_pattern[0].name == var.rules.commit_author_email_pattern.name + error_message = "The commit author email pattern name is incorrect." + } + assert { + condition = github_repository_ruleset.ruleset[0].rules[0].commit_author_email_pattern[0].negate == var.rules.commit_author_email_pattern.negate + error_message = "The commit author email pattern negate is incorrect." + } +} + +# Test the commit message pattern rule +run "ruleset_commit_message_pattern_test" { + assert { + condition = github_repository_ruleset.ruleset[0].rules[0].commit_message_pattern[0].operator == var.rules.commit_message_pattern.operator + error_message = "The commit message pattern operator is incorrect." + } + assert { + condition = github_repository_ruleset.ruleset[0].rules[0].commit_message_pattern[0].pattern == var.rules.commit_message_pattern.pattern + error_message = "The commit message pattern is incorrect." + } + assert { + condition = github_repository_ruleset.ruleset[0].rules[0].commit_message_pattern[0].name == var.rules.commit_message_pattern.name + error_message = "The commit message pattern name is incorrect." + } + assert { + condition = github_repository_ruleset.ruleset[0].rules[0].commit_message_pattern[0].negate == var.rules.commit_message_pattern.negate + error_message = "The commit message pattern negate is incorrect." + } +} + +# Test the committer email pattern rule +run "ruleset_committer_email_pattern_test" { + assert { + condition = github_repository_ruleset.ruleset[0].rules[0].committer_email_pattern[0].operator == var.rules.committer_email_pattern.operator + error_message = "The committer email pattern operator is incorrect." + } + assert { + condition = github_repository_ruleset.ruleset[0].rules[0].committer_email_pattern[0].pattern == var.rules.committer_email_pattern.pattern + error_message = "The committer email pattern is incorrect." + } + assert { + condition = github_repository_ruleset.ruleset[0].rules[0].committer_email_pattern[0].name == var.rules.committer_email_pattern.name + error_message = "The committer email pattern name is incorrect." + } + assert { + condition = github_repository_ruleset.ruleset[0].rules[0].committer_email_pattern[0].negate == var.rules.committer_email_pattern.negate + error_message = "The committer email pattern negate is incorrect." + } +} + +# Test the pull request rule +run "ruleset_pull_request_test" { + assert { + condition = github_repository_ruleset.ruleset[0].rules[0].pull_request[0].dismiss_stale_reviews_on_push == var.rules.pull_request.dismiss_stale_reviews_on_push + error_message = "The pull request dismiss stale reviews on push rule is incorrect." + } + assert { + condition = github_repository_ruleset.ruleset[0].rules[0].pull_request[0].require_code_owner_review == var.rules.pull_request.require_code_owner_review + error_message = "The pull request require code owner reviews rule is incorrect." + } + assert { + condition = github_repository_ruleset.ruleset[0].rules[0].pull_request[0].require_last_push_approval == var.rules.pull_request.require_last_push_approval + error_message = "The pull request require last push approval rule is incorrect." + } + assert { + condition = github_repository_ruleset.ruleset[0].rules[0].pull_request[0].required_approving_review_count == var.rules.pull_request.required_approving_review_count + error_message = "The pull request required approving review count rule is incorrect." + } + assert { + condition = github_repository_ruleset.ruleset[0].rules[0].pull_request[0].required_review_thread_resolution == var.rules.pull_request.required_review_thread_resolution + error_message = "The pull request required review thread resolution rule is incorrect." + } +} + +# Test the required status checks rule +run "ruleset_required_status_checks_test" { + # Can't test the required checks because the tf test framework doesn't + # support lists of objects yet. + assert { + condition = github_repository_ruleset.ruleset[0].rules[0].required_status_checks[0].strict_required_status_checks_policy == var.rules.required_status_checks.strict_required_status_check_policy + error_message = "The required status checks context is incorrect." + } +} + +# Test the required deployments rule +run "ruleset_required_deployments_test" { + assert { + condition = github_repository_ruleset.ruleset[0].rules[0].required_deployments[0].required_deployment_environments[0] == var.rules.required_deployment_environments[0] + error_message = "The required deployment environments is incorrect." + } + assert { + condition = github_repository_ruleset.ruleset[0].rules[0].required_deployments[0].required_deployment_environments[1] == var.rules.required_deployment_environments[1] + error_message = "The required deployment environments is incorrect." + } +} + +# Test the repository bypass actors +run "bypass_actor_repository_roles_test" { + assert { + condition = github_repository_ruleset.ruleset[0].bypass_actors[0].actor_id == tonumber(var.bypass_actors.repository_roles[0].role_id) + error_message = "The bypass actor role id is incorrect." + } + assert { + condition = github_repository_ruleset.ruleset[0].bypass_actors[0].bypass_mode == "pull_request" + error_message = "The bypass actor role bypass mode is incorrect." + } + assert { + condition = github_repository_ruleset.ruleset[0].bypass_actors[0].actor_type == "RepositoryRole" + error_message = "The bypass actor type is incorrect." + } +} + +# Test the Team bypass actors +run "bypass_actor_teams_test" { + assert { + condition = github_repository_ruleset.ruleset[0].bypass_actors[1].actor_id == tonumber(var.bypass_actors.teams[0].team_id) + error_message = "The bypass actor team id is incorrect." + } + assert { + condition = github_repository_ruleset.ruleset[0].bypass_actors[1].bypass_mode == "always" + error_message = "The bypass actor team bypass mode is incorrect." + } + assert { + condition = github_repository_ruleset.ruleset[0].bypass_actors[1].actor_type == "Team" + error_message = "The bypass actor type is incorrect." + } +} + +# Test the Integration bypass actors +run "bypass_actor_integrations_test" { + assert { + condition = github_repository_ruleset.ruleset[0].bypass_actors[2].actor_id == tonumber(var.bypass_actors.integrations[0].installation_id) + error_message = "The bypass actor integration id is incorrect." + } + assert { + condition = github_repository_ruleset.ruleset[0].bypass_actors[2].bypass_mode == "pull_request" + error_message = "The bypass actor integration bypass mode is incorrect." + } + assert { + condition = github_repository_ruleset.ruleset[0].bypass_actors[2].actor_type == "Integration" + error_message = "The bypass actor type is incorrect." + } +} + +# Test the Organization Admin bypass actors +run "bypass_actor_organization_admins_test" { + assert { + condition = github_repository_ruleset.ruleset[0].bypass_actors[3].actor_id == tonumber(var.bypass_actors.organization_admins[0].user_id) + error_message = "The bypass actor organization admin id is incorrect." + } + assert { + condition = github_repository_ruleset.ruleset[0].bypass_actors[3].bypass_mode == "always" + error_message = "The bypass actor organization admin bypass mode is incorrect." + } + assert { + condition = github_repository_ruleset.ruleset[0].bypass_actors[3].actor_type == "OrganizationAdmin" + error_message = "The bypass actor type is incorrect." + } +} diff --git a/modules/team/team.tftest.hcl b/modules/team/team.tftest.hcl new file mode 100644 index 0000000..3372213 --- /dev/null +++ b/modules/team/team.tftest.hcl @@ -0,0 +1,46 @@ +mock_provider "github" { +} + +variables { + team_name = "team 1" + team_description = "This is a test team" + privacy = "closed" + team_maintainers = ["user1", "user2"] + team_members = ["user3", "user4", "user5"] + team_id = "888" + parent_id = "777" +} + +run "team_test" { + + command = apply + + assert { + condition = github_team_membership.maintainers[var.team_maintainers[0]].team_id == var.team_id + error_message = "The maintainer's team id is incorrect. Expected: ${var.team_id}, Actual: ${github_team_membership.maintainers[var.team_maintainers[0]].team_id}" + } + assert { + condition = github_team_membership.maintainers[var.team_maintainers[0]].username == var.team_maintainers[0] + error_message = "The maintainer's username is incorrect. Expected: ${var.team_maintainers[0]}, Actual: ${github_team_membership.maintainers[var.team_maintainers[0]].username}" + } + assert { + condition = github_team_membership.maintainers[var.team_maintainers[0]].role == "maintainer" + error_message = "The maintainer's role is incorrect. Expected: maintainer, Actual: ${github_team_membership.maintainers[var.team_maintainers[0]].role}" + } +} + +run "team_member_test" { + assert { + condition = github_team_membership.members[var.team_members[0]].team_id == var.team_id + error_message = "The member's team id is incorrect. Expected: ${var.team_id}, Actual: ${github_team_membership.members[var.team_members[0]].team_id}" + } + assert { + condition = github_team_membership.members[var.team_members[0]].username == var.team_members[0] + error_message = "The member's username is incorrect. Expected: ${var.team_members[0]}, Actual: ${github_team_membership.members[var.team_members[0]].username}" + } + assert { + condition = github_team_membership.members[var.team_members[0]].role == "member" + error_message = "The member's role is incorrect. Expected: member, Actual: ${github_team_membership.members[var.team_members[0]].role}" + } + +} diff --git a/modules/team_set/README.md b/modules/team_set/README.md index fcf9e7a..3ffd7da 100644 --- a/modules/team_set/README.md +++ b/modules/team_set/README.md @@ -28,8 +28,8 @@ | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| -| [preexisting\_teams](#input\_preexisting\_teams) | A map of existing teams where the key is the team name and the value is the configuration. If the team does not have a parent team, the parent\_id should be an empty string. |
map(object({
bucket = string
prefix = string
output_name = string
maintainers = list(string)
members = list(string)
parent_id = string
}))
| `{}` | no | -| [teams](#input\_teams) | A map of teams to create where the key is the team name and the value is the configuration. If the team does not have a parent team, the parent\_id should be an empty string. |
map(object({
description = string
privacy = string
maintainers = list(string)
members = list(string)
parent_id = string
}))
| n/a | yes | +| [preexisting\_teams](#input\_preexisting\_teams) | A map of existing teams where the key is the team name and the value is the configuration. If the team does not have a parent team, the parent\_id should be an empty string. |
map(object({
bucket = string
prefix = string
output_name = string
maintainers = list(string)
members = list(string)
parent_id = string
}))
| `{}` | no | +| [teams](#input\_teams) | A map of teams to create where the key is the team name and the value is the configuration. If the team does not have a parent team, the parent\_id should be an empty string. |
map(object({
description = string
privacy = string
maintainers = list(string)
members = list(string)
parent_id = string
}))
| n/a | yes | ## Outputs diff --git a/modules/team_set/teams.tftest.hcl b/modules/team_set/teams.tftest.hcl new file mode 100644 index 0000000..0f06f6b --- /dev/null +++ b/modules/team_set/teams.tftest.hcl @@ -0,0 +1,56 @@ +mock_provider "github" { + mock_resource "github_team" { + defaults = { + slug = "team-1" + } + + } +} + +variables { + teams = { + "team-1" = { + maintainers = ["user1"] + members = ["user2"] + description = "This is a test team" + privacy = "closed" + parent_id = 777 + }, + "team-2" = { + maintainers = ["user1", "user2"] + members = ["user3"] + description = "This is another test team" + privacy = "closed" + parent_id = 888 + }, + "team-3" = { + maintainers = ["user1", "user2", "user3"] + members = ["user4", "user5"] + description = "This is a third test team" + privacy = "secret" + parent_id = 999 + } + } +} + +run "create_teams" { + + command = apply + + assert { + condition = module.team.team-1.name == "team-1" + error_message = "The team name is incorrect. Expected team-1 but got ${module.team.team-1.name}" + } + assert { + condition = module.team.team-1.slug == "team-1" + error_message = "The team slug is incorrect. Expected team-1 but got ${module.team.team-1.slug}" + } + assert { + condition = module.team.team-2.name == "team-2" + error_message = "The team name is incorrect. Expected team-2 but got ${module.team.team-2.name}" + } + assert { + condition = module.team["team-3"].name == "team-3" + error_message = "The team name is incorrect. Expected team-3 but got ${module.team.team-3.name}" + } +}