diff --git a/.github/workflows/data-share-account.yml b/.github/workflows/data-share-account.yml new file mode 100644 index 00000000..c98a6870 --- /dev/null +++ b/.github/workflows/data-share-account.yml @@ -0,0 +1,91 @@ +name: Module:data-share-account +on: + workflow_dispatch: + pull_request: + branches: + - main + paths: + - '.github/workflows/data-share-account.yml' + - 'terraform/data-share/data-share-account/**' +# - '.github/actions/**' + +env: + terraform_workingdir: "terraform/data-share/data-share-account" + GH_TOKEN: ${{ secrets.GH_TOKEN }} + ARM_CLIENT_ID: ${{ secrets.ARM_CLIENT_ID }} + ARM_CLIENT_SECRET: ${{ secrets.ARM_CLIENT_SECRET }} + ARM_SUBSCRIPTION_ID: ${{ secrets.ARM_SUBSCRIPTION_ID }} + ARM_TENANT_ID: ${{ secrets.ARM_TENANT_ID }} + +jobs: + terraform-lint: + name: Run Terraform lint + runs-on: ubuntu-latest + defaults: + run: + working-directory: "${{ env.terraform_workingdir }}" + + steps: + - uses: actions/checkout@v3 + - uses: hashicorp/setup-terraform@v2 + + - name: Terraform fmt + id: fmt + run: terraform fmt -check + continue-on-error: false + + terraform-sec: + name: Run Terraform tfsec + needs: + - terraform-lint + runs-on: ubuntu-latest + + steps: + - name: Check out code + uses: actions/checkout@main + + - name: Run tfsec with reviewdog output on the PR + uses: ./.github/actions/run-terraform-sec + + terratest: + name: Run Terratest + needs: + - terraform-sec + runs-on: ubuntu-latest + + defaults: + run: + working-directory: "${{ env.terraform_workingdir }}/test" + + steps: + - name: Check out code + uses: actions/checkout@v3 + + + - name: Set up Go + uses: actions/setup-go@v2 + with: + go-version: 1.18.2 + + - name: Setup Dependencies + run: go mod init test && go mod tidy + env: + GOPATH: "/home/runner/work/azure-labs-modules/azure-labs-modules/${{ env.terraform_workingdir }}" + + - name: Unit-test + run: go test -v -timeout 45m + env: + GOPATH: "/home/runner/work/azure-labs-modules/azure-labs-modules/${{ env.terraform_workingdir }}" + + terraform-docs: + name: Run Terraform Docs + needs: + - terratest + runs-on: ubuntu-latest + + steps: + - name: Check out code + uses: actions/checkout@v3 + + - name: Render terraform docs and push changes back to PR + uses: ./.github/actions/run-terraform-docs \ No newline at end of file diff --git a/.github/workflows/data-share.yml b/.github/workflows/data-share.yml new file mode 100644 index 00000000..e448b2ff --- /dev/null +++ b/.github/workflows/data-share.yml @@ -0,0 +1,91 @@ +name: Module:data-share +on: + workflow_dispatch: + pull_request: + branches: + - main + paths: + - '.github/workflows/data-share.yml' + - 'terraform/data-share/data-share/**' +# - '.github/actions/**' + +env: + terraform_workingdir: "terraform/data-share/data-share" + GH_TOKEN: ${{ secrets.GH_TOKEN }} + ARM_CLIENT_ID: ${{ secrets.ARM_CLIENT_ID }} + ARM_CLIENT_SECRET: ${{ secrets.ARM_CLIENT_SECRET }} + ARM_SUBSCRIPTION_ID: ${{ secrets.ARM_SUBSCRIPTION_ID }} + ARM_TENANT_ID: ${{ secrets.ARM_TENANT_ID }} + +jobs: + terraform-lint: + name: Run Terraform lint + runs-on: ubuntu-latest + defaults: + run: + working-directory: "${{ env.terraform_workingdir }}" + + steps: + - uses: actions/checkout@v3 + - uses: hashicorp/setup-terraform@v2 + + - name: Terraform fmt + id: fmt + run: terraform fmt -check + continue-on-error: false + + terraform-sec: + name: Run Terraform tfsec + needs: + - terraform-lint + runs-on: ubuntu-latest + + steps: + - name: Check out code + uses: actions/checkout@main + + - name: Run tfsec with reviewdog output on the PR + uses: ./.github/actions/run-terraform-sec + + terratest: + name: Run Terratest + needs: + - terraform-sec + runs-on: ubuntu-latest + + defaults: + run: + working-directory: "${{ env.terraform_workingdir }}/test" + + steps: + - name: Check out code + uses: actions/checkout@v3 + + + - name: Set up Go + uses: actions/setup-go@v2 + with: + go-version: 1.18.2 + + - name: Setup Dependencies + run: go mod init test && go mod tidy + env: + GOPATH: "/home/runner/work/azure-labs-modules/azure-labs-modules/${{ env.terraform_workingdir }}" + + - name: Unit-test + run: go test -v -timeout 45m + env: + GOPATH: "/home/runner/work/azure-labs-modules/azure-labs-modules/${{ env.terraform_workingdir }}" + + terraform-docs: + name: Run Terraform Docs + needs: + - terratest + runs-on: ubuntu-latest + + steps: + - name: Check out code + uses: actions/checkout@v3 + + - name: Render terraform docs and push changes back to PR + uses: ./.github/actions/run-terraform-docs \ No newline at end of file diff --git a/.github/workflows/log-analytics-cluster.yml b/.github/workflows/log-analytics-cluster.yml index 686292ff..dce9f26e 100644 --- a/.github/workflows/log-analytics-cluster.yml +++ b/.github/workflows/log-analytics-cluster.yml @@ -1,12 +1,12 @@ name: Module:log-analytics-cluster on: workflow_dispatch: - pull_request: - branches: - - main - paths: - - '.github/workflows/log-analytics-cluster.yml' - - 'terraform/log-analytics/log-analytics-cluster/**' +# pull_request: +# branches: +# - main +# paths: +# - '.github/workflows/log-analytics-cluster.yml' +# - 'terraform/log-analytics/log-analytics-cluster/**' # - '.github/actions/**' env: diff --git a/terraform/data-share/data-share-account/README.md b/terraform/data-share/data-share-account/README.md new file mode 100644 index 00000000..6b378ac2 --- /dev/null +++ b/terraform/data-share/data-share-account/README.md @@ -0,0 +1,26 @@ + +## Resources + +| Name | Type | +|------|------| +| [azurerm_data_share_account.adl_dsa](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/data_share_account) | resource | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [basename](#input\_basename) | Basename of the module. | `string` | n/a | yes | +| [location](#input\_location) | Location of the resource group. | `string` | n/a | yes | +| [module\_enabled](#input\_module\_enabled) | Variable to enable or disable the module. | `bool` | `true` | no | +| [rg\_name](#input\_rg\_name) | Resource group name. | `string` | n/a | yes | +| [tags](#input\_tags) | A mapping of tags which should be assigned to the deployed resource. | `map(string)` | `{}` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [id](#output\_id) | Resource identifier of the instance of Data Share account. | +| [identity](#output\_identity) | Principal ID and Tenant ID for the Service Principal associated with the identity of the Data Share account. | +| [name](#output\_name) | The name of the Data Share account. | +| [resource\_group\_name](#output\_resource\_group\_name) | Resource Group where the Data Share account exists. | + \ No newline at end of file diff --git a/terraform/data-share/data-share-account/main.tf b/terraform/data-share/data-share-account/main.tf new file mode 100644 index 00000000..82e5698e --- /dev/null +++ b/terraform/data-share/data-share-account/main.tf @@ -0,0 +1,13 @@ +# https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/data_share_account + +resource "azurerm_data_share_account" "adl_dsa" { + name = "dsa-${var.basename}" + location = var.location + resource_group_name = var.rg_name + identity { + type = "SystemAssigned" + } + tags = var.tags + + count = var.module_enabled ? 1 : 0 +} diff --git a/terraform/data-share/data-share-account/outputs.tf b/terraform/data-share/data-share-account/outputs.tf new file mode 100644 index 00000000..0838c980 --- /dev/null +++ b/terraform/data-share/data-share-account/outputs.tf @@ -0,0 +1,31 @@ +output "id" { + value = ( + length(azurerm_data_share_account.adl_dsa) > 0 ? + azurerm_data_share_account.adl_dsa[0].id : "" + ) + description = "Resource identifier of the instance of Data Share account." +} + +output "name" { + value = ( + length(azurerm_data_share_account.adl_dsa) > 0 ? + azurerm_data_share_account.adl_dsa[0].name : "" + ) + description = "The name of the Data Share account." +} + +output "resource_group_name" { + value = ( + length(azurerm_data_share_account.adl_dsa) > 0 ? + azurerm_data_share_account.adl_dsa[0].resource_group_name : "" + ) + description = "Resource Group where the Data Share account exists." +} + +output "identity" { + value = ( + length(azurerm_data_share_account.adl_dsa) > 0 ? + azurerm_data_share_account.adl_dsa[0].identity : [{}] + ) + description = "Principal ID and Tenant ID for the Service Principal associated with the identity of the Data Share account." +} \ No newline at end of file diff --git a/terraform/data-share/data-share-account/test/data_share_account.tf b/terraform/data-share/data-share-account/test/data_share_account.tf new file mode 100644 index 00000000..f86ea804 --- /dev/null +++ b/terraform/data-share/data-share-account/test/data_share_account.tf @@ -0,0 +1,16 @@ +module "data_share_account" { + source = "../" + basename = random_string.postfix.result + rg_name = module.local_rg.name + location = var.location + tags = {} +} + +# Modules dependencies + +module "local_rg" { + source = "../../../resource-group" + basename = random_string.postfix.result + location = var.location + tags = local.tags +} \ No newline at end of file diff --git a/terraform/data-share/data-share-account/test/locals.tf b/terraform/data-share/data-share-account/test/locals.tf new file mode 100644 index 00000000..5a1867c8 --- /dev/null +++ b/terraform/data-share/data-share-account/test/locals.tf @@ -0,0 +1,7 @@ +locals { + tags = { + Project = "Azure/azure-data-labs-modules" + Module = "data-share-account" + Toolkit = "Terraform" + } +} \ No newline at end of file diff --git a/terraform/data-share/data-share-account/test/outputs.tf b/terraform/data-share/data-share-account/test/outputs.tf new file mode 100644 index 00000000..35b9c1eb --- /dev/null +++ b/terraform/data-share/data-share-account/test/outputs.tf @@ -0,0 +1,15 @@ +output "id" { + value = module.data_share_account.id +} + +output "name" { + value = module.data_share_account.name +} + +output "resource_group_name" { + value = module.data_share_account.resource_group_name +} + +output "identity" { + value = module.data_share_account.identity +} \ No newline at end of file diff --git a/terraform/data-share/data-share-account/test/providers.tf b/terraform/data-share/data-share-account/test/providers.tf new file mode 100644 index 00000000..1843ed73 --- /dev/null +++ b/terraform/data-share/data-share-account/test/providers.tf @@ -0,0 +1,19 @@ +terraform { + backend "azurerm" { + resource_group_name = "rg-adl-terraform-state" + storage_account_name = "stadlterraformstate" + container_name = "default" + key = "datashareaccount.terraform.tfstate" + } + + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = "= 3.43.0" + } + } +} + +provider "azurerm" { + features {} +} diff --git a/terraform/data-share/data-share-account/test/unit_test.go b/terraform/data-share/data-share-account/test/unit_test.go new file mode 100644 index 00000000..f28655a8 --- /dev/null +++ b/terraform/data-share/data-share-account/test/unit_test.go @@ -0,0 +1,38 @@ +package test + +import ( + "testing" + "github.com/gruntwork-io/terratest/modules/terraform" + "github.com/stretchr/testify/assert" +) + +func TestModule(t *testing.T) { + t.Parallel() + + terraformOptions := &terraform.Options{ + TerraformDir: "./", + Lock: true, + LockTimeout: "1800s", + // VarFiles: []string{"terraform_unitest.tfvars"}, + } + + // At the end of the test, run `terraform destroy` to clean up any resources that were created + defer terraform.Destroy(t, terraformOptions) + + // Is used mainly for debugging, fail early if plan is not possible + terraform.InitAndPlan(t, terraformOptions) + + // This will run `terraform init` and `terraform apply` and fail the test if there are any errors + terraform.InitAndApply(t, terraformOptions) + + // Check if the outputs exist + assert := assert.New(t) + id := terraform.Output(t, terraformOptions, "id") + assert.NotNil(id) + name := terraform.Output(t, terraformOptions, "name") + assert.NotNil(name) + resource_group_name := terraform.Output(t, terraformOptions, "resource_group_name") + assert.NotNil(resource_group_name) + identity := terraform.Output(t, terraformOptions, "identity") + assert.NotNil(identity) +} \ No newline at end of file diff --git a/terraform/data-share/data-share-account/test/variables.tf b/terraform/data-share/data-share-account/test/variables.tf new file mode 100644 index 00000000..b025435b --- /dev/null +++ b/terraform/data-share/data-share-account/test/variables.tf @@ -0,0 +1,10 @@ +resource "random_string" "postfix" { + length = 8 + special = false + upper = false +} + +variable "location" { + type = string + default = "North Europe" +} \ No newline at end of file diff --git a/terraform/data-share/data-share-account/variables.tf b/terraform/data-share/data-share-account/variables.tf new file mode 100644 index 00000000..8544c465 --- /dev/null +++ b/terraform/data-share/data-share-account/variables.tf @@ -0,0 +1,34 @@ +variable "basename" { + type = string + description = "Basename of the module." + validation { + condition = can(regex("^[-\\w]{0,86}$", var.basename)) + error_message = "The name must be between 0 and 86 characters, can contain only letters, numbers, underscores, and hyphens." + } +} + +variable "rg_name" { + type = string + description = "Resource group name." + validation { + condition = can(regex("^[-\\w\\.\\(\\)]{1,90}$", var.rg_name)) && can(regex("[-\\w\\(\\)]+$", var.rg_name)) + error_message = "Resource group names must be between 1 and 90 characters and can only include alphanumeric, underscore, parentheses, hyphen, period (except at end)." + } +} + +variable "location" { + type = string + description = "Location of the resource group." +} + +variable "tags" { + type = map(string) + default = {} + description = "A mapping of tags which should be assigned to the deployed resource." +} + +variable "module_enabled" { + type = bool + description = "Variable to enable or disable the module." + default = true +} \ No newline at end of file diff --git a/terraform/data-share/data-share/README.md b/terraform/data-share/data-share/README.md new file mode 100644 index 00000000..e7711098 --- /dev/null +++ b/terraform/data-share/data-share/README.md @@ -0,0 +1,26 @@ + +## Resources + +| Name | Type | +|------|------| +| [azurerm_data_share.adl_ds](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/data_share) | resource | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [account\_id](#input\_account\_id) | The ID of the Data Share account in which the Data Share is created. | `string` | n/a | yes | +| [basename](#input\_basename) | Basename of the module. | `string` | n/a | yes | +| [description](#input\_description) | The Data Share's description. | `string` | `""` | no | +| [kind](#input\_kind) | The kind of the Data Share. | `string` | `"CopyBased"` | no | +| [module\_enabled](#input\_module\_enabled) | Variable to enable or disable the module. | `bool` | `true` | no | +| [snapshot\_schedule](#input\_snapshot\_schedule) | "
name - The name of the snapshot schedule.
recurrence - The interval of the synchronization with the source data. Possible values are Hour and Day.
start\_time - The synchronization with the source data's start time.
" |
map(
object(
{
name = optional(string)
recurrence = optional(string)
start_time = optional(string)
}
)
)
| `{}` | no | +| [terms](#input\_terms) | The terms of the Data Share. | `string` | `""` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [id](#output\_id) | Resource identifier of the instance of Data Share. | +| [name](#output\_name) | The name of the Data Share. | + \ No newline at end of file diff --git a/terraform/data-share/data-share/main.tf b/terraform/data-share/data-share/main.tf new file mode 100644 index 00000000..a522375a --- /dev/null +++ b/terraform/data-share/data-share/main.tf @@ -0,0 +1,21 @@ +# https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/data_share +# As of today, there is no support for Data Share invitation, subscription and dataset mapping: https://github.com/hashicorp/terraform-provider-azurerm/issues/14010 + +resource "azurerm_data_share" "adl_ds" { + name = "ds_${var.basename}" + account_id = var.account_id + kind = var.kind + description = var.description + terms = var.terms + + dynamic "snapshot_schedule" { + for_each = var.snapshot_schedule + content { + name = snapshot_schedule.value["name"] + recurrence = snapshot_schedule.value["recurrence"] + start_time = snapshot_schedule.value["start_time"] + } + } + + count = var.module_enabled ? 1 : 0 +} diff --git a/terraform/data-share/data-share/outputs.tf b/terraform/data-share/data-share/outputs.tf new file mode 100644 index 00000000..240b1c6c --- /dev/null +++ b/terraform/data-share/data-share/outputs.tf @@ -0,0 +1,15 @@ +output "id" { + value = ( + length(azurerm_data_share.adl_ds) > 0 ? + azurerm_data_share.adl_ds[0].id : "" + ) + description = "Resource identifier of the instance of Data Share." +} + +output "name" { + value = ( + length(azurerm_data_share.adl_ds) > 0 ? + azurerm_data_share.adl_ds[0].name : "" + ) + description = "The name of the Data Share." +} \ No newline at end of file diff --git a/terraform/data-share/data-share/test/data_share.tf b/terraform/data-share/data-share/test/data_share.tf new file mode 100644 index 00000000..6a084802 --- /dev/null +++ b/terraform/data-share/data-share/test/data_share.tf @@ -0,0 +1,23 @@ +module "data_share" { + source = "../" + basename = random_string.postfix.result + account_id = module.local_data_share_account.id + snapshot_schedule = var.snapshot_schedule +} + +# Module dependencies + +module "local_data_share_account" { + source = "../../data-share-account" + basename = random_string.postfix.result + rg_name = module.local_rg.name + location = var.location + tags = {} +} + +module "local_rg" { + source = "../../../resource-group" + basename = random_string.postfix.result + location = var.location + tags = local.tags +} diff --git a/terraform/data-share/data-share/test/locals.tf b/terraform/data-share/data-share/test/locals.tf new file mode 100644 index 00000000..7c8042ff --- /dev/null +++ b/terraform/data-share/data-share/test/locals.tf @@ -0,0 +1,7 @@ +locals { + tags = { + Project = "Azure/azure-data-labs-modules" + Module = "data-share" + Toolkit = "Terraform" + } +} \ No newline at end of file diff --git a/terraform/data-share/data-share/test/outputs.tf b/terraform/data-share/data-share/test/outputs.tf new file mode 100644 index 00000000..72418f57 --- /dev/null +++ b/terraform/data-share/data-share/test/outputs.tf @@ -0,0 +1,7 @@ +output "id" { + value = module.data_share.id +} + +output "name" { + value = module.data_share.name +} \ No newline at end of file diff --git a/terraform/data-share/data-share/test/providers.tf b/terraform/data-share/data-share/test/providers.tf new file mode 100644 index 00000000..025fbc90 --- /dev/null +++ b/terraform/data-share/data-share/test/providers.tf @@ -0,0 +1,19 @@ +terraform { + backend "azurerm" { + resource_group_name = "rg-adl-terraform-state" + storage_account_name = "stadlterraformstate" + container_name = "default" + key = "datashare.terraform.tfstate" + } + + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = "= 3.43.0" + } + } +} + +provider "azurerm" { + features {} +} diff --git a/terraform/data-share/data-share/test/unit_test.go b/terraform/data-share/data-share/test/unit_test.go new file mode 100644 index 00000000..7ed1307a --- /dev/null +++ b/terraform/data-share/data-share/test/unit_test.go @@ -0,0 +1,34 @@ +package test + +import ( + "testing" + "github.com/gruntwork-io/terratest/modules/terraform" + "github.com/stretchr/testify/assert" +) + +func TestModule(t *testing.T) { + t.Parallel() + + terraformOptions := &terraform.Options{ + TerraformDir: "./", + Lock: true, + LockTimeout: "1800s", + // VarFiles: []string{"terraform_unitest.tfvars"}, + } + + // At the end of the test, run `terraform destroy` to clean up any resources that were created + defer terraform.Destroy(t, terraformOptions) + + // Is used mainly for debugging, fail early if plan is not possible + terraform.InitAndPlan(t, terraformOptions) + + // This will run `terraform init` and `terraform apply` and fail the test if there are any errors + terraform.InitAndApply(t, terraformOptions) + + // Check if the outputs exist + assert := assert.New(t) + id := terraform.Output(t, terraformOptions, "id") + assert.NotNil(id) + name := terraform.Output(t, terraformOptions, "name") + assert.NotNil(name) +} diff --git a/terraform/data-share/data-share/test/variables.tf b/terraform/data-share/data-share/test/variables.tf new file mode 100644 index 00000000..42cd657f --- /dev/null +++ b/terraform/data-share/data-share/test/variables.tf @@ -0,0 +1,29 @@ +resource "random_string" "postfix" { + length = 8 + special = false + upper = false +} + +variable "location" { + type = string + default = "North Europe" +} + +variable "snapshot_schedule" { + type = map( + object( + { + name = optional(string) + recurrence = optional(string) + start_time = optional(string) + } + ) + ) + default = { + snap_sched = { + name = "example-ss" + recurrence = "Day" + start_time = "2020-04-17T04:47:52.9614956Z" + } + } +} diff --git a/terraform/data-share/data-share/variables.tf b/terraform/data-share/data-share/variables.tf new file mode 100644 index 00000000..b70d95ab --- /dev/null +++ b/terraform/data-share/data-share/variables.tf @@ -0,0 +1,73 @@ +variable "basename" { + type = string + description = "Basename of the module." + validation { + condition = can(regex("^[\\w]{0,87}$", var.basename)) + error_message = "The name must be between 0 and 87 characters, can contain only letters, numbers, and underscores." + } +} + +variable "account_id" { + type = string + description = "The ID of the Data Share account in which the Data Share is created." +} + +variable "module_enabled" { + type = bool + description = "Variable to enable or disable the module." + default = true +} + +variable "kind" { + type = string + description = "The kind of the Data Share." + validation { + condition = contains(["copybased", "inplace"], lower(var.kind)) + error_message = "Valid values for kind are \"CopyBased\" or \"InPlace\"." + } + default = "CopyBased" +} + +variable "description" { + type = string + description = "The Data Share's description." + validation { + condition = length(var.description) <= 100000 + error_message = "Length of description must not exceed 100,000 characters." + } + default = "" +} + +variable "terms" { + type = string + description = "The terms of the Data Share." + validation { + condition = length(var.terms) <= 100000 + error_message = "Length of terms must not exceed 100,000 characters." + } + default = "" +} + +variable "snapshot_schedule" { + type = map( + object( + { + name = optional(string) + recurrence = optional(string) + start_time = optional(string) + } + ) + ) + description = < 0])) + error_message = "Valid values for recurrence are \"Hour\" or \"Day\". Valid values for start_time are dates or date times in ISO 8601 format." + } + default = {} +}