6 | ## Requirements
7 |
8 | | Name | Version |
9 | |------|---------|
10 | | [azuread](#requirement\_azuread) | ~> 3.0.2 |
11 | | [azurerm](#requirement\_azurerm) | ~> 4.10.0 |
12 | | [github](#requirement\_github) | ~> 5.0 |
13 |
14 | ## Providers
15 |
16 | | Name | Version |
17 | |------|---------|
18 | | [azuread](#provider\_azuread) | ~> 3.0.2 |
19 | | [azurerm](#provider\_azurerm) | ~> 4.10.0 |
20 | | [github](#provider\_github) | ~> 5.0 |
21 | | [random](#provider\_random) | n/a |
22 |
23 | ## Modules
24 |
25 | | Name | Source | Version |
26 | |------|--------|---------|
27 | | [action\_azure\_login](#module\_action\_azure\_login) | Azure-Terraformer/action-azure-login-test/github | 1.0.1 |
28 | | [codebase](#module\_codebase) | Azure-Terraformer/codebase-terraform-azure-vm-app/github | 1.0.16 |
29 | | [gitflow](#module\_gitflow) | Azure-Terraformer/action-azure-application/github | 1.0.32 |
30 | | [github\_environments](#module\_github\_environments) | Azure-Terraformer/environment-terraform-azure/github | 1.0.3 |
31 | | [github\_identity](#module\_github\_identity) | Azure-Terraformer/github-credential/azuread | 1.0.10 |
32 |
33 | ## Resources
34 |
35 | | Name | Type |
36 | |------|------|
37 | | [azurerm_role_assignment.main](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/role_assignment) | resource |
38 | | [github_actions_environment_variable.gallery_name](https://registry.terraform.io/providers/integrations/github/latest/docs/resources/actions_environment_variable) | resource |
39 | | [github_actions_environment_variable.gallery_resource_group](https://registry.terraform.io/providers/integrations/github/latest/docs/resources/actions_environment_variable) | resource |
40 | | [github_actions_environment_variable.managed_image_destination](https://registry.terraform.io/providers/integrations/github/latest/docs/resources/actions_environment_variable) | resource |
41 | | [github_actions_variable.application_name](https://registry.terraform.io/providers/integrations/github/latest/docs/resources/actions_variable) | resource |
42 | | [github_branch.main](https://registry.terraform.io/providers/integrations/github/latest/docs/resources/branch) | resource |
43 | | [github_branch_default.default](https://registry.terraform.io/providers/integrations/github/latest/docs/resources/branch_default) | resource |
44 | | [github_repository.main](https://registry.terraform.io/providers/integrations/github/latest/docs/resources/repository) | resource |
45 | | [random_string.main](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/string) | resource |
46 | | [azuread_client_config.current](https://registry.terraform.io/providers/hashicorp/azuread/latest/docs/data-sources/client_config) | data source |
47 | | [azurerm_subscription.main](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/data-sources/subscription) | data source |
48 | | [github_user.current](https://registry.terraform.io/providers/integrations/github/latest/docs/data-sources/user) | data source |
49 |
50 | ## Inputs
51 |
52 | | Name | Description | Type | Default | Required |
53 | |------|-------------|------|---------|:--------:|
54 | | [application\_name](#input\_application\_name) | The name of the application to be deployed. This name is used for resource identification and naming conventions within the infrastructure.
It's best to keep this short and simple while also ensuring it is easily identifiable and relatively unique within your organization (or at least within the Subscriptions you are targetting). | `string` | n/a | yes |
55 | | [base\_address\_space](#input\_base\_address\_space) | n/a | `string` | n/a | yes |
56 | | [commit\_user](#input\_commit\_user) | The user information for committing changes to the repository, including name and email. | object({
name = string
email = string
})
| n/a | yes |
57 | | [delay\_after\_environment\_creation](#input\_delay\_after\_environment\_creation) | GitHub Environments have a glitch that causes them to not be available immediately after creation is reported. Environment variables
will fail unless there is some time granted to allow for the Environment creation to complete. This setting adds a fixed amount of time
after the creation of the environment before provisioning additional resources. | `number` | `10` | no |
58 | | [delete\_branch\_on\_merge](#input\_delete\_branch\_on\_merge) | The setting that controls whether the GitHub repository deletes the feature branch automatically after the merge. | `bool` | `true` | no |
59 | | [environments](#input\_environments) | Configuration for each of the environments for this application.
Each Environment has a name which is supplied by the key of the map.
Each Environment has a long-lived environment on the specified `branch_name`.
Each Environment can be provisioned within the context of an Azure Subscription specified by `subscription_id`.
Each Environment can have its own Terraform State Backend configuration which consists of an Azure Storage Account and containers for both state files and plan files. | map(object({
subscription_id = string
branch_name = string
backend = object({
resource_group_name = string
storage_account_name = string
state_container_name = string
plan_container_name = string
})
gallery = object({
name = string
resource_group = string
})
managed_image_destination = string
}))
| n/a | yes |
60 | | [github\_organization](#input\_github\_organization) | The GitHub organization under which the repository will be created. This should be the exact name of the GitHub organization. | `string` | n/a | yes |
61 | | [image\_names](#input\_image\_names) | n/a | `list(string)` | n/a | yes |
62 | | [location](#input\_location) | The primary Azure region where the Azure Functions Core Environment will be provisioned." | `string` | n/a | yes |
63 | | [packer\_version](#input\_packer\_version) | n/a | `string` | n/a | yes |
64 | | [repository\_description](#input\_repository\_description) | A brief description of the GitHub repository. This helps in understanding the purpose and scope of the repository.
This should describe the workload represented by the `application_name`. | `string` | n/a | yes |
65 | | [repository\_name](#input\_repository\_name) | The name of the GitHub repository to be created. This name should be unique within the specified GitHub organization.
The GitHub repository name should correlate to the `application_name` as it will contain the source code for the infrastructure
that is provisioned to Azure. | `string` | n/a | yes |
66 | | [repository\_visibility](#input\_repository\_visibility) | The visibility level of the GitHub repository. Accepted values are 'public', 'private', or 'internal'. Determines who can view and access the repository. | `string` | n/a | yes |
67 | | [terraform\_version](#input\_terraform\_version) | Specifies the version of Terraform to use. Defaults to '1.9.8'. | `string` | `"1.9.8"` | no |
68 | | [vm\_size](#input\_vm\_size) | n/a | `string` | n/a | yes |
69 |
70 | ## Outputs
71 |
72 | No outputs.
73 |
--------------------------------------------------------------------------------
/modules/azure-vm-app/codebase.tf:
--------------------------------------------------------------------------------
1 | module "codebase" {
2 |
3 | source = "Azure-Terraformer/codebase-terraform-azure-vm-app/github"
4 | version = "1.0.16"
5 |
6 | repository = var.repository_name
7 | branch = github_branch.main.branch
8 | commit_user = var.commit_user
9 | primary_location = var.location
10 | image_names = var.image_names
11 | vm_size = var.vm_size
12 | packer_version = var.packer_version
13 | base_address_space = var.base_address_space
14 |
15 | }
16 |
--------------------------------------------------------------------------------
/modules/azure-vm-app/environments.tf:
--------------------------------------------------------------------------------
1 |
2 | locals {
3 | extended_environments = {
4 | for key, value in var.environments : key => {
5 | subscription_id = value.subscription_id
6 | tenant_id = data.azuread_client_config.current.tenant_id
7 | client_id = module.github_identity[key].application.client_id
8 | branch_name = value.branch_name
9 | reviewers = []
10 | backend = value.backend
11 | }
12 | }
13 | }
14 |
15 | module "github_environments" {
16 | source = "Azure-Terraformer/environment-terraform-azure/github"
17 | version = "1.0.3"
18 |
19 | repository_name = var.repository_name
20 | terraform_version = var.terraform_version
21 | terraform_working_directory = module.codebase.terraform_path
22 | delay_after_environment_creation = 10
23 | environments = local.extended_environments
24 |
25 | depends_on = [github_branch_default.default]
26 |
27 | }
28 |
29 | resource "github_actions_environment_variable" "gallery_name" {
30 |
31 | for_each = local.extended_environments
32 |
33 | repository = var.repository_name
34 | environment = each.key
35 | variable_name = "PACKER_COMPUTE_GALLERY_NAME"
36 | value = var.environments[each.key].gallery.name
37 |
38 | depends_on = [module.github_environments]
39 |
40 | }
41 |
42 | resource "github_actions_environment_variable" "gallery_resource_group" {
43 |
44 | for_each = var.environments
45 |
46 | repository = var.repository_name
47 | environment = each.key
48 | variable_name = "PACKER_COMPUTE_GALLERY_RESOURCE_GROUP"
49 | value = var.environments[each.key].gallery.resource_group
50 |
51 | depends_on = [module.github_environments]
52 |
53 | }
54 |
55 |
56 | resource "github_actions_environment_variable" "managed_image_destination" {
57 |
58 | for_each = var.environments
59 |
60 | repository = var.repository_name
61 | environment = each.key
62 | variable_name = "PACKER_MANAGED_IMAGE_DESTINATION"
63 | value = var.environments[each.key].managed_image_destination
64 |
65 | depends_on = [module.github_environments]
66 |
67 | }
68 |
--------------------------------------------------------------------------------
/modules/azure-vm-app/identity.tf:
--------------------------------------------------------------------------------
1 | data "azuread_client_config" "current" {}
2 |
3 | resource "random_string" "main" {
4 | length = 8
5 | upper = false
6 | special = false
7 | }
8 |
9 | module "github_identity" {
10 |
11 | source = "Azure-Terraformer/github-credential/azuread"
12 | version = "1.0.10"
13 |
14 | for_each = var.environments
15 |
16 | name = "${var.github_organization}-${var.repository_name}-${each.key}"
17 | github_organization = var.github_organization
18 | repository_name = var.repository_name
19 | entity_type = "environment"
20 | environment_name = each.key
21 | owners = [data.azuread_client_config.current.object_id]
22 |
23 | }
24 |
25 | data "azurerm_subscription" "main" {
26 |
27 | for_each = var.environments
28 |
29 | subscription_id = each.value.subscription_id
30 |
31 | }
32 |
33 | resource "azurerm_role_assignment" "main" {
34 |
35 | for_each = var.environments
36 |
37 | scope = data.azurerm_subscription.main[each.key].id
38 | role_definition_name = "Owner"
39 | principal_id = module.github_identity[each.key].service_principal.object_id
40 |
41 | }
42 |
--------------------------------------------------------------------------------
/modules/azure-vm-app/main.tf:
--------------------------------------------------------------------------------
1 |
2 | data "github_user" "current" {
3 | username = ""
4 | }
5 |
6 | resource "github_repository" "main" {
7 |
8 | name = var.repository_name
9 | description = var.repository_description
10 |
11 | visibility = var.repository_visibility
12 | delete_branch_on_merge = var.delete_branch_on_merge
13 | auto_init = true
14 |
15 | }
16 |
17 | resource "github_branch" "main" {
18 | repository = var.repository_name
19 | branch = "main"
20 | }
21 |
22 | resource "github_branch_default" "default" {
23 | repository = var.repository_name
24 | branch = github_branch.main.branch
25 | }
26 |
27 | resource "github_actions_variable" "application_name" {
28 |
29 | repository = var.repository_name
30 | variable_name = "APPLICATION_NAME"
31 | value = var.application_name
32 |
33 | depends_on = [github_branch_default.default]
34 |
35 | }
36 |
--------------------------------------------------------------------------------
/modules/azure-vm-app/pipelines.tf:
--------------------------------------------------------------------------------
1 | # Local to convert to map(string)
2 | locals {
3 | branch_name_map = tomap({
4 | for key, value in var.environments : key => value.branch_name
5 | })
6 | }
7 |
8 | module "gitflow" {
9 |
10 | source = "Azure-Terraformer/action-azure-application/github"
11 | version = "1.0.32"
12 |
13 | repository = var.repository_name
14 | branch = github_branch.main.branch
15 | commit_user = var.commit_user
16 | environments = local.branch_name_map
17 |
18 | # we don't want the actions firing when we start pushing commits
19 | depends_on = [module.codebase]
20 |
21 | }
22 |
23 | module "action_azure_login" {
24 | source = "Azure-Terraformer/action-azure-login-test/github"
25 | version = "1.0.1"
26 |
27 | repository = var.repository_name
28 | branch = github_branch.main.branch
29 | commit_user = var.commit_user
30 |
31 | }
32 |
--------------------------------------------------------------------------------
/modules/azure-vm-app/variables.tf:
--------------------------------------------------------------------------------
1 | variable "application_name" {
2 | type = string
3 | description = <
6 | ## Requirements
7 |
8 | | Name | Version |
9 | |------|---------|
10 | | [azuread](#requirement\_azuread) | ~> 3.0.2 |
11 | | [azurerm](#requirement\_azurerm) | ~> 4.10.0 |
12 | | [github](#requirement\_github) | ~> 5.0 |
13 |
14 | ## Providers
15 |
16 | | Name | Version |
17 | |------|---------|
18 | | [azuread](#provider\_azuread) | ~> 3.0.2 |
19 | | [azurerm](#provider\_azurerm) | ~> 4.10.0 |
20 | | [github](#provider\_github) | ~> 5.0 |
21 | | [random](#provider\_random) | n/a |
22 |
23 | ## Modules
24 |
25 | | Name | Source | Version |
26 | |------|--------|---------|
27 | | [action\_azure\_login](#module\_action\_azure\_login) | Azure-Terraformer/action-azure-login-test/github | 1.0.1 |
28 | | [codebase](#module\_codebase) | Azure-Terraformer/codebase-terraform-azure-vm-core/github | 1.0.3 |
29 | | [gitflow](#module\_gitflow) | Azure-Terraformer/action-azure-application/github | 1.0.32 |
30 | | [github\_environments](#module\_github\_environments) | Azure-Terraformer/environment-terraform-azure/github | 1.0.3 |
31 | | [github\_identity](#module\_github\_identity) | Azure-Terraformer/github-credential/azuread | 1.0.10 |
32 |
33 | ## Resources
34 |
35 | | Name | Type |
36 | |------|------|
37 | | [azurerm_role_assignment.main](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/role_assignment) | resource |
38 | | [github_actions_variable.application_name](https://registry.terraform.io/providers/integrations/github/latest/docs/resources/actions_variable) | resource |
39 | | [github_branch.main](https://registry.terraform.io/providers/integrations/github/latest/docs/resources/branch) | resource |
40 | | [github_branch_default.default](https://registry.terraform.io/providers/integrations/github/latest/docs/resources/branch_default) | resource |
41 | | [github_repository.main](https://registry.terraform.io/providers/integrations/github/latest/docs/resources/repository) | resource |
42 | | [random_string.main](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/string) | resource |
43 | | [azuread_client_config.current](https://registry.terraform.io/providers/hashicorp/azuread/latest/docs/data-sources/client_config) | data source |
44 | | [azurerm_subscription.main](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/data-sources/subscription) | data source |
45 | | [github_user.current](https://registry.terraform.io/providers/integrations/github/latest/docs/data-sources/user) | data source |
46 |
47 | ## Inputs
48 |
49 | | Name | Description | Type | Default | Required |
50 | |------|-------------|------|---------|:--------:|
51 | | [application\_name](#input\_application\_name) | The name of the application to be deployed. This name is used for resource identification and naming conventions within the infrastructure.
It's best to keep this short and simple while also ensuring it is easily identifiable and relatively unique within your organization (or at least within the Subscriptions you are targetting). | `string` | n/a | yes |
52 | | [commit\_user](#input\_commit\_user) | The user information for committing changes to the repository, including name and email. | object({
name = string
email = string
})
| n/a | yes |
53 | | [delay\_after\_environment\_creation](#input\_delay\_after\_environment\_creation) | GitHub Environments have a glitch that causes them to not be available immediately after creation is reported. Environment variables
will fail unless there is some time granted to allow for the Environment creation to complete. This setting adds a fixed amount of time
after the creation of the environment before provisioning additional resources. | `number` | `10` | no |
54 | | [delete\_branch\_on\_merge](#input\_delete\_branch\_on\_merge) | The setting that controls whether the GitHub repository deletes the feature branch automatically after the merge. | `bool` | `true` | no |
55 | | [environments](#input\_environments) | Configuration for each of the environments for this application.
Each Environment has a name which is supplied by the key of the map.
Each Environment has a long-lived environment on the specified `branch_name`.
Each Environment can be provisioned within the context of an Azure Subscription specified by `subscription_id`.
Each Environment can have its own Terraform State Backend configuration which consists of an Azure Storage Account and containers for both state files and plan files. | map(object({
subscription_id = string
branch_name = string
backend = object({
resource_group_name = string
storage_account_name = string
state_container_name = string
plan_container_name = string
})
}))
| n/a | yes |
56 | | [github\_organization](#input\_github\_organization) | The GitHub organization under which the repository will be created. This should be the exact name of the GitHub organization. | `string` | n/a | yes |
57 | | [location](#input\_location) | The primary Azure region where the Azure Functions Core Environment will be provisioned." | `string` | n/a | yes |
58 | | [repository\_description](#input\_repository\_description) | A brief description of the GitHub repository. This helps in understanding the purpose and scope of the repository.
This should describe the workload represented by the `application_name`. | `string` | n/a | yes |
59 | | [repository\_name](#input\_repository\_name) | The name of the GitHub repository to be created. This name should be unique within the specified GitHub organization.
The GitHub repository name should correlate to the `application_name` as it will contain the source code for the infrastructure
that is provisioned to Azure. | `string` | n/a | yes |
60 | | [repository\_visibility](#input\_repository\_visibility) | The visibility level of the GitHub repository. Accepted values are 'public', 'private', or 'internal'. Determines who can view and access the repository. | `string` | n/a | yes |
61 | | [terraform\_version](#input\_terraform\_version) | Specifies the version of Terraform to use. Defaults to '1.9.8'. | `string` | `"1.9.8"` | no |
62 |
63 | ## Outputs
64 |
65 | No outputs.
66 |
--------------------------------------------------------------------------------
/modules/azure-vm-core/codebase.tf:
--------------------------------------------------------------------------------
1 | module "codebase" {
2 |
3 | source = "Azure-Terraformer/codebase-terraform-azure-vm-core/github"
4 | version = "1.0.3"
5 |
6 | repository = var.repository_name
7 | branch = github_branch.main.branch
8 | commit_user = var.commit_user
9 | location = var.location
10 |
11 | }
12 |
--------------------------------------------------------------------------------
/modules/azure-vm-core/environments.tf:
--------------------------------------------------------------------------------
1 |
2 | locals {
3 | extended_environments = {
4 | for key, value in var.environments : key => {
5 | subscription_id = value.subscription_id
6 | tenant_id = data.azuread_client_config.current.tenant_id
7 | client_id = module.github_identity[key].application.client_id
8 | branch_name = value.branch_name
9 | reviewers = []
10 | backend = value.backend
11 | }
12 | }
13 | }
14 |
15 | module "github_environments" {
16 | source = "Azure-Terraformer/environment-terraform-azure/github"
17 | version = "1.0.3"
18 |
19 | repository_name = var.repository_name
20 | terraform_version = var.terraform_version
21 | terraform_working_directory = module.codebase.path
22 | delay_after_environment_creation = 10
23 | environments = local.extended_environments
24 |
25 | depends_on = [github_branch_default.default]
26 |
27 | }
28 |
--------------------------------------------------------------------------------
/modules/azure-vm-core/identity.tf:
--------------------------------------------------------------------------------
1 | data "azuread_client_config" "current" {}
2 |
3 | resource "random_string" "main" {
4 | length = 8
5 | upper = false
6 | special = false
7 | }
8 |
9 | module "github_identity" {
10 |
11 | source = "Azure-Terraformer/github-credential/azuread"
12 | version = "1.0.10"
13 |
14 | for_each = var.environments
15 |
16 | name = "${var.github_organization}-${var.repository_name}-${each.key}"
17 | github_organization = var.github_organization
18 | repository_name = var.repository_name
19 | entity_type = "environment"
20 | environment_name = each.key
21 | owners = [data.azuread_client_config.current.object_id]
22 |
23 | }
24 |
25 | data "azurerm_subscription" "main" {
26 |
27 | for_each = var.environments
28 |
29 | subscription_id = each.value.subscription_id
30 |
31 | }
32 |
33 | resource "azurerm_role_assignment" "main" {
34 |
35 | for_each = var.environments
36 |
37 | scope = data.azurerm_subscription.main[each.key].id
38 | role_definition_name = "Owner"
39 | principal_id = module.github_identity[each.key].service_principal.object_id
40 |
41 | }
42 |
--------------------------------------------------------------------------------
/modules/azure-vm-core/main.tf:
--------------------------------------------------------------------------------
1 |
2 | data "github_user" "current" {
3 | username = ""
4 | }
5 |
6 | resource "github_repository" "main" {
7 |
8 | name = var.repository_name
9 | description = var.repository_description
10 |
11 | visibility = var.repository_visibility
12 | delete_branch_on_merge = var.delete_branch_on_merge
13 | auto_init = true
14 |
15 | }
16 |
17 | resource "github_branch" "main" {
18 | repository = var.repository_name
19 | branch = "main"
20 | }
21 |
22 | resource "github_branch_default" "default" {
23 | repository = var.repository_name
24 | branch = github_branch.main.branch
25 | }
26 |
27 | resource "github_actions_variable" "application_name" {
28 |
29 | repository = var.repository_name
30 | variable_name = "APPLICATION_NAME"
31 | value = var.application_name
32 |
33 | depends_on = [github_branch_default.default]
34 |
35 | }
36 |
--------------------------------------------------------------------------------
/modules/azure-vm-core/pipelines.tf:
--------------------------------------------------------------------------------
1 | # Local to convert to map(string)
2 | locals {
3 | branch_name_map = tomap({
4 | for key, value in var.environments : key => value.branch_name
5 | })
6 | }
7 |
8 | module "gitflow" {
9 |
10 | source = "Azure-Terraformer/action-azure-application/github"
11 | version = "1.0.32"
12 |
13 | repository = var.repository_name
14 | branch = github_branch.main.branch
15 | commit_user = var.commit_user
16 | environments = local.branch_name_map
17 |
18 | # we don't want the actions firing when we start pushing commits
19 | depends_on = [module.codebase]
20 |
21 | }
22 |
23 | module "action_azure_login" {
24 | source = "Azure-Terraformer/action-azure-login-test/github"
25 | version = "1.0.1"
26 |
27 | repository = var.repository_name
28 | branch = github_branch.main.branch
29 | commit_user = var.commit_user
30 |
31 | }
32 |
--------------------------------------------------------------------------------
/modules/azure-vm-core/variables.tf:
--------------------------------------------------------------------------------
1 | variable "application_name" {
2 | type = string
3 | description = <
2 | ## Requirements
3 |
4 | | Name | Version |
5 | |------|---------|
6 | | [azuread](#requirement\_azuread) | ~> 3.0.2 |
7 | | [azurerm](#requirement\_azurerm) | ~> 4.10.0 |
8 | | [github](#requirement\_github) | ~> 5.0 |
9 |
10 | ## Providers
11 |
12 | | Name | Version |
13 | |------|---------|
14 | | [random](#provider\_random) | n/a |
15 |
16 | ## Modules
17 |
18 | No modules.
19 |
20 | ## Resources
21 |
22 | | Name | Type |
23 | |------|------|
24 | | [random_string.repository_name](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/string) | resource |
25 |
26 | ## Inputs
27 |
28 | No inputs.
29 |
30 | ## Outputs
31 |
32 | | Name | Description |
33 | |------|-------------|
34 | | [repository\_name\_suffix](#output\_repository\_name\_suffix) | n/a |
35 |
--------------------------------------------------------------------------------
/tests/setup/main.tf:
--------------------------------------------------------------------------------
1 | resource "random_string" "repository_name" {
2 | length = 6
3 | special = false
4 | upper = false
5 | }
6 |
7 | output "repository_name_suffix" {
8 | value = random_string.repository_name.result
9 | }
10 |
--------------------------------------------------------------------------------
/tests/setup/terraform.tf:
--------------------------------------------------------------------------------
1 | terraform {
2 | required_providers {
3 | github = {
4 | source = "integrations/github"
5 | version = "~> 5.0"
6 | }
7 | azurerm = {
8 | source = "hashicorp/azurerm"
9 | version = "~> 4.10.0"
10 | }
11 | azuread = {
12 | source = "hashicorp/azuread"
13 | version = "~> 3.0.2"
14 | }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------