diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index ea4642684..8cf48542c 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -7,8 +7,8 @@ The basic process is pretty simple:
* Fork the Project
* Create your Feature Branch
`git checkout -b feature/AmazingFeature`
* Commit your Changes
`git commit -m 'Add some AmazingFeature`
-* Make sure Terraform linting is ok (hint: `terraform format`)
* Make sure tests pass!
`pytest # in the root folder`
+* Make sure Terraform linting is ok (hint: `terraform fmt -recursive` in the root folder)
* Make sure any changes to variables and outputs are reflected in READMEs
`./tools/tfdoc.py [changed folder]`
* Push to the Branch
`git push origin feature/AmazingFeature`
* Open a Pull Request
diff --git a/cloud-operations/scheduled-asset-inventory-export-bq/README.md b/cloud-operations/scheduled-asset-inventory-export-bq/README.md
index 1abecdd94..ef8bca889 100644
--- a/cloud-operations/scheduled-asset-inventory-export-bq/README.md
+++ b/cloud-operations/scheduled-asset-inventory-export-bq/README.md
@@ -43,7 +43,7 @@ You can also create a dashboard connecting [Datalab](https://datastudio.google.c
| name | description | type | required | default |
|---|---|:---: |:---:|:---:|
-| cai_config | Cloud Asset inventory export config. | object({...}) | ✓ | |
+| cai_config | Cloud Asset inventory export config. | object({...}) | ✓ | |
| project_id | Project id that references existing project. | string | ✓ | |
| *billing_account* | Billing account id used as default for new projects. | string | | null |
| *bundle_path* | Path used to write the intermediate Cloud Function code bundle. | string | | ./bundle.zip |
diff --git a/cloud-operations/scheduled-asset-inventory-export-bq/cf/main.py b/cloud-operations/scheduled-asset-inventory-export-bq/cf/main.py
index 77768190c..ad97c3262 100755
--- a/cloud-operations/scheduled-asset-inventory-export-bq/cf/main.py
+++ b/cloud-operations/scheduled-asset-inventory-export-bq/cf/main.py
@@ -50,17 +50,18 @@ def _configure_logging(verbose=True):
@click.option('--bq-project', required=True, help='Bigquery project to use.')
@click.option('--bq-dataset', required=True, help='Bigquery dataset to use.')
@click.option('--bq-table', required=True, help='Bigquery table name to use.')
+@click.option('--target-node', required=True, help='Node in Google Cloud resource hierarchy.')
@click.option('--read-time', required=False, help=(
'Day to take an asset snapshot in \'YYYYMMDD\' format, uses current day '
' as default. Export will run at midnight of the specified day.'))
@click.option('--verbose', is_flag=True, help='Verbose output')
-def main_cli(project=None, bq_project=None, bq_dataset=None, bq_table=None,
+def main_cli(project=None, bq_project=None, bq_dataset=None, bq_table=None, target_node=None,
read_time=None, verbose=False):
'''Trigger Cloud Asset inventory export to Bigquery. Data will be stored in
the dataset specified on a dated table with the name specified.
'''
try:
- _main(project, bq_project, bq_dataset, bq_table, read_time, verbose)
+ _main(project, bq_project, bq_dataset, bq_table, target_node, read_time, verbose)
except RuntimeError:
logging.exception('exception raised')
@@ -78,25 +79,25 @@ def main(event, context):
logging.exception('exception in cloud function entry point')
-def _main(project=None, bq_project=None, bq_dataset=None, bq_table=None, read_time=None, verbose=False):
+def _main(project=None, bq_project=None, bq_dataset=None, bq_table=None, target_node=None, read_time=None, verbose=False):
'Module entry point used by cli and cloud function wrappers.'
_configure_logging(verbose)
if not read_time:
read_time = datetime.datetime.now()
client = asset_v1.AssetServiceClient()
- parent = 'projects/%s' % project
content_type = asset_v1.ContentType.RESOURCE
output_config = asset_v1.OutputConfig()
output_config.bigquery_destination.dataset = 'projects/%s/datasets/%s' % (
bq_project, bq_dataset)
output_config.bigquery_destination.table = '%s_%s' % (
bq_table, read_time.strftime('%Y%m%d'))
+ output_config.bigquery_destination.separate_tables_per_asset_type = True
output_config.bigquery_destination.force = True
try:
response = client.export_assets(
request={
- 'parent': parent,
+ 'parent': target_node,
'read_time': read_time,
'content_type': content_type,
'output_config': output_config
@@ -105,7 +106,7 @@ def _main(project=None, bq_project=None, bq_dataset=None, bq_table=None, read_ti
except (GoogleAPIError, googleapiclient.errors.HttpError) as e:
logging.debug('API Error: %s', e, exc_info=True)
raise RuntimeError(
- 'Error fetching Asset Inventory entries (project: %s)' % parent, e)
+ 'Error fetching Asset Inventory entries (resource manager node: %s)' % target_node, e)
if __name__ == '__main__':
diff --git a/cloud-operations/scheduled-asset-inventory-export-bq/main.tf b/cloud-operations/scheduled-asset-inventory-export-bq/main.tf
index 1b5306c48..0052401d9 100644
--- a/cloud-operations/scheduled-asset-inventory-export-bq/main.tf
+++ b/cloud-operations/scheduled-asset-inventory-export-bq/main.tf
@@ -73,6 +73,7 @@ module "pubsub" {
module "cf" {
source = "../../modules/cloud-function"
project_id = module.project.project_id
+ region = var.region
name = var.name
bucket_name = "${var.name}-${random_pet.random.id}"
bucket_config = {
@@ -108,8 +109,8 @@ resource "google_app_engine_application" "app" {
resource "google_cloud_scheduler_job" "job" {
project = google_app_engine_application.app.project
region = var.region
- name = "test-job"
- description = "test http job"
+ name = "cai-export-job"
+ description = "CAI Export Job"
schedule = "* 9 * * 1"
time_zone = "Etc/UTC"
@@ -117,10 +118,11 @@ resource "google_cloud_scheduler_job" "job" {
attributes = {}
topic_name = module.pubsub.topic.id
data = base64encode(jsonencode({
- project = module.project.project_id
- bq_project = module.project.project_id
- bq_dataset = var.cai_config.bq_dataset
- bq_table = var.cai_config.bq_table
+ project = module.project.project_id
+ bq_project = module.project.project_id
+ bq_dataset = var.cai_config.bq_dataset
+ bq_table = var.cai_config.bq_table
+ target_node = var.cai_config.target_node
}))
}
}
@@ -133,6 +135,7 @@ module "bq" {
source = "../../modules/bigquery-dataset"
project_id = module.project.project_id
id = var.cai_config.bq_dataset
+ location = var.region
access = {
owner = { role = "OWNER", type = "user" }
}
diff --git a/cloud-operations/scheduled-asset-inventory-export-bq/variables.tf b/cloud-operations/scheduled-asset-inventory-export-bq/variables.tf
index 6f8217d33..5bb62166c 100644
--- a/cloud-operations/scheduled-asset-inventory-export-bq/variables.tf
+++ b/cloud-operations/scheduled-asset-inventory-export-bq/variables.tf
@@ -29,8 +29,9 @@ variable "bundle_path" {
variable "cai_config" {
description = "Cloud Asset inventory export config."
type = object({
- bq_dataset = string
- bq_table = string
+ bq_dataset = string
+ bq_table = string
+ target_node = string
})
}
diff --git a/modules/compute-mig/README.md b/modules/compute-mig/README.md
index 4293604e3..f1651bc08 100644
--- a/modules/compute-mig/README.md
+++ b/modules/compute-mig/README.md
@@ -1,8 +1,10 @@
# GCE Managed Instance Group module
-This module allows creating a managed instance group supporting one or more application versions via instance templates. A health check and an autoscaler can also be optionally created.
+This module allows creating a managed instance group supporting one or more application versions via instance templates. Optionally, a health check and an autoscaler can be created, and the managed instance group can be configured to be stateful.
-This module can be coupled with the [`compute-vm`](../compute-vm) module which can manage instance templates, and the [`net-ilb`](../net-ilb) module to assign the MIG to a backend wired to an Internal Load Balancer. The first use case is shown in the examples below.
+This module can be coupled with the [`compute-vm`](../compute-vm) module which can manage instance templates, and the [`net-ilb`](../net-ilb) module to assign the MIG to a backend wired to an Internal Load Balancer. The first use case is shown in the examples below.
+
+Stateful disks can be created directly, as shown in the last example below.
## Examples
@@ -266,6 +268,182 @@ module "nginx-mig" {
# tftest:modules=2:resources=2
```
+### Stateful MIGs - MIG Config
+
+Stateful MIGs have some limitations documented [here](https://cloud.google.com/compute/docs/instance-groups/configuring-stateful-migs#limitations). Enforcement of these requirements is the responsibility of users of this module.
+
+You can configure a disk defined in the instance template to be stateful for all instances in the MIG by configuring in the MIG's stateful policy, using the `stateful_disk_mig` variable. Alternatively, you can also configure stateful persistent disks individually per instance of the MIG by setting the `stateful_disk_instance` variable. A discussion on these scenarios can be found in the [docs](https://cloud.google.com/compute/docs/instance-groups/configuring-stateful-disks-in-migs).
+
+An example using only the configuration at the MIG level can be seen below.
+
+Note that when referencing the stateful disk, you use `device_name` and not `disk_name`.
+
+
+```hcl
+module "cos-nginx" {
+ source = "./modules/cloud-config-container/nginx"
+}
+
+module "nginx-template" {
+ source = "./modules/compute-vm"
+ project_id = var.project_id
+ name = "nginx-template"
+ zone = "europe-west1-b"
+ tags = ["http-server", "ssh"]
+ network_interfaces = [{
+ network = var.vpc.self_link
+ subnetwork = var.subnet.self_link
+ nat = false
+ addresses = null
+ }]
+ boot_disk = {
+ image = "projects/cos-cloud/global/images/family/cos-stable"
+ type = "pd-ssd"
+ size = 10
+ }
+ attached_disks = [{
+ name = "repd-1"
+ size = null
+ source_type = "attach"
+ source = "regions/${var.region}/disks/repd-test-1"
+ options = {
+ mode = "READ_ONLY"
+ replica_zone = "${var.region}-c"
+ type = "PERSISTENT"
+ }
+ }]
+ create_template = true
+ metadata = {
+ user-data = module.cos-nginx.cloud_config
+ }
+}
+
+module "nginx-mig" {
+ source = "./modules/compute-mig"
+ project_id = "my-project"
+ location = "europe-west1-b"
+ name = "mig-test"
+ target_size = 3
+ default_version = {
+ instance_template = module.nginx-template.template.self_link
+ name = "default"
+ }
+ autoscaler_config = {
+ max_replicas = 3
+ min_replicas = 1
+ cooldown_period = 30
+ cpu_utilization_target = 0.65
+ load_balancing_utilization_target = null
+ metric = null
+ }
+ stateful_config = {
+ per_instance_config = {},
+ mig_config = {
+ stateful_disks = {
+ persistent-disk-1 = {
+ delete_rule = "NEVER"
+ }
+ }
+ }
+ }
+}
+# tftest:modules=2:resources=3
+
+```
+
+### Stateful MIGs - Instance Config
+Here is an example defining the stateful config at the instance level.
+
+Note that you will need to know the instance name in order to use this configuration.
+
+```hcl
+module "cos-nginx" {
+ source = "./modules/cloud-config-container/nginx"
+}
+
+module "nginx-template" {
+ source = "./modules/compute-vm"
+ project_id = var.project_id
+ name = "nginx-template"
+ zone = "europe-west1-b"
+ tags = ["http-server", "ssh"]
+ network_interfaces = [{
+ network = var.vpc.self_link
+ subnetwork = var.subnet.self_link
+ nat = false
+ addresses = null
+ }]
+ boot_disk = {
+ image = "projects/cos-cloud/global/images/family/cos-stable"
+ type = "pd-ssd"
+ size = 10
+ }
+ attached_disks = [{
+ name = "repd-1"
+ size = null
+ source_type = "attach"
+ source = "regions/${var.region}/disks/repd-test-1"
+ options = {
+ mode = "READ_ONLY"
+ replica_zone = "${var.region}-c"
+ type = "PERSISTENT"
+ }
+ }]
+ create_template = true
+ metadata = {
+ user-data = module.cos-nginx.cloud_config
+ }
+}
+
+module "nginx-mig" {
+ source = "./modules/compute-mig"
+ project_id = "my-project"
+ location = "europe-west1-b"
+ name = "mig-test"
+ target_size = 3
+ default_version = {
+ instance_template = module.nginx-template.template.self_link
+ name = "default"
+ }
+ autoscaler_config = {
+ max_replicas = 3
+ min_replicas = 1
+ cooldown_period = 30
+ cpu_utilization_target = 0.65
+ load_balancing_utilization_target = null
+ metric = null
+ }
+ stateful_config = {
+ per_instance_config = {
+ # note that this needs to be the name of an existing instance within the Managed Instance Group
+ instance-1 = {
+ stateful_disks = {
+ persistent-disk-1 = {
+ source = "test-disk",
+ mode = "READ_ONLY",
+ delete_rule= "NEVER",
+ },
+ },
+ metadata = {
+ foo = "bar"
+ },
+ update_config = {
+ minimal_action = "NONE",
+ most_disruptive_allowed_action = "REPLACE",
+ remove_instance_state_on_destroy = false,
+ },
+ },
+ },
+ mig_config = {
+ stateful_disks = {
+ }
+ }
+ }
+}
+# tftest:modules=2:resources=4
+
+```
+
## Variables
@@ -280,6 +458,7 @@ module "nginx-mig" {
| *health_check_config* | Optional auto-created health check configuration, use the output self-link to set it in the auto healing policy. Refer to examples for usage. | object({...}) | | null |
| *named_ports* | Named ports. | map(number) | | null |
| *regional* | Use regional instance group. When set, `location` should be set to the region. | bool | | false |
+| *stateful_config* | Stateful configuration can be done by individual instances or for all instances in the MIG. They key in per_instance_config is the name of the specific instance. The key of the stateful_disks is the 'device_name' field of the resource. Please note that device_name is defined at the OS mount level, unlike the disk name. | object({...}) | | null |
| *target_pools* | Optional list of URLs for target pools to which new instances in the group are added. | list(string) | | [] |
| *target_size* | Group target size, leave null when using an autoscaler. | number | | null |
| *update_policy* | Update policy. Type can be 'OPPORTUNISTIC' or 'PROACTIVE', action 'REPLACE' or 'restart', surge type 'fixed' or 'percent'. | object({...}) | | null |
@@ -297,4 +476,4 @@ module "nginx-mig" {
## TODO
-- [ ] add support for instance groups
+- [✓] add support for instance groups
diff --git a/modules/compute-mig/main.tf b/modules/compute-mig/main.tf
index 75ab2d3dc..5b3a92425 100644
--- a/modules/compute-mig/main.tf
+++ b/modules/compute-mig/main.tf
@@ -84,6 +84,14 @@ resource "google_compute_instance_group_manager" "default" {
initial_delay_sec = config.value.initial_delay_sec
}
}
+ dynamic "stateful_disk" {
+ for_each = try(var.stateful_config.mig_config.stateful_disks, {})
+ iterator = config
+ content {
+ device_name = config.key
+ delete_rule = config.value.delete_rule
+ }
+ }
dynamic "update_policy" {
for_each = var.update_policy == null ? [] : [var.update_policy]
iterator = config
@@ -135,6 +143,43 @@ resource "google_compute_instance_group_manager" "default" {
}
}
+locals {
+ instance_group_manager = (
+ var.regional ?
+ google_compute_region_instance_group_manager.default :
+ google_compute_instance_group_manager.default
+ )
+}
+
+resource "google_compute_per_instance_config" "default" {
+ for_each = try(var.stateful_config.per_instance_config, {})
+ #for_each = var.stateful_config && var.stateful_config.per_instance_config == null ? {} : length(var.stateful_config.per_instance_config)
+ zone = var.location
+ # terraform error, solved with locals
+ #instance_group_manager = var.regional ? google_compute_region_instance_group_manager.default : google_compute_instance_group_manager.default
+ instance_group_manager = local.instance_group_manager[0].id
+ name = each.key
+ project = var.project_id
+ minimal_action = try(each.value.update_config.minimal_action, null)
+ most_disruptive_allowed_action = try(each.value.update_config.most_disruptive_allowed_action, null)
+ remove_instance_state_on_destroy = try(each.value.update_config.remove_instance_state_on_destroy, null)
+ preserved_state {
+
+ metadata = each.value.metadata
+
+ dynamic "disk" {
+ for_each = try(each.value.stateful_disks, {})
+ #for_each = var.stateful_config.mig_config.stateful_disks == null ? {} : var.stateful_config.mig_config.stateful_disks
+ iterator = config
+ content {
+ device_name = config.key
+ source = config.value.source
+ mode = config.value.mode
+ delete_rule = config.value.delete_rule
+ }
+ }
+ }
+}
resource "google_compute_region_autoscaler" "default" {
provider = google-beta
@@ -206,6 +251,15 @@ resource "google_compute_region_instance_group_manager" "default" {
initial_delay_sec = config.value.initial_delay_sec
}
}
+ dynamic "stateful_disk" {
+ for_each = try(var.stateful_config.mig_config.stateful_disks, {})
+ iterator = config
+ content {
+ device_name = config.key
+ delete_rule = config.value.delete_rule
+ }
+ }
+
dynamic "update_policy" {
for_each = var.update_policy == null ? [] : [var.update_policy]
iterator = config
diff --git a/modules/compute-mig/variables.tf b/modules/compute-mig/variables.tf
index def5a74cc..31da4aa34 100644
--- a/modules/compute-mig/variables.tf
+++ b/modules/compute-mig/variables.tf
@@ -65,7 +65,6 @@ variable "location" {
description = "Compute zone, or region if `regional` is set to true."
type = string
}
-
variable "name" {
description = "Managed group name."
type = string
@@ -88,6 +87,37 @@ variable "regional" {
default = false
}
+variable "stateful_config" {
+ description = "Stateful configuration can be done by individual instances or for all instances in the MIG. They key in per_instance_config is the name of the specific instance. The key of the stateful_disks is the 'device_name' field of the resource. Please note that device_name is defined at the OS mount level, unlike the disk name."
+ type = object({
+ per_instance_config = map(object({
+ #name is the key
+ #name = string
+ stateful_disks = map(object({
+ #device_name is the key
+ source = string
+ mode = string # READ_WRITE | READ_ONLY
+ delete_rule = string # NEVER | ON_PERMANENT_INSTANCE_DELETION
+ }))
+ metadata = map(string)
+ update_config = object({
+ minimal_action = string # NONE | REPLACE | RESTART | REFRESH
+ most_disruptive_allowed_action = string # REPLACE | RESTART | REFRESH | NONE
+ remove_instance_state_on_destroy = bool
+ })
+ }))
+
+ mig_config = object({
+ stateful_disks = map(object({
+ #device_name is the key
+ delete_rule = string # NEVER | ON_PERMANENT_INSTANCE_DELETION
+ }))
+ })
+
+ })
+ default = null
+}
+
variable "target_pools" {
description = "Optional list of URLs for target pools to which new instances in the group are added."
type = list(string)
diff --git a/tests/cloud_operations/scheduled_asset_inventory_export_bq/fixture/variables.tf b/tests/cloud_operations/scheduled_asset_inventory_export_bq/fixture/variables.tf
index 67d2624b4..1d70f8272 100644
--- a/tests/cloud_operations/scheduled_asset_inventory_export_bq/fixture/variables.tf
+++ b/tests/cloud_operations/scheduled_asset_inventory_export_bq/fixture/variables.tf
@@ -19,12 +19,14 @@ variable "billing_account" {
variable "cai_config" {
type = object({
- bq_dataset = string
- bq_table = string
+ bq_dataset = string
+ bq_table = string
+ target_node = string
})
default = {
- bq_dataset = "my-dataset"
- bq_table = "my_table"
+ bq_dataset = "my-dataset"
+ bq_table = "my_table"
+ target_node = "organization/1234567890"
}
}
diff --git a/tests/conftest.py b/tests/conftest.py
index 2a90f9256..33c63596c 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -30,7 +30,7 @@ def _plan_runner():
"Runs Terraform plan and returns parsed output."
tf = tftest.TerraformTest(fixture_path, BASEDIR,
os.environ.get('TERRAFORM', 'terraform'))
- tf.setup()
+ tf.setup(upgrade=True)
return tf.plan(output=True, refresh=refresh, tf_vars=tf_vars, targets=targets)
return run_plan
@@ -94,7 +94,7 @@ def apply_runner():
"Runs Terraform apply and returns parsed output"
tf = tftest.TerraformTest(fixture_path, BASEDIR,
os.environ.get('TERRAFORM', 'terraform'))
- tf.setup()
+ tf.setup(upgrade=True)
apply = tf.apply(tf_vars=tf_vars)
output = tf.output(json_format=True)
return apply, output
diff --git a/tests/modules/compute_mig/fixture/main.tf b/tests/modules/compute_mig/fixture/main.tf
index 4d3a53c41..a18b237eb 100644
--- a/tests/modules/compute_mig/fixture/main.tf
+++ b/tests/modules/compute_mig/fixture/main.tf
@@ -14,6 +14,15 @@
* limitations under the License.
*/
+# Used in stateful disk test
+resource "google_compute_disk" "default" {
+ name = "test-disk"
+ type = "pd-ssd"
+ zone = "europe-west1-c"
+ image = "debian-9-stretch-v20200805"
+ physical_block_size_bytes = 4096
+}
+
module "test" {
source = "../../../../modules/compute-mig"
project_id = "my-project"
@@ -28,6 +37,8 @@ module "test" {
health_check_config = var.health_check_config
named_ports = var.named_ports
regional = var.regional
- update_policy = var.update_policy
- versions = var.versions
+ stateful_config = var.stateful_config
+
+ update_policy = var.update_policy
+ versions = var.versions
}
diff --git a/tests/modules/compute_mig/fixture/variables.tf b/tests/modules/compute_mig/fixture/variables.tf
index 2a02cb6c9..c025665cd 100644
--- a/tests/modules/compute_mig/fixture/variables.tf
+++ b/tests/modules/compute_mig/fixture/variables.tf
@@ -60,6 +60,37 @@ variable "regional" {
default = false
}
+variable "stateful_config" {
+ description = "Stateful configuration can be done by individual instances or for all instances in the MIG. They key in per_instance_config is the name of the specific instance. The key of the stateful_disks is the 'device_name' field of the resource. Please note that device_name is defined at the OS mount level, unlike the disk name."
+ type = object({
+ per_instance_config = map(object({
+ #name is the key
+ #name = string
+ stateful_disks = map(object({
+ #device_name is the key
+ source = string
+ mode = string # READ_WRITE | READ_ONLY
+ delete_rule = string # NEVER | ON_PERMANENT_INSTANCE_DELETION
+ }))
+ metadata = map(string)
+ update_config = object({
+ minimal_action = string # NONE | REPLACE | RESTART | REFRESH
+ most_disruptive_allowed_action = string # REPLACE | RESTART | REFRESH | NONE
+ remove_instance_state_on_destroy = bool
+ })
+ }))
+
+ mig_config = object({
+ stateful_disks = map(object({
+ #device_name is the key
+ delete_rule = string # NEVER | ON_PERMANENT_INSTANCE_DELETION
+ }))
+ })
+
+ })
+ default = null
+}
+
variable "update_policy" {
type = object({
type = string # OPPORTUNISTIC | PROACTIVE
diff --git a/tests/modules/compute_mig/test_plan.py b/tests/modules/compute_mig/test_plan.py
index 81e6a313a..a6987904c 100644
--- a/tests/modules/compute_mig/test_plan.py
+++ b/tests/modules/compute_mig/test_plan.py
@@ -24,6 +24,7 @@ def test_defaults(plan_runner):
"Test variable defaults."
_, resources = plan_runner(FIXTURES_DIR)
assert len(resources) == 1
+ print(resources[0]['type'])
mig = resources[0]
assert mig['type'] == 'google_compute_instance_group_manager'
assert mig['values']['target_size'] == 2
@@ -35,7 +36,6 @@ def test_defaults(plan_runner):
assert mig['values']['target_size'] == 2
assert mig['values']['region']
-
def test_health_check(plan_runner):
"Test health check resource."
health_check_config = '{type="tcp", check={port=80}, config=null, logging=false}'
@@ -75,3 +75,81 @@ def test_autoscaler(plan_runner):
assert len(resources) == 2
autoscaler = resources[0]
assert autoscaler['type'] == 'google_compute_region_autoscaler'
+
+def test_stateful_mig(plan_runner):
+ "Test stateful instances - mig."
+
+ stateful_config = (
+ '{'
+ 'per_instance_config = {},'
+ 'mig_config = {'
+ 'stateful_disks = {'
+ 'persistent-disk-1 = {delete_rule="NEVER"}'
+ '}'
+ '}'
+ '}'
+ )
+ _, resources = plan_runner(
+ FIXTURES_DIR, stateful_config=stateful_config)
+ assert len(resources) == 1
+ statefuldisk = resources[0]
+ assert statefuldisk['type'] == 'google_compute_instance_group_manager'
+ assert statefuldisk['values']['stateful_disk'] == [{
+ 'device_name': 'persistent-disk-1',
+ 'delete_rule': 'NEVER',
+ }]
+
+def test_stateful_instance(plan_runner):
+ "Test stateful instances - instance."
+ stateful_config = (
+ '{'
+ 'per_instance_config = {'
+ 'instance-1 = {'
+ 'stateful_disks = {'
+ 'persistent-disk-1 = {'
+ 'source = "test-disk",'
+ 'mode = "READ_ONLY",'
+ 'delete_rule= "NEVER",'
+ '},'
+ '},'
+ 'metadata = {'
+ 'foo = "bar"'
+ '},'
+ 'update_config = {'
+ 'minimal_action = "NONE",'
+ 'most_disruptive_allowed_action = "REPLACE",'
+ 'remove_instance_state_on_destroy = false,'
+
+ '},'
+ '},'
+ '},'
+ 'mig_config = {'
+ 'stateful_disks = {'
+ 'persistent-disk-1 = {delete_rule="NEVER"}'
+ '}'
+ '}'
+ '}'
+ )
+ _, resources = plan_runner(
+ FIXTURES_DIR, stateful_config=stateful_config)
+ assert len(resources) == 2
+ instanceconfig = resources[0]
+ assert instanceconfig['type'] == 'google_compute_instance_group_manager'
+ instanceconfig = resources[1]
+ assert instanceconfig['type'] == 'google_compute_per_instance_config'
+
+ assert instanceconfig['values']['preserved_state'] == [{
+ 'disk': [{
+ 'device_name': 'persistent-disk-1',
+ 'delete_rule': 'NEVER',
+ 'source': 'test-disk',
+ 'mode': 'READ_ONLY',
+ }],
+ 'metadata': {
+ 'foo': 'bar'
+ }
+ }]
+
+ assert instanceconfig['values']['minimal_action'] == 'NONE'
+ assert instanceconfig['values']['most_disruptive_allowed_action'] == 'REPLACE'
+ assert instanceconfig['values']['remove_instance_state_on_destroy'] == False
\ No newline at end of file
diff --git a/tests/requirements.txt b/tests/requirements.txt
index 1b4d95451..480dbeb5b 100644
--- a/tests/requirements.txt
+++ b/tests/requirements.txt
@@ -1,4 +1,4 @@
pytest>=4.6.0
PyYAML>=5.3
-tftest>=1.5.2
+tftest>=1.6.1
marko>=0.9.1