From 25d02d182b53409b13a49bc5bced018db7e62bd8 Mon Sep 17 00:00:00 2001 From: Paul Xu Date: Wed, 24 Jul 2024 18:22:12 -0400 Subject: [PATCH 01/15] update terraform versions --- .pre-commit-config.yaml | 2 +- .tool-versions | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6aba1c4..0b33ed7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/antonbabenko/pre-commit-terraform - rev: v1.81.0 # Get the latest from: https://github.com/antonbabenko/pre-commit-terraform/releases + rev: v1.92.0 # Get the latest from: https://github.com/antonbabenko/pre-commit-terraform/releases hooks: - id: terraform_fmt - id: terraform_docs diff --git a/.tool-versions b/.tool-versions index bbb0794..3874604 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,2 +1 @@ -terraform 1.5.0 -ruby 3.2.2 +terraform 1.9.2 From 64176c83dac9c0bce1bc86b48b3bccc1c942e0b7 Mon Sep 17 00:00:00 2001 From: Paul Xu Date: Wed, 24 Jul 2024 18:23:07 -0400 Subject: [PATCH 02/15] update provider version --- README.md | 5 ++--- version.tf | 8 +++++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index f7fde01..e50a631 100644 --- a/README.md +++ b/README.md @@ -35,9 +35,8 @@ code by adding a `module` configuration and setting its `source` parameter to UR | Name | Version | |------|---------| -| [terraform](#requirement\_terraform) | >= 1.5.0 | -| [google](#requirement\_google) | >= 4.72.0, <5.0.0 | -| [google-beta](#requirement\_google-beta) | >= 4.72.0, <5.0.0 | +| [terraform](#requirement\_terraform) | >= 1.9.2 | +| [google](#requirement\_google) | 5.38.0 | ## Providers diff --git a/version.tf b/version.tf index 93ed479..94395d2 100644 --- a/version.tf +++ b/version.tf @@ -1,8 +1,10 @@ terraform { - required_version = ">= 1.5.0" + required_version = ">= 1.9.2" required_providers { - google = ">= 4.72.0, <5.0.0" - google-beta = ">= 4.72.0, <5.0.0" + google = { + source = "hashicorp/google-beta" + version = "5.38.0" + } } } From 0c35a9ced3e9fecbaf9a80a6109547385485b5be Mon Sep 17 00:00:00 2001 From: Paul Xu Date: Wed, 24 Jul 2024 18:31:41 -0400 Subject: [PATCH 03/15] use trivy instead of tfsec --- .pre-commit-config.yaml | 2 +- Brewfile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0b33ed7..58377c5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,7 +4,7 @@ repos: hooks: - id: terraform_fmt - id: terraform_docs - - id: terraform_tfsec + - id: terraform_trivy - id: terraform_tflint args: - --args=--enable-rule=terraform_unused_declarations \ No newline at end of file diff --git a/Brewfile b/Brewfile index 196f86c..29e9cf3 100644 --- a/Brewfile +++ b/Brewfile @@ -1,6 +1,6 @@ brew "pre-commit" brew "terraform-docs" brew "tflint" -brew "tfsec" +brew "trivy" cask "google-cloud-sdk" \ No newline at end of file From 0d9b9aa3feacde92c57953a237a5a9bb3db50afd Mon Sep 17 00:00:00 2001 From: Paul Xu Date: Wed, 24 Jul 2024 18:34:59 -0400 Subject: [PATCH 04/15] remove everything in example directory --- examples/brown-modules-cluster/main.tf | 73 -------------- examples/brown-modules-cluster/outputs.tf | 37 ------- .../terraform.tfvars.example | 3 - examples/brown-modules-cluster/variables.tf | 19 ---- examples/brown-modules-cluster/version.tf | 8 -- examples/simple-cluster/main.tf | 96 ------------------- examples/simple-cluster/outputs.tf | 36 ------- .../simple-cluster/terraform.tfvars.example | 3 - examples/simple-cluster/variables.tf | 19 ---- examples/simple-cluster/version.tf | 12 --- 10 files changed, 306 deletions(-) delete mode 100644 examples/brown-modules-cluster/main.tf delete mode 100644 examples/brown-modules-cluster/outputs.tf delete mode 100644 examples/brown-modules-cluster/terraform.tfvars.example delete mode 100644 examples/brown-modules-cluster/variables.tf delete mode 100644 examples/brown-modules-cluster/version.tf delete mode 100644 examples/simple-cluster/main.tf delete mode 100644 examples/simple-cluster/outputs.tf delete mode 100644 examples/simple-cluster/terraform.tfvars.example delete mode 100644 examples/simple-cluster/variables.tf delete mode 100644 examples/simple-cluster/version.tf diff --git a/examples/brown-modules-cluster/main.tf b/examples/brown-modules-cluster/main.tf deleted file mode 100644 index bcbdd00..0000000 --- a/examples/brown-modules-cluster/main.tf +++ /dev/null @@ -1,73 +0,0 @@ -# ---------------------------------------------------------------------------- -# TEST RESOURCES -# These resources are directly tested. -# ---------------------------------------------------------------------------- -locals { - gcp_region = "us-east1" - gcp_zone = "us-east1-b" - project_name = "inspec-cluster-brown" - regional = false - network_name = "network-01" - subnet_name = "subnet-01" - routing_mode = "REGIONAL" -} - -# ------------------------------------------------------------ -# MAIN BLOCK -# ------------------------------------------------------------ -# Create the GCP Project -module "project" { - source = "git::https://github.com/BrownUniversity/terraform-gcp-project.git?ref=v0.1.5" - project_name = local.project_name - org_id = var.org_id - billing_account = var.billing_account - folder_id = var.folder_id - activate_apis = var.activate_apis -} - -module "vpc" { - source = "git::https://github.com/BrownUniversity/terraform-gcp-vpc.git?ref=v0.1.3" - project_id = module.project.project_id - network_name = local.network_name - subnet_name = local.subnet_name - subnet_region = local.gcp_region - routing_mode = local.routing_mode -} - - -module "simple_cluster" { - source = "../../" - - network = module.vpc.network_name - subnetwork = module.vpc.subnet_name - - regional = local.regional - region = local.gcp_region - node_zones = [local.gcp_zone] - maintenance_start_time = "03:00" - http_load_balancing = false - horizontal_pod_autoscaling = true - - project_id = module.project.project_id - service_account_email = module.project.service_account_email - - core_pool_machine_type = "n1-standard-1" - core_pool_min_count = 1 - core_pool_max_count = 3 - core_pool_local_ssd_count = 0 - core_pool_disk_size_gb = 10 - core_pool_auto_repair = true - core_pool_auto_upgrade = true - core_pool_preemptible = false - core_pool_initial_node_count = 1 - - user_pool_machine_type = "n1-standard-1" - user_pool_min_count = 0 - user_pool_max_count = 3 - user_pool_local_ssd_count = 0 - user_pool_disk_size_gb = 10 - user_pool_auto_repair = true - user_pool_auto_upgrade = true - user_pool_preemptible = false - user_pool_initial_node_count = 1 -} diff --git a/examples/brown-modules-cluster/outputs.tf b/examples/brown-modules-cluster/outputs.tf deleted file mode 100644 index c0d652e..0000000 --- a/examples/brown-modules-cluster/outputs.tf +++ /dev/null @@ -1,37 +0,0 @@ -output "project_id" { - value = module.project.project_id -} - -output "project_name" { - value = local.project_name -} - -output "location" { - value = module.simple_cluster.location -} - -output "service_account" { - description = "The service account to default running nodes as if not overridden in `node_pools`." - value = module.simple_cluster.service_account -} - -output "region" { - value = module.simple_cluster.region -} - -output "cluster_name" { - description = "Cluster name" - value = module.simple_cluster.cluster_name -} - -output "network_name" { - value = module.vpc.network_name -} - -output "subnet_name" { - value = module.vpc.subnet_name -} - - - - diff --git a/examples/brown-modules-cluster/terraform.tfvars.example b/examples/brown-modules-cluster/terraform.tfvars.example deleted file mode 100644 index 34edb64..0000000 --- a/examples/brown-modules-cluster/terraform.tfvars.example +++ /dev/null @@ -1,3 +0,0 @@ -org_id = -billing_account = -folder_id = diff --git a/examples/brown-modules-cluster/variables.tf b/examples/brown-modules-cluster/variables.tf deleted file mode 100644 index 88f5fde..0000000 --- a/examples/brown-modules-cluster/variables.tf +++ /dev/null @@ -1,19 +0,0 @@ -variable "org_id" { - type = string -} -variable "billing_account" { - type = string -} -variable "folder_id" { - type = string -} -variable "activate_apis" { - type = list(string) - description = "The list of apis to activate within the project" - default = [ - "compute.googleapis.com", - "container.googleapis.com", - "containerregistry.googleapis.com", - "appengine.googleapis.com" - ] -} diff --git a/examples/brown-modules-cluster/version.tf b/examples/brown-modules-cluster/version.tf deleted file mode 100644 index 93ed479..0000000 --- a/examples/brown-modules-cluster/version.tf +++ /dev/null @@ -1,8 +0,0 @@ -terraform { - required_version = ">= 1.5.0" - - required_providers { - google = ">= 4.72.0, <5.0.0" - google-beta = ">= 4.72.0, <5.0.0" - } -} diff --git a/examples/simple-cluster/main.tf b/examples/simple-cluster/main.tf deleted file mode 100644 index be09ced..0000000 --- a/examples/simple-cluster/main.tf +++ /dev/null @@ -1,96 +0,0 @@ -# ---------------------------------------------------------------------------- -# TEST RESOURCES -# These resources are directly tested. -# ---------------------------------------------------------------------------- -locals { - gcp_region = "us-east1" - gcp_zone = "us-east1-b" - project_name = "inspec-cluster-test1" - network_prefix = "cft-gke-test" - regional = false -} - -# ------------------------------------------------------------ -# MAIN BLOCK -# ------------------------------------------------------------ -# Create the GCP Project -module "project" { - source = "git::https://github.com/BrownUniversity/terraform-gcp-project.git?ref=v0.1.4" - project_name = local.project_name - org_id = var.org_id - billing_account = var.billing_account - folder_id = var.folder_id - activate_apis = var.activate_apis -} - -resource "random_string" "suffix" { - length = 4 - special = false - upper = false -} - -resource "google_compute_network" "main" { - project = module.project.project_id - name = "${local.network_prefix}-${random_string.suffix.result}" - auto_create_subnetworks = false -} - -resource "google_compute_subnetwork" "main" { - project = module.project.project_id - name = "${local.network_prefix}-${random_string.suffix.result}" - ip_cidr_range = "10.0.0.0/17" - region = local.gcp_region - network = google_compute_network.main.self_link - - secondary_ip_range { - range_name = "${local.network_prefix}-pods-${random_string.suffix.result}" - ip_cidr_range = "192.168.0.0/18" - } - - secondary_ip_range { - range_name = "${local.network_prefix}-services-${random_string.suffix.result}" - ip_cidr_range = "192.168.64.0/18" - } -} - - - - -module "simple_cluster" { - source = "../../" - - network = google_compute_network.main.name - subnetwork = google_compute_subnetwork.main.name - ip_range_pods = google_compute_subnetwork.main.secondary_ip_range[0].range_name - ip_range_services = google_compute_subnetwork.main.secondary_ip_range[1].range_name - - regional = local.regional - region = local.gcp_region - node_zones = [local.gcp_zone] - maintenance_start_time = "03:00" - http_load_balancing = false - horizontal_pod_autoscaling = true - - project_id = module.project.project_id - service_account_email = module.project.service_account_email - - core_pool_machine_type = "n1-standard-1" - core_pool_min_count = 1 - core_pool_max_count = 3 - core_pool_local_ssd_count = 0 - core_pool_disk_size_gb = 10 - core_pool_auto_repair = true - core_pool_auto_upgrade = true - core_pool_preemptible = false - core_pool_initial_node_count = 1 - - user_pool_machine_type = "n1-standard-1" - user_pool_min_count = 0 - user_pool_max_count = 3 - user_pool_local_ssd_count = 0 - user_pool_disk_size_gb = 10 - user_pool_auto_repair = true - user_pool_auto_upgrade = true - user_pool_preemptible = false - user_pool_initial_node_count = 1 -} diff --git a/examples/simple-cluster/outputs.tf b/examples/simple-cluster/outputs.tf deleted file mode 100644 index 1fdc28c..0000000 --- a/examples/simple-cluster/outputs.tf +++ /dev/null @@ -1,36 +0,0 @@ -output "project_id" { - value = module.project.project_id -} - -output "project_name" { - value = local.project_name -} - -output "location" { - value = module.simple_cluster.location -} - -output "service_account" { - description = "The service account to default running nodes as if not overridden in `node_pools`." - value = module.simple_cluster.service_account -} - -output "network_prefix" { - value = local.network_prefix -} - -output "random_string" { - value = random_string.suffix.result -} - -output "cluster_name" { - description = "Cluster name" - value = module.simple_cluster.cluster_name -} - - - - - - - diff --git a/examples/simple-cluster/terraform.tfvars.example b/examples/simple-cluster/terraform.tfvars.example deleted file mode 100644 index 34edb64..0000000 --- a/examples/simple-cluster/terraform.tfvars.example +++ /dev/null @@ -1,3 +0,0 @@ -org_id = -billing_account = -folder_id = diff --git a/examples/simple-cluster/variables.tf b/examples/simple-cluster/variables.tf deleted file mode 100644 index 88f5fde..0000000 --- a/examples/simple-cluster/variables.tf +++ /dev/null @@ -1,19 +0,0 @@ -variable "org_id" { - type = string -} -variable "billing_account" { - type = string -} -variable "folder_id" { - type = string -} -variable "activate_apis" { - type = list(string) - description = "The list of apis to activate within the project" - default = [ - "compute.googleapis.com", - "container.googleapis.com", - "containerregistry.googleapis.com", - "appengine.googleapis.com" - ] -} diff --git a/examples/simple-cluster/version.tf b/examples/simple-cluster/version.tf deleted file mode 100644 index 4033b52..0000000 --- a/examples/simple-cluster/version.tf +++ /dev/null @@ -1,12 +0,0 @@ -terraform { - required_version = ">= 1.5.0" - - required_providers { - google = ">= 4.72.0, <5.0.0" - google-beta = ">= 4.72.0, <5.0.0" - random = { - source = "hashicorp/random" - version = "3.5.1" - } - } -} From d8a4614381b8903d62516d5e476f6762aec047f7 Mon Sep 17 00:00:00 2001 From: Paul Xu Date: Wed, 24 Jul 2024 18:53:25 -0400 Subject: [PATCH 05/15] switch back to tfsec --- .pre-commit-config.yaml | 2 +- README.md | 10 ++++++---- main.tf | 8 ++------ outputs.tf | 19 +++++++++++++++++-- variables.tf | 16 +++++----------- 5 files changed, 31 insertions(+), 24 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 58377c5..0b33ed7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,7 +4,7 @@ repos: hooks: - id: terraform_fmt - id: terraform_docs - - id: terraform_trivy + - id: terraform_tfsec - id: terraform_tflint args: - --args=--enable-rule=terraform_unused_declarations \ No newline at end of file diff --git a/README.md b/README.md index e50a631..dfae664 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ No providers. | Name | Source | Version | |------|--------|---------| -| [gke](#module\_gke) | terraform-google-modules/kubernetes-engine/google//modules/beta-private-cluster | 27.0.0 | +| [gke](#module\_gke) | terraform-google-modules/kubernetes-engine/google//modules/beta-private-cluster | 31.0.0 | ## Resources @@ -70,7 +70,7 @@ No resources. | [core\_pool\_name](#input\_core\_pool\_name) | Name for the core-component pool | `string` | `"core-pool"` | no | | [core\_pool\_preemptible](#input\_core\_pool\_preemptible) | Make core-component pool preemptible | `bool` | `false` | no | | [create\_service\_account](#input\_create\_service\_account) | Defines if service account specified to run nodes should be created. | `bool` | `false` | no | -| [enable\_private\_nodes](#input\_enable\_private\_nodes) | (Beta) Whether nodes have internal IP addresses only | `bool` | `true` | no | +| [deletion\_protection](#input\_deletion\_protection) | Enable deletion protection for the cluster | `bool` | `false` | no | | [gce\_pd\_csi\_driver](#input\_gce\_pd\_csi\_driver) | (Beta) Whether this cluster should enable the Google Compute Engine Persistent Disk Container Storage Interface (CSI) Driver. | `bool` | `true` | no | | [horizontal\_pod\_autoscaling](#input\_horizontal\_pod\_autoscaling) | Enable horizontal pod autoscaling addon | `bool` | `true` | no | | [http\_load\_balancing](#input\_http\_load\_balancing) | Enable http load balancer add-on | `bool` | `false` | no | @@ -79,7 +79,6 @@ No resources. | [kubernetes\_version](#input\_kubernetes\_version) | The Kubernetes version of the masters. If set to 'latest' it will pull latest available version in the selected region. | `string` | `"latest"` | no | | [logging\_service](#input\_logging\_service) | The logging service that the cluster should write logs to. Available options include logging.googleapis.com, logging.googleapis.com/kubernetes (beta), and none | `string` | `"logging.googleapis.com/kubernetes"` | no | | [maintenance\_start\_time](#input\_maintenance\_start\_time) | Time window specified for daily maintenance operations in RFC3339 format | `string` | `"03:00"` | no | -| [master\_ipv4\_cidr\_block](#input\_master\_ipv4\_cidr\_block) | (Beta) The IP range in CIDR notation to use for the hosted master network | `string` | `"172.16.0.0/28"` | no | | [monitoring\_service](#input\_monitoring\_service) | The monitoring service that the cluster should write metrics to. Automatically send metrics from pods in the cluster to the Google Cloud Monitoring API. VM metrics will be collected by Google Compute Engine regardless of this setting Available options include monitoring.googleapis.com, monitoring.googleapis.com/kubernetes (beta) and none | `string` | `"monitoring.googleapis.com/kubernetes"` | no | | [network](#input\_network) | The VPC network to host the cluster in. | `string` | `"kubernetes-vpc"` | no | | [network\_policy](#input\_network\_policy) | Enable network policy addon | `bool` | `true` | no | @@ -109,7 +108,10 @@ No resources. | Name | Description | |------|-------------| | [cluster\_name](#output\_cluster\_name) | Cluster name | -| [location](#output\_location) | n/a | +| [horizontal\_pod\_autoscaling\_enabled](#output\_horizontal\_pod\_autoscaling\_enabled) | Whether the cluster enables horizontal pod autoscaling | +| [http\_load\_balancing\_enabled](#output\_http\_load\_balancing\_enabled) | Whether the cluster enables HTTP load balancing | +| [location](#output\_location) | The location (region or zone) in which the cluster master will be created | +| [node\_pools\_names](#output\_node\_pools\_names) | List of node pools names | | [region](#output\_region) | n/a | | [service\_account](#output\_service\_account) | The service account to default running nodes as if not overridden in `node_pools`. | | [zones](#output\_zones) | List of zones in which the cluster resides | diff --git a/main.tf b/main.tf index 69d2141..2e84da9 100644 --- a/main.tf +++ b/main.tf @@ -4,12 +4,9 @@ # Create the GKE Cluster -# tfsec:ignore:google-gke-enforce-pod-security-policy -# tfsec:ignore:google-gke-enable-master-networks -# tfsec:ignore:google-gke-use-cluster-labels module "gke" { source = "terraform-google-modules/kubernetes-engine/google//modules/beta-private-cluster" - version = "27.0.0" + version = "31.0.0" release_channel = var.release_channel kubernetes_version = var.kubernetes_version project_id = var.project_id @@ -29,10 +26,9 @@ module "gke" { http_load_balancing = var.http_load_balancing horizontal_pod_autoscaling = var.horizontal_pod_autoscaling network_policy = var.network_policy - enable_private_nodes = var.enable_private_nodes - master_ipv4_cidr_block = var.master_ipv4_cidr_block remove_default_node_pool = var.remove_default_node_pool gce_pd_csi_driver = var.gce_pd_csi_driver + deletion_protection = var.deletion_protection node_pools = [ { diff --git a/outputs.tf b/outputs.tf index 874ee81..19f4f25 100644 --- a/outputs.tf +++ b/outputs.tf @@ -8,11 +8,12 @@ output "region" { output "cluster_name" { description = "Cluster name" - value = var.cluster_name + value = module.gke.name } output "location" { - value = module.gke.location + description = "The location (region or zone) in which the cluster master will be created" + value = module.gke.location } output "zones" { @@ -25,3 +26,17 @@ output "service_account" { value = module.gke.service_account } +output "node_pools_names" { + description = "List of node pools names" + value = module.gke.node_pools_names +} + +output "http_load_balancing_enabled" { + description = "Whether the cluster enables HTTP load balancing" + value = module.gke.http_load_balancing_enabled +} + +output "horizontal_pod_autoscaling_enabled" { + description = "Whether the cluster enables horizontal pod autoscaling" + value = module.gke.horizontal_pod_autoscaling_enabled +} diff --git a/variables.tf b/variables.tf index f50a178..4f22277 100644 --- a/variables.tf +++ b/variables.tf @@ -115,21 +115,15 @@ variable "network_policy" { default = true } -variable "enable_private_nodes" { +variable "remove_default_node_pool" { type = bool - description = "(Beta) Whether nodes have internal IP addresses only" - default = true -} - -variable "master_ipv4_cidr_block" { - type = string - description = "(Beta) The IP range in CIDR notation to use for the hosted master network" - default = "172.16.0.0/28" + description = "Remove default node pool while setting up the cluster" + default = false } -variable "remove_default_node_pool" { +variable "deletion_protection" { type = bool - description = "Remove default node pool while setting up the cluster" + description = "Enable deletion protection for the cluster" default = false } From 0630e53902c1ab908fb6016819c69e254d6b351b Mon Sep 17 00:00:00 2001 From: Paul Xu Date: Wed, 24 Jul 2024 18:54:07 -0400 Subject: [PATCH 06/15] add ignore rules --- main.tf | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/main.tf b/main.tf index 2e84da9..1f40c43 100644 --- a/main.tf +++ b/main.tf @@ -4,6 +4,10 @@ # Create the GKE Cluster +#tfsec:ignore:google-gke-enforce-pod-security-policy +#tfsec:ignore:google-gke-enable-master-networks +#tfsec:ignore:google-gke-enable-private-cluster +#tfsec:ignore:google-gke-use-cluster-labels module "gke" { source = "terraform-google-modules/kubernetes-engine/google//modules/beta-private-cluster" version = "31.0.0" From 929325afea95db6ea0bd71b52a3f4e2ea244f817 Mon Sep 17 00:00:00 2001 From: Paul Xu Date: Wed, 24 Jul 2024 19:09:48 -0400 Subject: [PATCH 07/15] add test modules --- tests/brown-modules-cluster/main.tf | 73 ++++++++++++++++ tests/brown-modules-cluster/outputs.tf | 37 ++++++++ tests/brown-modules-cluster/variables.tf | 19 +++++ tests/brown-modules-cluster/version.tf | 10 +++ tests/simple-cluster/main.tf | 102 +++++++++++++++++++++++ tests/simple-cluster/outputs.tf | 44 ++++++++++ tests/simple-cluster/variables.tf | 19 +++++ tests/simple-cluster/version.tf | 14 ++++ tests/terraform.tfvars.example | 3 + 9 files changed, 321 insertions(+) create mode 100644 tests/brown-modules-cluster/main.tf create mode 100644 tests/brown-modules-cluster/outputs.tf create mode 100644 tests/brown-modules-cluster/variables.tf create mode 100644 tests/brown-modules-cluster/version.tf create mode 100644 tests/simple-cluster/main.tf create mode 100644 tests/simple-cluster/outputs.tf create mode 100644 tests/simple-cluster/variables.tf create mode 100644 tests/simple-cluster/version.tf create mode 100644 tests/terraform.tfvars.example diff --git a/tests/brown-modules-cluster/main.tf b/tests/brown-modules-cluster/main.tf new file mode 100644 index 0000000..a3ad0d3 --- /dev/null +++ b/tests/brown-modules-cluster/main.tf @@ -0,0 +1,73 @@ +# ---------------------------------------------------------------------------- +# TEST RESOURCES +# These resources are directly tested. +# ---------------------------------------------------------------------------- +locals { + gcp_region = "us-east1" + gcp_zone = "us-east1-b" + project_name = "inspec-cluster-brown" + regional = false + network_name = "network-01" + subnet_name = "subnet-01" + routing_mode = "REGIONAL" +} + +# ------------------------------------------------------------ +# MAIN BLOCK +# ------------------------------------------------------------ +# Create the GCP Project +module "project" { + source = "git::https://github.com/BrownUniversity/terraform-gcp-project.git?ref=v0.1.6" + project_name = local.project_name + org_id = var.org_id + billing_account = var.billing_account + folder_id = var.folder_id + activate_apis = var.activate_apis +} + +module "vpc" { + source = "git::https://github.com/BrownUniversity/terraform-gcp-vpc.git?ref=v0.1.4" + project_id = module.project.project_id + network_name = local.network_name + subnet_name = local.subnet_name + subnet_region = local.gcp_region + routing_mode = local.routing_mode +} + +#tfsec:ignore:google-gke-use-service-account +module "simple_cluster" { + source = "../../" + + network = module.vpc.network_name + subnetwork = module.vpc.subnet_name + + regional = local.regional + region = local.gcp_region + node_zones = [local.gcp_zone] + maintenance_start_time = "03:00" + http_load_balancing = false + horizontal_pod_autoscaling = true + + project_id = module.project.project_id + service_account_email = module.project.service_account_email + + core_pool_machine_type = "n1-standard-1" + core_pool_min_count = 1 + core_pool_max_count = 3 + core_pool_local_ssd_count = 0 + core_pool_disk_size_gb = 10 + core_pool_auto_repair = true + core_pool_auto_upgrade = true + core_pool_preemptible = false + core_pool_initial_node_count = 1 + + user_pool_machine_type = "n1-standard-1" + user_pool_min_count = 0 + user_pool_max_count = 3 + user_pool_local_ssd_count = 0 + user_pool_disk_size_gb = 10 + user_pool_auto_repair = true + user_pool_auto_upgrade = true + user_pool_preemptible = false + user_pool_initial_node_count = 1 +} diff --git a/tests/brown-modules-cluster/outputs.tf b/tests/brown-modules-cluster/outputs.tf new file mode 100644 index 0000000..c0d652e --- /dev/null +++ b/tests/brown-modules-cluster/outputs.tf @@ -0,0 +1,37 @@ +output "project_id" { + value = module.project.project_id +} + +output "project_name" { + value = local.project_name +} + +output "location" { + value = module.simple_cluster.location +} + +output "service_account" { + description = "The service account to default running nodes as if not overridden in `node_pools`." + value = module.simple_cluster.service_account +} + +output "region" { + value = module.simple_cluster.region +} + +output "cluster_name" { + description = "Cluster name" + value = module.simple_cluster.cluster_name +} + +output "network_name" { + value = module.vpc.network_name +} + +output "subnet_name" { + value = module.vpc.subnet_name +} + + + + diff --git a/tests/brown-modules-cluster/variables.tf b/tests/brown-modules-cluster/variables.tf new file mode 100644 index 0000000..88f5fde --- /dev/null +++ b/tests/brown-modules-cluster/variables.tf @@ -0,0 +1,19 @@ +variable "org_id" { + type = string +} +variable "billing_account" { + type = string +} +variable "folder_id" { + type = string +} +variable "activate_apis" { + type = list(string) + description = "The list of apis to activate within the project" + default = [ + "compute.googleapis.com", + "container.googleapis.com", + "containerregistry.googleapis.com", + "appengine.googleapis.com" + ] +} diff --git a/tests/brown-modules-cluster/version.tf b/tests/brown-modules-cluster/version.tf new file mode 100644 index 0000000..94395d2 --- /dev/null +++ b/tests/brown-modules-cluster/version.tf @@ -0,0 +1,10 @@ +terraform { + required_version = ">= 1.9.2" + + required_providers { + google = { + source = "hashicorp/google-beta" + version = "5.38.0" + } + } +} diff --git a/tests/simple-cluster/main.tf b/tests/simple-cluster/main.tf new file mode 100644 index 0000000..82f3db7 --- /dev/null +++ b/tests/simple-cluster/main.tf @@ -0,0 +1,102 @@ +# ---------------------------------------------------------------------------- +# TEST RESOURCES +# These resources are directly tested. +# ---------------------------------------------------------------------------- +locals { + gcp_region = "us-east1" + gcp_zone = "us-east1-b" + project_name = "inspec-cluster-test1" + network_prefix = "cft-gke-test" + regional = false +} + +# ------------------------------------------------------------ +# MAIN BLOCK +# ------------------------------------------------------------ +# Create the GCP Project +module "project" { + source = "git::https://github.com/BrownUniversity/terraform-gcp-project.git?ref=v0.1.6" + project_name = local.project_name + org_id = var.org_id + billing_account = var.billing_account + folder_id = var.folder_id + activate_apis = var.activate_apis +} + +resource "random_string" "suffix" { + length = 4 + special = false + upper = false +} + +resource "google_compute_network" "main" { + project = module.project.project_id + name = "${local.network_prefix}-${random_string.suffix.result}" + auto_create_subnetworks = false +} + +resource "google_compute_subnetwork" "main" { + project = module.project.project_id + name = "${local.network_prefix}-${random_string.suffix.result}" + ip_cidr_range = "10.0.0.0/17" + region = local.gcp_region + network = google_compute_network.main.self_link + + secondary_ip_range { + range_name = "${local.network_prefix}-pods-${random_string.suffix.result}" + ip_cidr_range = "192.168.0.0/18" + } + + secondary_ip_range { + range_name = "${local.network_prefix}-services-${random_string.suffix.result}" + ip_cidr_range = "192.168.64.0/18" + } + + log_config { + aggregation_interval = "INTERVAL_10_MIN" + flow_sampling = 0.5 + metadata = "INCLUDE_ALL_METADATA" + } +} + + + +#tfsec:ignore:google-gke-use-service-account +module "simple_cluster" { + source = "../../" + + network = google_compute_network.main.name + subnetwork = google_compute_subnetwork.main.name + ip_range_pods = google_compute_subnetwork.main.secondary_ip_range[0].range_name + ip_range_services = google_compute_subnetwork.main.secondary_ip_range[1].range_name + + regional = local.regional + region = local.gcp_region + node_zones = [local.gcp_zone] + maintenance_start_time = "03:00" + http_load_balancing = false + horizontal_pod_autoscaling = true + + project_id = module.project.project_id + service_account_email = module.project.service_account_email + + core_pool_machine_type = "n1-standard-1" + core_pool_min_count = 1 + core_pool_max_count = 3 + core_pool_local_ssd_count = 0 + core_pool_disk_size_gb = 10 + core_pool_auto_repair = true + core_pool_auto_upgrade = true + core_pool_preemptible = false + core_pool_initial_node_count = 1 + + user_pool_machine_type = "n1-standard-1" + user_pool_min_count = 0 + user_pool_max_count = 3 + user_pool_local_ssd_count = 0 + user_pool_disk_size_gb = 10 + user_pool_auto_repair = true + user_pool_auto_upgrade = true + user_pool_preemptible = false + user_pool_initial_node_count = 1 +} diff --git a/tests/simple-cluster/outputs.tf b/tests/simple-cluster/outputs.tf new file mode 100644 index 0000000..3dc7bc6 --- /dev/null +++ b/tests/simple-cluster/outputs.tf @@ -0,0 +1,44 @@ +output "project_id" { + value = module.project.project_id +} + +output "project_name" { + value = local.project_name +} + +output "location" { + value = module.simple_cluster.location +} + +output "service_account" { + description = "The service account to default running nodes as if not overridden in `node_pools`." + value = module.simple_cluster.service_account +} + +output "network_prefix" { + value = local.network_prefix +} + +output "random_string" { + value = random_string.suffix.result +} + +output "cluster_name" { + description = "Cluster name" + value = module.simple_cluster.cluster_name +} + +output "node_pools_names" { + description = "List of node pools names" + value = module.simple_cluster.node_pools_names +} + +output "http_load_balancing_enabled" { + description = "Whether the cluster enables HTTP load balancing" + value = module.simple_cluster.http_load_balancing_enabled +} + +output "horizontal_pod_autoscaling_enabled" { + description = "Whether the cluster enables horizontal pod autoscaling" + value = module.simple_cluster.horizontal_pod_autoscaling_enabled +} diff --git a/tests/simple-cluster/variables.tf b/tests/simple-cluster/variables.tf new file mode 100644 index 0000000..88f5fde --- /dev/null +++ b/tests/simple-cluster/variables.tf @@ -0,0 +1,19 @@ +variable "org_id" { + type = string +} +variable "billing_account" { + type = string +} +variable "folder_id" { + type = string +} +variable "activate_apis" { + type = list(string) + description = "The list of apis to activate within the project" + default = [ + "compute.googleapis.com", + "container.googleapis.com", + "containerregistry.googleapis.com", + "appengine.googleapis.com" + ] +} diff --git a/tests/simple-cluster/version.tf b/tests/simple-cluster/version.tf new file mode 100644 index 0000000..f5eb5fe --- /dev/null +++ b/tests/simple-cluster/version.tf @@ -0,0 +1,14 @@ +terraform { + required_version = ">= 1.9.2" + + required_providers { + google = { + source = "hashicorp/google-beta" + version = "5.38.0" + } + random = { + source = "hashicorp/random" + version = "3.6.2" + } + } +} \ No newline at end of file diff --git a/tests/terraform.tfvars.example b/tests/terraform.tfvars.example new file mode 100644 index 0000000..34edb64 --- /dev/null +++ b/tests/terraform.tfvars.example @@ -0,0 +1,3 @@ +org_id = +billing_account = +folder_id = From c9987f90d4783c37d5e98d4d08662cb1fa22058e Mon Sep 17 00:00:00 2001 From: Paul Xu Date: Wed, 24 Jul 2024 19:10:12 -0400 Subject: [PATCH 08/15] add test files --- tests/test_brown_modules_cluster.tftest.hcl | 46 ++++++++++++++ tests/test_simple_cluster.tftest.hcl | 66 +++++++++++++++++++++ 2 files changed, 112 insertions(+) create mode 100644 tests/test_brown_modules_cluster.tftest.hcl create mode 100644 tests/test_simple_cluster.tftest.hcl diff --git a/tests/test_brown_modules_cluster.tftest.hcl b/tests/test_brown_modules_cluster.tftest.hcl new file mode 100644 index 0000000..d180ab7 --- /dev/null +++ b/tests/test_brown_modules_cluster.tftest.hcl @@ -0,0 +1,46 @@ +run "create_gke_cluster" { + + module { + source = "./tests/brown-modules-cluster" + } + + assert { + condition = output.project_id != "" + error_message = "Project ID should not be empty" + } + + assert { + condition = output.project_name == "inspec-cluster-brown" + error_message = "Project name is incorrect" + } + + assert { + condition = output.location == "us-east1-b" + error_message = "Cluster location does not match expected value" + } + + assert { + condition = output.service_account != "" + error_message = "Service account should not be empty" + } + + assert { + condition = output.region == "us-east1" + error_message = "Cluster name should not be empty" + } + + assert { + condition = output.cluster_name != "" + error_message = "Cluster name should not be empty" + } + + assert { + condition = output.network_name == "network-01" + error_message = "Cluster network does not match the created network" + } + + assert { + condition = output.subnet_name == "subnet-01" + error_message = "Cluster subnetwork does not match the created subnetwork" + } +} diff --git a/tests/test_simple_cluster.tftest.hcl b/tests/test_simple_cluster.tftest.hcl new file mode 100644 index 0000000..5ce9e1a --- /dev/null +++ b/tests/test_simple_cluster.tftest.hcl @@ -0,0 +1,66 @@ +run "create_gke_cluster" { + + module { + source = "./tests/simple-cluster" + } + + assert { + condition = output.project_id != "" + error_message = "Project ID should not be empty" + } + + assert { + condition = output.project_name == "inspec-cluster-test1" + error_message = "Project name is incorrect" + } + + assert { + condition = output.location == "us-east1-b" + error_message = "Cluster location does not match expected value" + } + + assert { + condition = output.cluster_name != "" + error_message = "Cluster name should not be empty" + } + + assert { + condition = length(output.node_pools_names) == 4 + error_message = "Expected 2 node pools (core and user)" + } + + assert { + condition = output.node_pools_names[0] == "core-pool" + error_message = "First node pool should be named 'core-pool'" + } + + assert { + condition = output.node_pools_names[1] == "user-pool" + error_message = "Second node pool should be named 'user-pool'" + } + + assert { + condition = !output.http_load_balancing_enabled + error_message = "HTTP load balancing should be disabled" + } + + assert { + condition = output.horizontal_pod_autoscaling_enabled + error_message = "Horizontal pod autoscaling should be enabled" + } + + assert { + condition = google_compute_network.main.name == "${output.network_prefix}-${output.random_string}" + error_message = "Cluster network does not match the created network" + } + + assert { + condition = google_compute_subnetwork.main.name == "${output.network_prefix}-${output.random_string}" + error_message = "Cluster subnetwork does not match the created subnetwork" + } + + assert { + condition = output.service_account != "" + error_message = "Service account should not be empty" + } +} From ce121e879a0edb83d54d5bcd84294df05f28f727 Mon Sep 17 00:00:00 2001 From: Paul Xu Date: Wed, 24 Jul 2024 19:10:30 -0400 Subject: [PATCH 09/15] add terraform lock file --- .terraform.lock.hcl | 122 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 .terraform.lock.hcl diff --git a/.terraform.lock.hcl b/.terraform.lock.hcl new file mode 100644 index 0000000..85a4a29 --- /dev/null +++ b/.terraform.lock.hcl @@ -0,0 +1,122 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/google" { + version = "5.38.0" + constraints = ">= 3.43.0, >= 3.53.0, >= 4.28.0, >= 5.22.0, >= 5.25.0, 5.38.0, < 6.0.0" + hashes = [ + "h1:5xqoIwJbuXeHvq2NVt9S783fGS05hQUHg1AnnIJ+6EI=", + "zh:0db794dfbd4d3604499f1a4011faf7aa3f4c07eace32aa238bdf0b235965259a", + "zh:372f399f67c7e320c25e71d5a2c4c9bad6b4f9b35f7fcdc86fb5009711e34751", + "zh:a010ff05ea62396ee5d0771c614a7e6abd6a869e8ee9b7d9da0747eb4b10f2ba", + "zh:a416a2d7784436409a317cdf42a82eaf98d0f2d935bb5bacb50820f88207e1d9", + "zh:b3926e2295604c87aae81f305d0d1960157a734811ec0b3324f933c8c2421b1a", + "zh:c317d75cf290fac34be9438c14ecf8a9f80f966823e5346020fe7b6c6281259b", + "zh:c97127635c25cc72fc3e2d6bb48b2e9f88730b3c46629bdc335e391791ad53da", + "zh:ccfd23ad869c3bd2037a231718dea44e42c12cff605880edbb37c46255bd1e91", + "zh:dfd389432df9084e54c32809ab960058bafa002393a051640d11bf70533f1cc7", + "zh:ecb5b65be8188a51b19b37be587797a72b238bbdc252aee2e1e22dd5f59e20df", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + "zh:fa5f5d9bce05bb02ef58bead70e4b8653feb98293eb4e0d95bce58b3b1f0a943", + ] +} + +provider "registry.terraform.io/hashicorp/google-beta" { + version = "5.38.0" + constraints = ">= 3.43.0, >= 4.11.0, >= 5.22.0, >= 5.25.0, 5.38.0, < 6.0.0" + hashes = [ + "h1:Q4bKg0gqqOa3IUks206PqHTTyDM0LsLB/lwpn0AALnk=", + "zh:19b33b874b1aa47699dfefc2691464932d6974fbe14bff7b60e0f64b3142e334", + "zh:215f675174d4074f11226f5c48b75880b59c2d56f0ae22ae3ea16012023b68e3", + "zh:22bc73738a7a57e3a022b28bdb34039da07cc105c1f46b288002c02b05c05a71", + "zh:4876a02bc040c98fb662e985a78caecd2b6a7355480362648ff36826e45d09fa", + "zh:899a64484ea44092dd0822478032e87f661dd85e8d735adbb553f7796d7ee497", + "zh:991990de697bd32066e4f2b67aa10c59b84e18f75170be976e75fb711cd0070f", + "zh:a4db4d424c060c0e8f686b0fb714aa7d41e3a5ba20d6aba05496eb8c6e635a3d", + "zh:a988d8a24e673aeff6b1a298d731ed69ef63bba7af9cd4ceb5c51834eecf9fde", + "zh:be3855a320cc44b1936c0778c9cbcaebe92435a2d5eef1f6e5a967ce2a3dc01e", + "zh:e26c72bc15ae27a3a6a99ed0f60ed38f2fca54a310c7ddfee3126b93fb90509e", + "zh:efbe757f0764a1dd69d23a440c03eaf69a8e5742ad63e814212611a16d769eb6", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + ] +} + +provider "registry.terraform.io/hashicorp/kubernetes" { + version = "2.31.0" + constraints = "~> 2.10" + hashes = [ + "h1:ZlKkkHJrjF4AiMueI2yA+abBc1c37cfwjyxURdLKhEw=", + "zh:0d16b861edb2c021b3e9d759b8911ce4cf6d531320e5dc9457e2ea64d8c54ecd", + "zh:1bad69ed535a5f32dec70561eb481c432273b81045d788eb8b37f2e4a322cc40", + "zh:43c58e3912fcd5bb346b5cb89f31061508a9be3ca7dd4cd8169c066203bcdfb3", + "zh:4778123da9206918a92dfa73cc711475d2b9a8275ff25c13a30513c523ac9660", + "zh:8bfa67d2db03b3bfae62beebe6fb961aee8d91b7a766efdfe4d337b33dfd23dd", + "zh:9020bb5729db59a520ade5e24984b737e65f8b81751fbbd343926f6d44d22176", + "zh:90431dbfc5b92498bfbce38f0b989978c84421a6c33245b97788a46b563fbd6e", + "zh:b71a061dda1244f6a52500e703a9524b851e7b11bbf238c17bbd282f27d51cb2", + "zh:d6232a7651b834b89591b94bf4446050119dcde740247e6083a4d55a2cefd28a", + "zh:d89fba43e699e28e2b5e92fff2f75fc03dbc8de0df9dacefe1a8836f8f430753", + "zh:ef85c0b744f5ba1b10dadc3c11e331ba4225c45bb733e024d7218c24b02b0512", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + ] +} + +provider "registry.terraform.io/hashicorp/null" { + version = "3.2.2" + constraints = ">= 2.1.0" + hashes = [ + "h1:IMVAUHKoydFrlPrl9OzasDnw/8ntZFerCC9iXw1rXQY=", + "zh:3248aae6a2198f3ec8394218d05bd5e42be59f43a3a7c0b71c66ec0df08b69e7", + "zh:32b1aaa1c3013d33c245493f4a65465eab9436b454d250102729321a44c8ab9a", + "zh:38eff7e470acb48f66380a73a5c7cdd76cc9b9c9ba9a7249c7991488abe22fe3", + "zh:4c2f1faee67af104f5f9e711c4574ff4d298afaa8a420680b0cb55d7bbc65606", + "zh:544b33b757c0b954dbb87db83a5ad921edd61f02f1dc86c6186a5ea86465b546", + "zh:696cf785090e1e8cf1587499516b0494f47413b43cb99877ad97f5d0de3dc539", + "zh:6e301f34757b5d265ae44467d95306d61bef5e41930be1365f5a8dcf80f59452", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:913a929070c819e59e94bb37a2a253c228f83921136ff4a7aa1a178c7cce5422", + "zh:aa9015926cd152425dbf86d1abdbc74bfe0e1ba3d26b3db35051d7b9ca9f72ae", + "zh:bb04798b016e1e1d49bcc76d62c53b56c88c63d6f2dfe38821afef17c416a0e1", + "zh:c23084e1b23577de22603cff752e59128d83cfecc2e6819edadd8cf7a10af11e", + ] +} + +provider "registry.terraform.io/hashicorp/random" { + version = "3.6.2" + constraints = ">= 2.1.0, >= 2.2.0, 3.6.2" + hashes = [ + "h1:VavG5unYCa3SYISMKF9pzc3718M0bhPlcbUZZGl7wuo=", + "zh:0ef01a4f81147b32c1bea3429974d4d104bbc4be2ba3cfa667031a8183ef88ec", + "zh:1bcd2d8161e89e39886119965ef0f37fcce2da9c1aca34263dd3002ba05fcb53", + "zh:37c75d15e9514556a5f4ed02e1548aaa95c0ecd6ff9af1119ac905144c70c114", + "zh:4210550a767226976bc7e57d988b9ce48f4411fa8a60cd74a6b246baf7589dad", + "zh:562007382520cd4baa7320f35e1370ffe84e46ed4e2071fdc7e4b1a9b1f8ae9b", + "zh:5efb9da90f665e43f22c2e13e0ce48e86cae2d960aaf1abf721b497f32025916", + "zh:6f71257a6b1218d02a573fc9bff0657410404fb2ef23bc66ae8cd968f98d5ff6", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:9647e18f221380a85f2f0ab387c68fdafd58af6193a932417299cdcae4710150", + "zh:bb6297ce412c3c2fa9fec726114e5e0508dd2638cad6a0cb433194930c97a544", + "zh:f83e925ed73ff8a5ef6e3608ad9225baa5376446349572c2449c0c0b3cf184b7", + "zh:fbef0781cb64de76b1df1ca11078aecba7800d82fd4a956302734999cfd9a4af", + ] +} + +provider "registry.terraform.io/hashicorp/time" { + version = "0.12.0" + constraints = ">= 0.5.0" + hashes = [ + "h1:Os2Ok7txtlUJHh6Hg7o+74Ql85SnRb/fGmah22yXpLw=", + "zh:019a4c09af254ef80b72cf0d843dfe72d99483e227138cf5b514a1b9977ab4c3", + "zh:0ae310ec740ebc6f275529507d60bb747d0bf39e72fc5a2fa90d74486006132c", + "zh:13d6aec117f05237fbf8c7d91d6ebb19797b00aa87e7a812642d3ea4738a394e", + "zh:2e87abbc261f9317d0c2ef26e01d5fabf77679da7d2cac6f47df7d198f720989", + "zh:4a6d471176ce0264455aa7d5457b8702f78400010c201c1719708958a1b7b647", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:8978d5474a6da30bc0ad21c17db188d6918cacf3df3f6506b72ef3a268d53e2e", + "zh:b109efe138dfcb45dc04a9cc6809d185ab8b0ebc12040847c2dac430fda5af68", + "zh:b58e039b9106ac0a8de3c07f53b5279d7f0215fb35f2d23df642dfce0875382f", + "zh:ba2cbb2e515922d13efe3a46647be84f5426fcfcaa0f1520b3efeab8db847ed3", + "zh:c6c1ef1f26f25bca3abb5e07fa33dca37ed39cc26d0ff877964f2ffe5edd618c", + "zh:f8e171f923b7d2e789abd034072465dec3e6133c3a7644b7a7a965a74d52224e", + ] +} From 5f0329617e6f0d3aee8a88bbd9dcd5c0dc397095 Mon Sep 17 00:00:00 2001 From: Paul Xu Date: Wed, 24 Jul 2024 19:10:54 -0400 Subject: [PATCH 10/15] remove ruby --- .kitchen.yml | 27 -------- .ruby-version | 1 - .terraform-version | 1 - Dockerfile | 28 --------- Gemfile | 3 - chefignore | 2 - .../controls/brown-modules-cluster.rb | 63 ------------------- .../brown-modules-cluster/inspec.yml | 7 --- .../simple-cluster/controls/simple-cluster.rb | 62 ------------------ test/integration/simple-cluster/inspec.yml | 7 --- 10 files changed, 201 deletions(-) delete mode 100644 .kitchen.yml delete mode 100644 .ruby-version delete mode 100644 .terraform-version delete mode 100644 Dockerfile delete mode 100644 Gemfile delete mode 100644 chefignore delete mode 100644 test/integration/brown-modules-cluster/controls/brown-modules-cluster.rb delete mode 100644 test/integration/brown-modules-cluster/inspec.yml delete mode 100644 test/integration/simple-cluster/controls/simple-cluster.rb delete mode 100644 test/integration/simple-cluster/inspec.yml diff --git a/.kitchen.yml b/.kitchen.yml deleted file mode 100644 index 4ce67f0..0000000 --- a/.kitchen.yml +++ /dev/null @@ -1,27 +0,0 @@ ---- - provisioner: - name: terraform - - verifier: - name: terraform - systems: - - name: inspec-gcp - backend: gcp - - platforms: - - name: terraform - - suites: - - name: simple-cluster - driver: - name: terraform - root_module_directory: examples/simple-cluster - command_timeout: 12000 - parallelism: 1 - - name: brown-modules-cluster - driver: - name: terraform - root_module_directory: examples/brown-modules-cluster - command_timeout: 12000 - parallelism: 1 - \ No newline at end of file diff --git a/.ruby-version b/.ruby-version deleted file mode 100644 index be94e6f..0000000 --- a/.ruby-version +++ /dev/null @@ -1 +0,0 @@ -3.2.2 diff --git a/.terraform-version b/.terraform-version deleted file mode 100644 index 3e1ad72..0000000 --- a/.terraform-version +++ /dev/null @@ -1 +0,0 @@ -1.5.0 \ No newline at end of file diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index dbb2090..0000000 --- a/Dockerfile +++ /dev/null @@ -1,28 +0,0 @@ -FROM hashicorp/terraform:1.5.0 - -COPY .ruby-version .ruby-version - -# Update and install all of the required packages. -# At the end, remove the apk cache -RUN apk upgrade && \ - apk add --update \ - bash \ - curl-dev \ - curl \ - "ruby-dev=~$(cat .ruby-version)" \ - "ruby-full=~$(cat .ruby-version)" \ - build-base \ - python3 && \ - rm -rf /var/cache/apk/* - -RUN mkdir /usr/app -WORKDIR /usr/app - -COPY Gemfile* ./ -RUN gem install bundler && \ - bundle config set system 'true' && \ - bundle install - - -ENTRYPOINT ["/bin/bash"] - diff --git a/Gemfile b/Gemfile deleted file mode 100644 index b0cd946..0000000 --- a/Gemfile +++ /dev/null @@ -1,3 +0,0 @@ -source "https://rubygems.org/" do - gem "kitchen-terraform", "~> 7.0.0" - end \ No newline at end of file diff --git a/chefignore b/chefignore deleted file mode 100644 index 77924ab..0000000 --- a/chefignore +++ /dev/null @@ -1,2 +0,0 @@ -.kitchen -kitchen*.yml \ No newline at end of file diff --git a/test/integration/brown-modules-cluster/controls/brown-modules-cluster.rb b/test/integration/brown-modules-cluster/controls/brown-modules-cluster.rb deleted file mode 100644 index d3d6f4e..0000000 --- a/test/integration/brown-modules-cluster/controls/brown-modules-cluster.rb +++ /dev/null @@ -1,63 +0,0 @@ -# copyright: 2018, The Authors - -title "Cluster Section" - -project_id = attribute('project_id') -project_name= attribute('project_name') -location = attribute('location') -service_account = attribute('service_account') -cluster_name = attribute('cluster_name') -network_name = attribute('network_name') -subnet_name = attribute('subnet_name') - -# Project Tests -describe google_project(project: project_id) do - it { should exist } - its('project_id') { should eq project_id } -end - -describe google_project(project: project_id) do - its('lifecycle_state') { should eq "ACTIVE" } -end - -describe google_project(project: project_id) do - its('name') { should eq project_name } -end - -# K8s Cluster Test -describe google_container_cluster(project: project_id, location: location, name: cluster_name) do - # It's up and running - it { should exist } - its('status') { should eq 'RUNNING' } - its('locations.sort'){ should cmp ["us-east1-b"] } - - - # Has correct network and subnet names - its('network'){should eq network_name} - its('subnetwork'){should eq subnet_name} - - # Has correct node pool configuration - its('node_config.disk_size_gb'){should eq 100} - its('node_config.image_type'){should be_in ["COS", "COS_CONTAINERD"]} - its('node_config.machine_type'){should be_in ["n1-standard-1", "e2-medium"]} - its('node_pools.count'){should eq 3} - its('node_config.oauth_scopes'){should eq ["https://www.googleapis.com/auth/userinfo.email", "https://www.googleapis.com/auth/cloud-platform"]} - its('node_config.service_account'){should eq service_account} -end - -# Node Pool Tests -describe google_container_node_pools(project: project_id, location: location, cluster_name: cluster_name) do - its('node_pool_names') { should include "default-pool" } - its('node_pool_names') { should include "user-pool" } - its('node_pool_names') { should include "core-pool" } - -end - -google_container_node_pools(project: project_id, location: location, cluster_name: cluster_name).where(node_pool_name: /-pool$/).node_pool_names.each do |node_pool_name| - describe google_container_node_pool(project: project_id, location: location, cluster_name: cluster_name, nodepool_name: node_pool_name) do - it { should exist } - its('status') { should eq 'RUNNING' } - end -end - - diff --git a/test/integration/brown-modules-cluster/inspec.yml b/test/integration/brown-modules-cluster/inspec.yml deleted file mode 100644 index 800e0cf..0000000 --- a/test/integration/brown-modules-cluster/inspec.yml +++ /dev/null @@ -1,7 +0,0 @@ -name: brown-modules-cluster -depends: -- name: inspec-gcp - git: https://github.com/inspec/inspec-gcp.git - tag: v1.11.1 -supports: -- platform: gcp diff --git a/test/integration/simple-cluster/controls/simple-cluster.rb b/test/integration/simple-cluster/controls/simple-cluster.rb deleted file mode 100644 index f9a871f..0000000 --- a/test/integration/simple-cluster/controls/simple-cluster.rb +++ /dev/null @@ -1,62 +0,0 @@ -# copyright: 2018, The Authors - -title "Cluster Section" - -project_id = attribute('project_id') -project_name= attribute('project_name') -location = attribute('location') -service_account = attribute('service_account') -network_name = attribute('network_prefix') + '-' + attribute('random_string') -cluster_name = attribute('cluster_name') - - -# Project Tests -describe google_project(project: project_id) do - it { should exist } - its('project_id') { should eq project_id } -end - -describe google_project(project: project_id) do - its('lifecycle_state') { should eq "ACTIVE" } -end - -describe google_project(project: project_id) do - its('name') { should eq project_name } -end - -# K8s Cluster Test -describe google_container_cluster(project: project_id, location: location, name: cluster_name) do - # It's up and running - it { should exist } - its('status') { should eq 'RUNNING' } - its('locations.sort'){ should cmp ["us-east1-b"] } - - # Has correct network and subnet names - its('network'){should eq network_name} - its('subnetwork'){should eq network_name} - - # Has correct node pool configuration - its('node_config.disk_size_gb'){should eq 100} - its('node_config.image_type'){should be_in ["COS", "COS_CONTAINERD"]} - its('node_config.machine_type'){should be_in ["n1-standard-1", "e2-medium"]} - its('node_pools.count'){should eq 3} - its('node_config.oauth_scopes'){should eq ["https://www.googleapis.com/auth/userinfo.email", "https://www.googleapis.com/auth/cloud-platform"]} - its('node_config.service_account'){should eq service_account} -end - -# Node Pool Tests -describe google_container_node_pools(project: project_id, location: location, cluster_name: cluster_name) do - its('node_pool_names') { should include "default-pool" } - its('node_pool_names') { should include "user-pool" } - its('node_pool_names') { should include "core-pool" } - -end - -google_container_node_pools(project: project_id, location: location, cluster_name: cluster_name).where(node_pool_name: /-pool$/).node_pool_names.each do |node_pool_name| - describe google_container_node_pool(project: project_id, location: location, cluster_name: cluster_name, nodepool_name: node_pool_name) do - it { should exist } - its('status') { should eq 'RUNNING' } - end -end - - diff --git a/test/integration/simple-cluster/inspec.yml b/test/integration/simple-cluster/inspec.yml deleted file mode 100644 index e6b753d..0000000 --- a/test/integration/simple-cluster/inspec.yml +++ /dev/null @@ -1,7 +0,0 @@ -name: simple-cluster -depends: -- name: inspec-gcp - git: https://github.com/inspec/inspec-gcp.git - tag: v1.11.1 -supports: -- platform: gcp From 919a0e49f35b6bab826fcc8650de8b61d7cd3198 Mon Sep 17 00:00:00 2001 From: Paul Xu Date: Wed, 24 Jul 2024 19:11:10 -0400 Subject: [PATCH 11/15] update github actions --- .github/workflows/kitchen-tests.yml | 127 -------------------------- .github/workflows/pr-labeler.yml | 2 +- .github/workflows/release-drafter.yml | 2 +- .github/workflows/terraform-tests.yml | 43 +++++++++ 4 files changed, 45 insertions(+), 129 deletions(-) delete mode 100644 .github/workflows/kitchen-tests.yml create mode 100644 .github/workflows/terraform-tests.yml diff --git a/.github/workflows/kitchen-tests.yml b/.github/workflows/kitchen-tests.yml deleted file mode 100644 index 9cdec5c..0000000 --- a/.github/workflows/kitchen-tests.yml +++ /dev/null @@ -1,127 +0,0 @@ -name: kitchen-tests - -on: - push: - branches: - - "main" - tags: - - "v*.*.*" - pull_request: - branches: - - "main" - -env: - REGISTRY: ghcr.io - IMAGE_NAME: brownuniversity/terraform-gcp-cluster - -jobs: - docker: - runs-on: ubuntu-latest - - permissions: - packages: write - contents: read - - outputs: - full_image_id: ${{ steps.save_full_image_id.outputs.full_image_id }} - - steps: - - uses: actions/checkout@v3 - - - uses: dorny/paths-filter@v2.2.0 - id: filter - with: - base: ${{ github.ref }} - filters: | - all: - - '.github/workflows/kitchen-tests.yml' - - 'Dockerfile' - - 'Gemfile*' - - - name: Set up QEMU - uses: docker/setup-qemu-action@v2 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - - - name: Log in to the Container registry - uses: docker/login-action@v2 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract metadata (tags, labels) for Docker - id: meta - uses: docker/metadata-action@v4 - with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - tags: | - type=ref,event=branch - type=ref,event=pr - type=semver,pattern={{major}}.{{minor}} - - # NOTE: We are tapping into json output because tags could become a list if prior step is modified - - id: save_full_image_id - run: echo "full_image_id=${{ fromJSON(steps.meta.outputs.json).tags[0] }}" >> $GITHUB_OUTPUT - - - name: print_tag - run: echo "${{ fromJSON(steps.meta.outputs.json).tags[0] }}" - - - name: Build and push Docker image - uses: docker/build-push-action@v4 - with: - context: . - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache - cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache - - simple-cluster: - needs: [docker] - if: "!contains(github.event.commits[0].message, '[skip ci]')" - runs-on: ubuntu-latest - container: - image: "${{ needs.docker.outputs.full_image_id }}" - steps: - - uses: actions/checkout@v3 - - name: Create Credential File - run: | - echo "$GCP_CREDENTIAL_JSON" | base64 -d > /tmp/credentials.json - env: - GCP_CREDENTIAL_JSON: ${{ secrets.GCP_CI_CREDENTIAL_JSON}} - - name: Run Kitchen - run: kitchen test simple-cluster - env: - TF_VAR_billing_account: ${{ secrets.GCP_BURWOOD_BILLING_ACCOUNT }} - TF_VAR_org_id: ${{ secrets.GCP_ORG_ID }} - TF_VAR_folder_id: ${{ secrets.GCP_CCV_CI_FOLDER_ID }} - TF_VAR_network_name: "network-01" - TF_VAR_subnet_name: "subnet-01" - TF_VAR_routing_mode: "REGIONAL" - GOOGLE_APPLICATION_CREDENTIALS: /tmp/credentials.json - - brown-modules: - needs: [docker] - if: "!contains(github.event.commits[0].message, '[skip ci]')" - runs-on: ubuntu-latest - container: - image: "${{ needs.docker.outputs.full_image_id }}" - steps: - - uses: actions/checkout@v3 - - name: Create Credential File - run: | - echo "$GCP_CREDENTIAL_JSON" | base64 -d > /tmp/credentials.json - env: - GCP_CREDENTIAL_JSON: ${{ secrets.GCP_CI_CREDENTIAL_JSON}} - - name: Run Kitchen - run: kitchen test brown-modules-cluster - env: - TF_VAR_billing_account: ${{ secrets.GCP_BURWOOD_BILLING_ACCOUNT }} - TF_VAR_org_id: ${{ secrets.GCP_ORG_ID }} - TF_VAR_folder_id: ${{ secrets.GCP_CCV_CI_FOLDER_ID }} - TF_VAR_network_name: "network-01" - TF_VAR_subnet_name: "subnet-01" - TF_VAR_routing_mode: "REGIONAL" - GOOGLE_APPLICATION_CREDENTIALS: /tmp/credentials.json diff --git a/.github/workflows/pr-labeler.yml b/.github/workflows/pr-labeler.yml index 9c6c65f..5afdeff 100644 --- a/.github/workflows/pr-labeler.yml +++ b/.github/workflows/pr-labeler.yml @@ -7,7 +7,7 @@ jobs: pr-labeler: runs-on: ubuntu-latest steps: - - uses: TimonVS/pr-labeler-action@v3 + - uses: TimonVS/pr-labeler-action@v5 with: configuration-path: .github/pr-labeler.yml # optional, .github/pr-labeler.yml is the default value env: diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml index e43769d..97f1b03 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/release-drafter.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: # Drafts your next Release notes as Pull Requests are merged into "master" - - uses: release-drafter/release-drafter@v5 + - uses: release-drafter/release-drafter@v6 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/terraform-tests.yml b/.github/workflows/terraform-tests.yml new file mode 100644 index 0000000..69c2a5a --- /dev/null +++ b/.github/workflows/terraform-tests.yml @@ -0,0 +1,43 @@ +name: kitchen-tests + +on: + push: + branches: + - "main" + tags: + - "v*.*.*" + pull_request: + branches: + - "main" + +env: + REGISTRY: ghcr.io + IMAGE_NAME: brownuniversity/terraform-gcp-cluster + +jobs: + terraform_tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Create Credential File + run: | + echo "$GCP_CREDENTIAL_JSON" | base64 -d > /tmp/credentials.json + env: + GCP_CREDENTIAL_JSON: ${{ secrets.GCP_CI_CREDENTIAL_JSON}} + - name: Setup Terraform + uses: hashicorp/setup-terraform@v3 + with: + terraform_version: 1.9.2 + - name: Run terraform test + run: | + terraform init + terraform test -filter=tests/test_simple_cluster.tftest.hcl + terraform test -filter=tests/test_brown_modules.tftest.hcl + env: + TF_VAR_billing_account: ${{ secrets.GCP_BURWOOD_BILLING_ACCOUNT }} + TF_VAR_org_id: ${{ secrets.GCP_ORG_ID }} + TF_VAR_folder_id: ${{ secrets.GCP_CCV_CI_FOLDER_ID }} + TF_VAR_network_name: "network-01" + TF_VAR_subnet_name: "subnet-01" + TF_VAR_routing_mode: "REGIONAL" + GOOGLE_APPLICATION_CREDENTIALS: /tmp/credentials.json From 860ad89542e52630c8e7ee121a1d4a3e746bec2e Mon Sep 17 00:00:00 2001 From: Paul Xu Date: Wed, 24 Jul 2024 19:14:05 -0400 Subject: [PATCH 12/15] fix error in github actions --- .github/workflows/terraform-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/terraform-tests.yml b/.github/workflows/terraform-tests.yml index 69c2a5a..451264b 100644 --- a/.github/workflows/terraform-tests.yml +++ b/.github/workflows/terraform-tests.yml @@ -32,7 +32,7 @@ jobs: run: | terraform init terraform test -filter=tests/test_simple_cluster.tftest.hcl - terraform test -filter=tests/test_brown_modules.tftest.hcl + terraform test -filter=tests/test_brown_modules_cluster.tftest.hcl env: TF_VAR_billing_account: ${{ secrets.GCP_BURWOOD_BILLING_ACCOUNT }} TF_VAR_org_id: ${{ secrets.GCP_ORG_ID }} From f37e41a8b4df570cf7919b09164cb18733b14855 Mon Sep 17 00:00:00 2001 From: Paul Xu Date: Wed, 24 Jul 2024 19:14:43 -0400 Subject: [PATCH 13/15] fix error in github actions --- .github/workflows/terraform-tests.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/terraform-tests.yml b/.github/workflows/terraform-tests.yml index 451264b..920c3d5 100644 --- a/.github/workflows/terraform-tests.yml +++ b/.github/workflows/terraform-tests.yml @@ -10,10 +10,6 @@ on: branches: - "main" -env: - REGISTRY: ghcr.io - IMAGE_NAME: brownuniversity/terraform-gcp-cluster - jobs: terraform_tests: runs-on: ubuntu-latest From 777d727919817fb3fcec5f46aa37948f8b1b6e55 Mon Sep 17 00:00:00 2001 From: Paul Xu Date: Wed, 24 Jul 2024 21:32:05 -0400 Subject: [PATCH 14/15] update tests --- tests/test_brown_modules_cluster.tftest.hcl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_brown_modules_cluster.tftest.hcl b/tests/test_brown_modules_cluster.tftest.hcl index d180ab7..32ea131 100644 --- a/tests/test_brown_modules_cluster.tftest.hcl +++ b/tests/test_brown_modules_cluster.tftest.hcl @@ -1,4 +1,4 @@ -run "create_gke_cluster" { +run "create_brown_modules_cluster" { module { source = "./tests/brown-modules-cluster" From fd1b11617536b587ae500ac5bc66366970b5f767 Mon Sep 17 00:00:00 2001 From: Paul Xu Date: Wed, 24 Jul 2024 23:19:46 -0400 Subject: [PATCH 15/15] update README --- .github/workflows/terraform-tests.yml | 2 +- Brewfile | 2 +- README.md | 45 +++++++-------------------- 3 files changed, 13 insertions(+), 36 deletions(-) diff --git a/.github/workflows/terraform-tests.yml b/.github/workflows/terraform-tests.yml index 920c3d5..533071d 100644 --- a/.github/workflows/terraform-tests.yml +++ b/.github/workflows/terraform-tests.yml @@ -11,7 +11,7 @@ on: - "main" jobs: - terraform_tests: + terraform-tests: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 diff --git a/Brewfile b/Brewfile index 29e9cf3..196f86c 100644 --- a/Brewfile +++ b/Brewfile @@ -1,6 +1,6 @@ brew "pre-commit" brew "terraform-docs" brew "tflint" -brew "trivy" +brew "tfsec" cask "google-cloud-sdk" \ No newline at end of file diff --git a/README.md b/README.md index dfae664..a4bb816 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Core/User Cluster -![kitchen-tests](https://github.com/BrownUniversity/terraform-gcp-cluster/workflows/kitchen-tests/badge.svg) +![terraform-tests](https://github.com/BrownUniversity/terraform-gcp-cluster/workflows/terraform-tests/badge.svg) This folder contains a [Terraform](https://www.terraform.io/) module to deploy a @@ -28,7 +28,7 @@ GOOGLE_APPLICATION_CREDENTIALS=/path/to/file.json ## How to use this module This repository defines a [Terraform module](https://www.terraform.io/docs/modules/usage.html), which you can use in your -code by adding a `module` configuration and setting its `source` parameter to URL of this repository. See the [examples](/examples) folder for guidance +code by adding a `module` configuration and setting its `source` parameter to URL of this repository. See the [tests](/tests) folder for guidance ## Requirements @@ -128,22 +128,19 @@ Use [GitLab Flow](https://docs.gitlab.com/ee/topics/gitlab_flow.html#production- ### Version managers -We recommend using [asdf](https://asdf-vm.com) to manage your versions of Terrafom and Ruby. +We recommend using [asdf](https://asdf-vm.com) to manage your versions of Terrafom. ``` brew install asdf ``` -Alternatively you can use [tfenv](https://github.com/tfutils/tfenv) and [rbenv](https://github.com/rbenv/rbenv) +### Terraform -### Terraform and Ruby - -The tests can simply run in CI. If you want to run the tests locally, you will need to install the version of terraform and Ruby specified in the `.tool-versions` file (or `.terraform-version`, `.ruby-version`). +You can also install the latest version of terraform version via brew. ``` -asdf plugin-add terraform https://github.com/asdf-community/asdf-hashicorp.git -asdf plugin add ruby https://github.com/asdf-vm/asdf-ruby.git -asdf install +brew tap hashicorp/tap +brew install hashicorp/tap/terraform ``` #### Pre-commit hooks @@ -188,34 +185,14 @@ to set and uset the `GOOGLE_APPLICATION_CREDENTIALS` variable. ### Testing -This repository uses Kitchen-Terraform to test the terraform modules. In the [examples](/examples) directory you can find examples of how each module can be used. Those examples are fed to [Test Kitchen](https://kitchen.ci/). To install test kitchen, first make sure you have Ruby and bundler installed. - -``` -gem install bundler -``` - -Then install the prerequisites for test kitchen. - -``` -bundle install -``` - -You'll need to add some common credentials and secret variables - -And now you're ready to run test kitchen. Test kitchen has a couple main commands: - -- `bundle exec kitchen create` initializes terraform. -- `bundle exec kitchen converge` runs our terraform examples. -- `bundle exec kitchen verify` runs our inspec scripts against a converged kitchen. -- `bundle exec kitchen destroy` destroys infrastructure. -- `bundle exec kitchen test` does all the above. +The tests can be run locally with `terraform test` after running `terraform init`. You will need to supply `org_id`, `folder_id`, and `billing_account` variables through `terraform.tfvars` file. Please see `terraform.tfvars.example` file for an example. ### CI This project has three workflows enabled: -1. PR labeler: When openning a PR to defaukt branch, a label is given assigned automatically accourding to the name of your feature branch. The labeler follows the follows rules in [pr-labeler.yml](.github/pr-labeler.yml) +1. PR labeler: When opening a PR to default branch, a label is given assigned automatically according to the name of your feature branch. The labeler follows the follows rules in [pr-labeler.yml](.github/pr-labeler.yml) -2. Realease Drafter: When merging to main, a release is drafted using the [Release-Drafter Action](https://github.com/marketplace/actions/release-drafter) +2. Release Drafter: When merging to master, a release is drafted using the [Release-Drafter Action](https://github.com/marketplace/actions/release-drafter) -3. `Kitchen test` runs on PR, merge to main and releases. \ No newline at end of file +3. `terraform test` runs on PR, merge to main and releases. \ No newline at end of file