diff --git a/.github/workflows/pipeline.yaml b/.github/workflows/pipeline.yaml new file mode 100644 index 000000000..81934dbac --- /dev/null +++ b/.github/workflows/pipeline.yaml @@ -0,0 +1,83 @@ +name: Push + +on: + push: + branches: + - master + +env: + TF_LOG: INFO + +permissions: + id-token: write + contents: read + +jobs: + deploy-infra: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Setup Terraform + uses: hashicorp/setup-terraform@v2 + + # Log into Azure with OIDC integration + - name: 'Az CLI login' + uses: azure/login@v1 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.DCD_CFT_SANDBOX_SUBSCRIPTION }} + + - name: Install jq + run: | + sudo apt-get install jq -y + + # List all the repos and branches that the rules are going to be applied on + - name: List Repositories and Branches + run: | + echo "Checking Repositories and Branches" + cat ./test-repos.json + for repo in $(jq -r '.[]' ./test-repos.json); do + echo "Checking repository: $repo" + curl -H "Authorization: token ${{ secrets.PAT_TOKEN }}" \ + -H "Accept: application/vnd.github.v3+json" \ + https://api.github.com/repos/hmcts/$repo + + for branch in main master; do + echo "Checking branch: $branch in repository: $repo" + curl -H "Authorization: token ${{ secrets.PAT_TOKEN }}" \ + -H "Accept: application/vnd.github.v3+json" \ + https://api.github.com/repos/hmcts/$repo/branches/$branch + done + done + + - name: Terraform Init + working-directory: components + env: + STORAGE_ACCOUNT: ${{ secrets.STORAGE_ACCOUNT }} + CONTAINER_NAME: ${{ secrets.CONTAINER_NAME }} + RESOURCE_GROUP_NAME: ${{ secrets.RESOURCE_GROUP_NAME }} + ARM_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} + ARM_SUBSCRIPTION_ID: ${{ secrets.DCD_CFT_SANDBOX_SUBSCRIPTION }} + ARM_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} + run: terraform init -backend-config="storage_account_name=$STORAGE_ACCOUNT" -backend-config="container_name=$CONTAINER_NAME" -backend-config="resource_group_name=$RESOURCE_GROUP_NAME" + + - name: Terraform Plan + working-directory: components + env: + ARM_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} + ARM_SUBSCRIPTION_ID: ${{ secrets.DCD_CFT_SANDBOX_SUBSCRIPTION }} + ARM_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} + GITHUB_TOKEN: ${{ secrets.PAT_TOKEN }} + run: terraform plan -var="github_token=${{ secrets.PAT_TOKEN }}" + + - name: Terraform Apply + working-directory: components + env: + ARM_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} + ARM_SUBSCRIPTION_ID: ${{ secrets.DCD_CFT_SANDBOX_SUBSCRIPTION }} + ARM_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} + GITHUB_TOKEN: ${{ secrets.PAT_TOKEN }} + run: terraform apply -var="github_token=${{ secrets.PAT_TOKEN }}" -auto-approve diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml new file mode 100644 index 000000000..433249102 --- /dev/null +++ b/.github/workflows/pr.yaml @@ -0,0 +1,93 @@ +name: Pull Request + +on: + pull_request: + branches: + - master + +env: + TF_LOG: INFO + +permissions: + id-token: write + issues: write + pull-requests: write + contents: read + +jobs: + pr-infra-check: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Setup Terraform + uses: hashicorp/setup-terraform@v2 + + # Log into Azure with OIDC integration + - name: 'Az CLI login' + uses: azure/login@v1 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.DCD_CFT_SANDBOX_SUBSCRIPTION }} + + - name: Install jq + run: sudo apt-get install jq -y + + # List all the repos and branches that the rules are going to be applied on + - name: List Repositories and Branches + run: | + echo "Checking Repositories and Branches" + cat ./test-repos.json + for repo in $(jq -r '.[]' ./test-repos.json); do + echo "Checking repository: $repo" + curl -H "Authorization: token ${{ secrets.PAT_TOKEN }}" \ + -H "Accept: application/vnd.github.v3+json" \ + https://api.github.com/repos/hmcts/$repo + + for branch in main master; do + echo "Checking branch: $branch in repository: $repo" + curl -H "Authorization: token ${{ secrets.PAT_TOKEN }}" \ + -H "Accept: application/vnd.github.v3+json" \ + https://api.github.com/repos/hmcts/$repo/branches/$branch + done + done + + - name: Terraform Init + working-directory: components + env: + STORAGE_ACCOUNT: ${{ secrets.STORAGE_ACCOUNT }} + CONTAINER_NAME: ${{ secrets.CONTAINER_NAME }} + RESOURCE_GROUP_NAME: ${{ secrets.RESOURCE_GROUP_NAME }} + ARM_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} + ARM_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} + ARM_SUBSCRIPTION_ID: ${{ secrets.DCD_CFT_SANDBOX_SUBSCRIPTION }} + ARM_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} + run: terraform init -backend-config="storage_account_name=$STORAGE_ACCOUNT" -backend-config="container_name=$CONTAINER_NAME" -backend-config="resource_group_name=$RESOURCE_GROUP_NAME" + + - name: Terraform format + working-directory: components + id: fmt + run: terraform fmt -check + + - name: Terraform validate + working-directory: components + id: validate + env: + ARM_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} + ARM_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} + ARM_SUBSCRIPTION_ID: ${{ secrets.DCD_CFT_SANDBOX_SUBSCRIPTION }} + ARM_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} + run: terraform validate + + - name: Terraform plan + working-directory: components + id: plan + env: + ARM_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} + ARM_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} + ARM_SUBSCRIPTION_ID: ${{ secrets.DCD_CFT_SANDBOX_SUBSCRIPTION }} + ARM_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} + GITHUB_TOKEN: ${{ secrets.PAT_TOKEN }} + run: terraform plan -var="github_token=${{ secrets.PAT_TOKEN }}" diff --git a/components/.gitignore b/components/.gitignore new file mode 100644 index 000000000..9b8a46e69 --- /dev/null +++ b/components/.gitignore @@ -0,0 +1,34 @@ +# Local .terraform directories +**/.terraform/* + +# .tfstate files +*.tfstate +*.tfstate.* + +# Crash log files +crash.log +crash.*.log + +# Exclude all .tfvars files, which are likely to contain sensitive data, such as +# password, private keys, and other secrets. These should not be part of version +# control as they are data points which are potentially sensitive and subject +# to change depending on the environment. +*.tfvars +*.tfvars.json + +# Ignore override files as they are usually used to override resources locally and so +# are not checked in +override.tf +override.tf.json +*_override.tf +*_override.tf.json + +# Include override files you do wish to add to version control using negated pattern +# !example_override.tf + +# Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan +# example: *tfplan* + +# Ignore CLI configuration files +.terraformrc +terraform.rc diff --git a/components/.terraform.lock.hcl b/components/.terraform.lock.hcl new file mode 100644 index 000000000..d9a377f29 --- /dev/null +++ b/components/.terraform.lock.hcl @@ -0,0 +1,44 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/azurerm" { + version = "3.109.0" + constraints = "3.109.0" + hashes = [ + "h1:tb3a5x6HV4YRxyL3VpdTWe1vsKocKi1HT0KFWnF5ZjM=", + "zh:4324c3df26709c7e669b751259cc5e62c4694ab44370dfcdfe197dcd9261c365", + "zh:4e3e83649240cea7105cd2802d0ae64b143fb543c2f559173feae5a108bc4287", + "zh:74ebf6be1277e9bd357b011026b80fc5ec1c26b70ec7ddd5fcae5e977f9a66ef", + "zh:82cfd3c92035f834a05f4b91d813a059a29ff4157792e36a0b3a224cba8737ae", + "zh:93f05c8ae3555c885c84b82781b2e90774671c321138b7f3c38ecd498009e1d8", + "zh:9b445a9a1544b4b38db10fadbd9ffd5efdded0def54feb9ca593e1bec6fbec5f", + "zh:b21ccd2c1bc691cf2f9876482b6e226d8a37a48de951b168a10f96ba929ebefd", + "zh:b7b7e458eb3c22669e1d36e9ef1886272c10f310501001abce8ae76383014fa5", + "zh:bd3c0cf7caab0a989227934bc60a8ac27131efcf84dd77cb6e32e68374170aee", + "zh:f4b9ccbb28eadf3825f6d7d38a3519379de222f136235a2f21a96c0221d65fb8", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + "zh:f8ef0b4a970ff5edeadfdeed77f9d0682befdca5df4e9b6d9dcfdf9903305b26", + ] +} + +provider "registry.terraform.io/hashicorp/github" { + version = "6.2.2" + hashes = [ + "h1:zi0URfg9FBXCPk918XU6RqV5k6kGVknQQA7p670zmik=", + "zh:43d7e5f1e11d67e38ca717016d209d6d9a6fa03321b489f91984351bfb143b69", + "zh:46e788395034b410bf59dfa43eb748a3d81ecfd23fc442349990fd7d92bd856a", + "zh:5234b7d5c5817ff7ebec29756050708372a071a701e2c8236e714a0bd29ef160", + "zh:74c485a241cc8e8cb99f988d38116fb14e51de896761fc9ca35a34ca5c999a7e", + "zh:7606789521c50937913ea13f851150828b5f9b8804ba80c5b2538c0b019339d8", + "zh:760fb0e74590459689c7159456b6e76f165634f7d0f89f5572d56b57d387f645", + "zh:7979d9085d809bb7d0db2c67e6c3443d1c18d12e51b72220dcb4cc5e883cd64a", + "zh:8bed25d8199bf8b2e7ccf67edc1a4a2fc041bd490b2c11565c669b80be43896c", + "zh:9ff82a6279fb7ae0cd9e44f1e73b64dd2aeca43d4d3096f3f2866b1ebbcb9431", + "zh:a886055ecd63ccb9b880e3c3301c0eca9acb108580d12519617554ae2be9a393", + "zh:c1f20386704919c7964a95daffcb29f494efb061abc28469840df4532833cecf", + "zh:cb6e9c4e33d6a57770073867e174c09c0eed401ee70473a688d20cb1cf0394f7", + "zh:f89ca130cc90b87dc25d036fe8f8cadb6fb53dc33368a032c5cee6275f3bcddc", + "zh:f94a2d1174091f04ed361192cdda9503baa3d161849d4f218c55a96bfb1ea33d", + "zh:fbd1fee2c9df3aa19cf8851ce134dea6e45ea01cb85695c1726670c285797e25", + ] +} diff --git a/components/locals.tf b/components/locals.tf new file mode 100644 index 000000000..6c7421ca4 --- /dev/null +++ b/components/locals.tf @@ -0,0 +1,43 @@ +locals { + # Read the repositories list from the JSON file + repositories_list = jsondecode(file("${path.module}/../test-repos.json")) + + # Filter out excluded repositories + included_repositories = [ + for repo in local.repositories_list : repo + if !contains(var.excluded_repositories, repo) + ] + + # Create a combination of repositories and branches + repo_branch_combinations = flatten([ + for repo in local.included_repositories : [ + for branch in var.branches : { + repo = repo + branch = branch + } + ] + ]) +} + +locals { + env_display_names = { + sbox = "Sandbox" + prod = "Production" + nonprod = "Non-Production" + test = "Test" + staging = "staging" + } + common_tags = { + "managedBy" = "DevOps" + "solutionOwner" = "RDO" + "activityName" = "Storage Account" + "dataClassification" = "Internal" + "automation" = "" + "costCentre" = "" + } + enforced_tags = module.tags.common_tags +} + + + + diff --git a/components/main.tf b/components/main.tf new file mode 100644 index 000000000..7dfee8246 --- /dev/null +++ b/components/main.tf @@ -0,0 +1,69 @@ +module "tags" { + source = "git::https://github.com/hmcts/terraform-module-common-tags.git?ref=master" + environment = var.env + product = var.product + builtFrom = var.builtFrom +} + +resource "azurerm_resource_group" "rg" { + name = var.resource_group_name + location = var.location + tags = module.tags.common_tags +} + +resource "azurerm_storage_account" "sa" { + name = var.storage_account_name + resource_group_name = azurerm_resource_group.rg.name + location = azurerm_resource_group.rg.location + account_tier = "Standard" + account_replication_type = "LRS" + tags = module.tags.common_tags +} + +resource "azurerm_storage_container" "tfstate" { + name = "tfstate" + storage_account_name = azurerm_storage_account.sa.name + container_access_type = "private" +} + +# Check if repositories exist +data "github_repository" "existing_repos" { + for_each = { for repo in local.included_repositories : repo => repo } + name = each.value +} + +# Check if branches exist +data "github_branch" "existing_branches" { + for_each = { for combo in local.repo_branch_combinations : "${combo.repo}:${combo.branch}" => combo if contains(keys(data.github_repository.existing_repos), combo.repo) } + repository = each.value.repo + branch = each.value.branch +} + +# Apply branch protection rules only if the branch exists +resource "github_branch_protection_v3" "branch_protection" { + for_each = { + for combo in local.repo_branch_combinations : "${combo.repo}:${combo.branch}" => combo + if try(data.github_branch.existing_branches["${combo.repo}:${combo.branch}"].branch, null) != null + } + + repository = each.value.repo + branch = each.value.branch + enforce_admins = false # Excludes organisation admins + + required_status_checks { + strict = true + contexts = ["ci/test", "ci/lint"] + } + + required_pull_request_reviews { + dismiss_stale_reviews = true + require_code_owner_reviews = false + required_approving_review_count = 1 # Ensure at least 1 reviewer + } + + restrictions { + users = [] + teams = [] + apps = [] + } +} \ No newline at end of file diff --git a/components/outputs.tf b/components/outputs.tf new file mode 100644 index 000000000..d3d652a2c --- /dev/null +++ b/components/outputs.tf @@ -0,0 +1,8 @@ +output "common_tags" { + value = { + Environment = var.env + Product = var.product + BuiltFrom = var.builtFrom + } +} + diff --git a/components/provider.tf b/components/provider.tf new file mode 100644 index 000000000..1594f51ae --- /dev/null +++ b/components/provider.tf @@ -0,0 +1,26 @@ +provider "azurerm" { + features {} +} + +provider "github" { + owner = "hmcts" + token = var.github_token +} + +terraform { + required_version = ">= 1.5.7" + + backend "azurerm" { + resource_group_name = "rule-set-rg" + storage_account_name = "rulesetsa" + container_name = "tfstate" + key = "terraform.tfstate" + } + + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = "3.109.0" + } + } +} diff --git a/components/variables.tf b/components/variables.tf new file mode 100644 index 000000000..89b2e6d53 --- /dev/null +++ b/components/variables.tf @@ -0,0 +1,64 @@ +variable "github_token" { + description = "GitHub token to use for authentication." + type = string + sensitive = true +} + +variable "branches" { + description = "List of branches to apply protection rules" + type = list(string) + default = [ + "master", + "main" + ] +} + +variable "excluded_repositories" { + description = "List of repositories to exclude from branch protection rules" + type = list(string) + default = [ + "rule-set-test-repo8" + ] +} + +variable "override_action" { + description = "The action to override" + type = string + default = "plan" +} + +variable "location" { + description = "The location for the resources" + type = string + default = "UK South" +} + +variable "resource_group_name" { + description = "The name of the resource group" + type = string + default = "rule-set-rg" +} + +variable "storage_account_name" { + description = "The name of the storage account" + type = string + default = "rulesetsa" +} + +variable "env" { + description = "The environment for the deployment (e.g., dev, staging, prod)" + type = string + default = "dev" +} + +variable "product" { + description = "The product name or identifier" + type = string + default = "sds-platform" +} + +variable "builtFrom" { + description = "Information about the build source or version" + type = string + default = "https://github.com/hmcts/github-repository-rules" +} \ No newline at end of file diff --git a/test-repos.json b/test-repos.json new file mode 100644 index 000000000..caad0b3cc --- /dev/null +++ b/test-repos.json @@ -0,0 +1,14 @@ +[ + "rule-set-test-repo", + "rule-set-test-repo1", + "rule-set-test-repo2", + "rule-set-test-repo3", + "rule-set-test-repo4", + "rule-set-test-repo5", + "rule-set-test-repo6", + "rule-set-test-repo7", + "rule-set-test-repo8", + "rule-set-test-repo9" + ] + + \ No newline at end of file