From 166e96945684bedf808916e9bc557f2e11df7b70 Mon Sep 17 00:00:00 2001 From: Experience Coder Date: Mon, 9 Sep 2019 21:43:40 +0000 Subject: [PATCH 1/3] GCP Inspec for Standard Application Version Signed-off-by: Modular Magician --- .../google_appengine_standard_app_version.md | 34 +++++++ .../google_appengine_standard_app_versions.md | 32 +++++++ .../google_appengine_standard_app_version.rb | 66 +++++++++++++ .../google_appengine_standard_app_versions.rb | 90 ++++++++++++++++++ test/integration/build/gcp-mm.tf | 37 ++++++- .../integration/configuration/hello-world.zip | Bin 0 -> 58362 bytes .../configuration/mm-attributes.yml | 10 +- .../google_appengine_standard_app_version.rb | 36 +++++++ .../google_appengine_standard_app_versions.rb | 34 +++++++ 9 files changed, 337 insertions(+), 2 deletions(-) create mode 100644 docs/resources/google_appengine_standard_app_version.md create mode 100644 docs/resources/google_appengine_standard_app_versions.md create mode 100644 libraries/google_appengine_standard_app_version.rb create mode 100644 libraries/google_appengine_standard_app_versions.rb create mode 100644 test/integration/configuration/hello-world.zip create mode 100644 test/integration/verify/controls/google_appengine_standard_app_version.rb create mode 100644 test/integration/verify/controls/google_appengine_standard_app_versions.rb diff --git a/docs/resources/google_appengine_standard_app_version.md b/docs/resources/google_appengine_standard_app_version.md new file mode 100644 index 000000000..eb68a11c8 --- /dev/null +++ b/docs/resources/google_appengine_standard_app_version.md @@ -0,0 +1,34 @@ +--- +title: About the google_appengine_standard_app_version resource +platform: gcp +--- + +## Syntax +A `google_appengine_standard_app_version` is used to test a Google StandardAppVersion resource + +## Examples +``` + +describe google_appengine_standard_app_version(project: 'chef-gcp-inspec', location: 'europe-west2', version_id: 'v2', service: 'default') do + it { should exist } + its('version_id') { should eq 'v2' } + its('runtime') { should eq 'nodejs10' } +end +``` + +## Properties +Properties that can be accessed from the `google_appengine_standard_app_version` resource: + + * `name`: Full path to the Version resource in the API. Example, "v1". + + * `version_id`: Relative name of the version within the service. For example, `v1`. Version names can contain only lowercase letters, numbers, or hyphens. Reserved names,"default", "latest", and any name with the prefix "ah-". + + * `runtime`: Desired runtime. Example python27. + + * `threadsafe`: Whether multiple requests can be dispatched to this version at once. + + + +## GCP Permissions + +Ensure the [App Engine Admin API](https://console.cloud.google.com/apis/library/appengine.googleapis.com/) is enabled for the current project. diff --git a/docs/resources/google_appengine_standard_app_versions.md b/docs/resources/google_appengine_standard_app_versions.md new file mode 100644 index 000000000..407535f74 --- /dev/null +++ b/docs/resources/google_appengine_standard_app_versions.md @@ -0,0 +1,32 @@ +--- +title: About the google_appengine_standard_app_versions resource +platform: gcp +--- + +## Syntax +A `google_appengine_standard_app_versions` is used to test a Google StandardAppVersion resource + +## Examples +``` + +describe google_appengine_standard_app_versions(project: 'chef-gcp-inspec', location: 'europe-west2',service: 'default') do + its('runtimes') { should include 'nodejs10' } +end +``` + +## Properties +Properties that can be accessed from the `google_appengine_standard_app_versions` resource: + +See [google_appengine_standard_app_version.md](google_appengine_standard_app_version.md) for more detailed information + * `names`: an array of `google_appengine_standard_app_version` name + * `version_ids`: an array of `google_appengine_standard_app_version` version_id + * `runtimes`: an array of `google_appengine_standard_app_version` runtime + * `threadsaves`: an array of `google_appengine_standard_app_version` threadsafe + +## Filter Criteria +This resource supports all of the above properties as filter criteria, which can be used +with `where` as a block or a method. + +## GCP Permissions + +Ensure the [App Engine Admin API](https://console.cloud.google.com/apis/library/appengine.googleapis.com/) is enabled for the current project. diff --git a/libraries/google_appengine_standard_app_version.rb b/libraries/google_appengine_standard_app_version.rb new file mode 100644 index 000000000..a1cc14340 --- /dev/null +++ b/libraries/google_appengine_standard_app_version.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: false + +# ---------------------------------------------------------------------------- +# +# *** AUTO GENERATED CODE *** AUTO GENERATED CODE *** +# +# ---------------------------------------------------------------------------- +# +# This file is automatically generated by Magic Modules and manual +# changes will be clobbered when the file is regenerated. +# +# Please read more about how to change this file in README.md and +# CONTRIBUTING.md located at the root of this package. +# +# ---------------------------------------------------------------------------- +require 'gcp_backend' + +# A provider to manage App Engine resources. +class StandardAppVersion < GcpResourceBase + name 'google_appengine_standard_app_version' + desc 'StandardAppVersion' + supports platform: 'gcp' + + attr_reader :params + attr_reader :name + attr_reader :version_id + attr_reader :runtime + attr_reader :threadsafe + + def initialize(params) + super(params.merge({ use_http_transport: true })) + @params = params + @fetched = @connection.fetch(product_url, resource_base_url, params, 'Get') + parse unless @fetched.nil? + end + + def parse + @name = @fetched['name'] + @version_id = @fetched['id'] + @runtime = @fetched['runtime'] + @threadsafe = @fetched['threadsafe'] + end + + # Handles parsing RFC3339 time string + def parse_time_string(time_string) + time_string ? Time.parse(time_string) : nil + end + + def exists? + !@fetched.nil? + end + + def to_s + "StandardAppVersion #{@params[:version_id]}" + end + + private + + def product_url + 'https://appengine.googleapis.com/v1/' + end + + def resource_base_url + 'apps/{{project}}/services/{{service}}/versions/{{version_id}}' + end +end diff --git a/libraries/google_appengine_standard_app_versions.rb b/libraries/google_appengine_standard_app_versions.rb new file mode 100644 index 000000000..9e595203c --- /dev/null +++ b/libraries/google_appengine_standard_app_versions.rb @@ -0,0 +1,90 @@ +# frozen_string_literal: false + +# ---------------------------------------------------------------------------- +# +# *** AUTO GENERATED CODE *** AUTO GENERATED CODE *** +# +# ---------------------------------------------------------------------------- +# +# This file is automatically generated by Magic Modules and manual +# changes will be clobbered when the file is regenerated. +# +# Please read more about how to change this file in README.md and +# CONTRIBUTING.md located at the root of this package. +# +# ---------------------------------------------------------------------------- +require 'gcp_backend' +class StandardAppVersions < GcpResourceBase + name 'google_appengine_standard_app_versions' + desc 'StandardAppVersion plural resource' + supports platform: 'gcp' + + attr_reader :table + + filter_table_config = FilterTable.create + + filter_table_config.add(:names, field: :name) + filter_table_config.add(:version_ids, field: :version_id) + filter_table_config.add(:runtimes, field: :runtime) + filter_table_config.add(:threadsaves, field: :threadsafe) + + filter_table_config.connect(self, :table) + + def initialize(params = {}) + super(params.merge({ use_http_transport: true })) + @params = params + @table = fetch_wrapped_resource('versions') + end + + def fetch_wrapped_resource(wrap_path) + # fetch_resource returns an array of responses (to handle pagination) + result = @connection.fetch_all(product_url, resource_base_url, @params, 'Get') + return if result.nil? + + # Conversion of string -> object hash to symbol -> object hash that InSpec needs + converted = [] + result.each do |response| + next if response.nil? || !response.key?(wrap_path) + response[wrap_path].each do |hash| + hash_with_symbols = {} + hash.each_key do |key| + name, value = transform(key, hash) + hash_with_symbols[name] = value + end + converted.push(hash_with_symbols) + end + end + + converted + end + + def transform(key, value) + return transformers[key].call(value) if transformers.key?(key) + + [key.to_sym, value] + end + + def transformers + { + 'name' => ->(obj) { return :name, obj['name'] }, + 'id' => ->(obj) { return :version_id, obj['id'] }, + 'runtime' => ->(obj) { return :runtime, obj['runtime'] }, + 'threadsafe' => ->(obj) { return :threadsafe, obj['threadsafe'] }, + } + end + + # Handles parsing RFC3339 time string + def parse_time_string(time_string) + time_string ? Time.parse(time_string) : nil + end + + private + + def product_url + 'https://appengine.googleapis.com/v1/' + end + + def resource_base_url + 'apps/{{project}}/services/{{service}}/versions' + end +end diff --git a/test/integration/build/gcp-mm.tf b/test/integration/build/gcp-mm.tf index bd9564059..c3f8dee94 100644 --- a/test/integration/build/gcp-mm.tf +++ b/test/integration/build/gcp-mm.tf @@ -145,6 +145,10 @@ variable "org_sink" { type = "map" } +variable "standardappversion" { + type = "map" +} + resource "google_compute_ssl_policy" "custom-ssl-policy" { name = "${var.ssl_policy["name"]}" min_tls_version = "${var.ssl_policy["min_tls_version"]}" @@ -568,4 +572,35 @@ resource "google_logging_organization_sink" "my-sink" { # Log all WARN or higher severity messages relating to instances filter = "${var.org_sink.filter}" -} \ No newline at end of file +} + +resource "google_storage_bucket" "bucket" { + name = "appengine-static-content" +} + +resource "google_storage_bucket_object" "object" { + name = "hello-world.zip" + bucket = "${google_storage_bucket.bucket.name}" + source = "./test-fixtures/appengine/hello-world.zip" +} + +resource "google_app_engine_standard_app_version" "default" { + project = "${var.gcp_project_id}" + version_id = "${var.standardappversion["version_id"]}" + service = "${var.standardappversion["service"]}" + runtime = "${var.standardappversion["runtime"]}" + noop_on_destroy = true + entrypoint { + shell = "${var.standardappversion["entrypoint"]}" + } + + deployment { + zip { + source_url = "https://storage.googleapis.com/${google_storage_bucket.bucket.name}/hello-world.zip" + } + } + + env_variables = { + port = "${var.standardappversion["port"]}" + } +} diff --git a/test/integration/configuration/hello-world.zip b/test/integration/configuration/hello-world.zip new file mode 100644 index 0000000000000000000000000000000000000000..34c746dd389de9234b3a34c14a410ca34fe63c8d GIT binary patch literal 58362 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 Date: Tue, 10 Sep 2019 00:09:53 +0000 Subject: [PATCH 2/3] Add legacy_name to inspec, add ML model Signed-off-by: Modular Magician --- docs/resources/google_ml_engine_model.md | 47 +++++++++ docs/resources/google_ml_engine_models.md | 36 +++++++ .../property/model_default_version.rb | 34 +++++++ libraries/google_ml_engine_model.rb | 73 ++++++++++++++ libraries/google_ml_engine_models.rb | 96 +++++++++++++++++++ test/integration/build/gcp-mm.tf | 22 ++++- .../configuration/mm-attributes.yml | 8 +- .../verify/controls/google_ml_engine_model.rb | 41 ++++++++ .../controls/google_ml_engine_models.rb | 35 +++++++ 9 files changed, 387 insertions(+), 5 deletions(-) create mode 100644 docs/resources/google_ml_engine_model.md create mode 100644 docs/resources/google_ml_engine_models.md create mode 100644 libraries/google/mlengine/property/model_default_version.rb create mode 100644 libraries/google_ml_engine_model.rb create mode 100644 libraries/google_ml_engine_models.rb create mode 100644 test/integration/verify/controls/google_ml_engine_model.rb create mode 100644 test/integration/verify/controls/google_ml_engine_models.rb diff --git a/docs/resources/google_ml_engine_model.md b/docs/resources/google_ml_engine_model.md new file mode 100644 index 000000000..fec3ad28c --- /dev/null +++ b/docs/resources/google_ml_engine_model.md @@ -0,0 +1,47 @@ +--- +title: About the google_ml_engine_model resource +platform: gcp +--- + +## Syntax +A `google_ml_engine_model` is used to test a Google Model resource + +## Examples +``` +describe google_ml_engine_model(project: 'chef-gcp-inspec', name: 'ml_model') do + it { should exist } + its('description') { should cmp 'My awesome ML model' } + its('regions') { should include 'us-central1' } + its('online_prediction_logging') { should cmp 'true' } + its('online_prediction_console_logging') { should cmp 'true' } +end + +describe google_ml_engine_model(project: 'chef-gcp-inspec', name: 'nonexistent') do + it { should_not exist } +end +``` + +## Properties +Properties that can be accessed from the `google_ml_engine_model` resource: + + * `name`: The name specified for the model. + + * `description`: The description specified for the model when it was created. + + * `default_version`: The default version of the model. This version will be used to handle prediction requests that do not specify a version. + + * `name`: The name specified for the version when it was created. + + * `regions`: The list of regions where the model is going to be deployed. Currently only one region per model is supported + + * `online_prediction_logging`: If true, online prediction access logs are sent to StackDriver Logging. + + * `online_prediction_console_logging`: If true, online prediction nodes send stderr and stdout streams to Stackdriver Logging + + * `labels`: One or more labels that you can add, to organize your models. + + + +## GCP Permissions + +Ensure the [Cloud ML](https://console.cloud.google.com/apis/library/ml.googleapis.com) is enabled for the current project. diff --git a/docs/resources/google_ml_engine_models.md b/docs/resources/google_ml_engine_models.md new file mode 100644 index 000000000..9a5165163 --- /dev/null +++ b/docs/resources/google_ml_engine_models.md @@ -0,0 +1,36 @@ +--- +title: About the google_ml_engine_models resource +platform: gcp +--- + +## Syntax +A `google_ml_engine_models` is used to test a Google Model resource + +## Examples +``` +describe google_ml_engine_models(project: 'chef-gcp-inspec') do + its('descriptions') { should include 'My awesome ML model' } + its('online_prediction_loggings') { should include 'true' } + its('online_prediction_console_loggings') { should include 'true' } +end +``` + +## Properties +Properties that can be accessed from the `google_ml_engine_models` resource: + +See [google_ml_engine_model.md](google_ml_engine_model.md) for more detailed information + * `names`: an array of `google_ml_engine_model` name + * `descriptions`: an array of `google_ml_engine_model` description + * `default_versions`: an array of `google_ml_engine_model` default_version + * `regions`: an array of `google_ml_engine_model` regions + * `online_prediction_loggings`: an array of `google_ml_engine_model` online_prediction_logging + * `online_prediction_console_loggings`: an array of `google_ml_engine_model` online_prediction_console_logging + * `labels`: an array of `google_ml_engine_model` labels + +## Filter Criteria +This resource supports all of the above properties as filter criteria, which can be used +with `where` as a block or a method. + +## GCP Permissions + +Ensure the [Cloud ML](https://console.cloud.google.com/apis/library/ml.googleapis.com) is enabled for the current project. diff --git a/libraries/google/mlengine/property/model_default_version.rb b/libraries/google/mlengine/property/model_default_version.rb new file mode 100644 index 000000000..3305c3b3c --- /dev/null +++ b/libraries/google/mlengine/property/model_default_version.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: false + +# ---------------------------------------------------------------------------- +# +# *** AUTO GENERATED CODE *** AUTO GENERATED CODE *** +# +# ---------------------------------------------------------------------------- +# +# This file is automatically generated by Magic Modules and manual +# changes will be clobbered when the file is regenerated. +# +# Please read more about how to change this file in README.md and +# CONTRIBUTING.md located at the root of this package. +# +# ---------------------------------------------------------------------------- +module GoogleInSpec + module MLEngine + module Property + class ModelDefaultVersion + attr_reader :name + + def initialize(args = nil, parent_identifier = nil) + return if args.nil? + @parent_identifier = parent_identifier + @name = args['name'] + end + + def to_s + "#{@parent_identifier} ModelDefaultVersion" + end + end + end + end +end diff --git a/libraries/google_ml_engine_model.rb b/libraries/google_ml_engine_model.rb new file mode 100644 index 000000000..774d941db --- /dev/null +++ b/libraries/google_ml_engine_model.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: false + +# ---------------------------------------------------------------------------- +# +# *** AUTO GENERATED CODE *** AUTO GENERATED CODE *** +# +# ---------------------------------------------------------------------------- +# +# This file is automatically generated by Magic Modules and manual +# changes will be clobbered when the file is regenerated. +# +# Please read more about how to change this file in README.md and +# CONTRIBUTING.md located at the root of this package. +# +# ---------------------------------------------------------------------------- +require 'gcp_backend' +require 'google/mlengine/property/model_default_version' + +# A provider to manage ML Engine resources. +class Model < GcpResourceBase + name 'google_ml_engine_model' + desc 'Model' + supports platform: 'gcp' + + attr_reader :params + attr_reader :name + attr_reader :description + attr_reader :default_version + attr_reader :regions + attr_reader :online_prediction_logging + attr_reader :online_prediction_console_logging + attr_reader :labels + + def initialize(params) + super(params.merge({ use_http_transport: true })) + @params = params + @fetched = @connection.fetch(product_url, resource_base_url, params, 'Get') + parse unless @fetched.nil? + end + + def parse + @name = @fetched['name'] + @description = @fetched['description'] + @default_version = GoogleInSpec::MLEngine::Property::ModelDefaultVersion.new(@fetched['defaultVersion'], to_s) + @regions = @fetched['regions'] + @online_prediction_logging = @fetched['onlinePredictionLogging'] + @online_prediction_console_logging = @fetched['onlinePredictionConsoleLogging'] + @labels = @fetched['labels'] + end + + # Handles parsing RFC3339 time string + def parse_time_string(time_string) + time_string ? Time.parse(time_string) : nil + end + + def exists? + !@fetched.nil? + end + + def to_s + "Model #{@params[:name]}" + end + + private + + def product_url + 'https://ml.googleapis.com/v1/' + end + + def resource_base_url + 'projects/{{project}}/models/{{name}}' + end +end diff --git a/libraries/google_ml_engine_models.rb b/libraries/google_ml_engine_models.rb new file mode 100644 index 000000000..5278bcb68 --- /dev/null +++ b/libraries/google_ml_engine_models.rb @@ -0,0 +1,96 @@ +# frozen_string_literal: false + +# ---------------------------------------------------------------------------- +# +# *** AUTO GENERATED CODE *** AUTO GENERATED CODE *** +# +# ---------------------------------------------------------------------------- +# +# This file is automatically generated by Magic Modules and manual +# changes will be clobbered when the file is regenerated. +# +# Please read more about how to change this file in README.md and +# CONTRIBUTING.md located at the root of this package. +# +# ---------------------------------------------------------------------------- +require 'gcp_backend' +class Models < GcpResourceBase + name 'google_ml_engine_models' + desc 'Model plural resource' + supports platform: 'gcp' + + attr_reader :table + + filter_table_config = FilterTable.create + + filter_table_config.add(:names, field: :name) + filter_table_config.add(:descriptions, field: :description) + filter_table_config.add(:default_versions, field: :default_version) + filter_table_config.add(:regions, field: :regions) + filter_table_config.add(:online_prediction_loggings, field: :online_prediction_logging) + filter_table_config.add(:online_prediction_console_loggings, field: :online_prediction_console_logging) + filter_table_config.add(:labels, field: :labels) + + filter_table_config.connect(self, :table) + + def initialize(params = {}) + super(params.merge({ use_http_transport: true })) + @params = params + @table = fetch_wrapped_resource('models') + end + + def fetch_wrapped_resource(wrap_path) + # fetch_resource returns an array of responses (to handle pagination) + result = @connection.fetch_all(product_url, resource_base_url, @params, 'Get') + return if result.nil? + + # Conversion of string -> object hash to symbol -> object hash that InSpec needs + converted = [] + result.each do |response| + next if response.nil? || !response.key?(wrap_path) + response[wrap_path].each do |hash| + hash_with_symbols = {} + hash.each_key do |key| + name, value = transform(key, hash) + hash_with_symbols[name] = value + end + converted.push(hash_with_symbols) + end + end + + converted + end + + def transform(key, value) + return transformers[key].call(value) if transformers.key?(key) + + [key.to_sym, value] + end + + def transformers + { + 'name' => ->(obj) { return :name, obj['name'] }, + 'description' => ->(obj) { return :description, obj['description'] }, + 'defaultVersion' => ->(obj) { return :default_version, GoogleInSpec::MLEngine::Property::ModelDefaultVersion.new(obj['defaultVersion'], to_s) }, + 'regions' => ->(obj) { return :regions, obj['regions'] }, + 'onlinePredictionLogging' => ->(obj) { return :online_prediction_logging, obj['onlinePredictionLogging'] }, + 'onlinePredictionConsoleLogging' => ->(obj) { return :online_prediction_console_logging, obj['onlinePredictionConsoleLogging'] }, + 'labels' => ->(obj) { return :labels, obj['labels'] }, + } + end + + # Handles parsing RFC3339 time string + def parse_time_string(time_string) + time_string ? Time.parse(time_string) : nil + end + + private + + def product_url + 'https://ml.googleapis.com/v1/' + end + + def resource_base_url + 'projects/{{project}}/models' + end +end diff --git a/test/integration/build/gcp-mm.tf b/test/integration/build/gcp-mm.tf index c3f8dee94..43f5521e4 100644 --- a/test/integration/build/gcp-mm.tf +++ b/test/integration/build/gcp-mm.tf @@ -149,6 +149,10 @@ variable "standardappversion" { type = "map" } +variable "ml_model" { + type = "map" +} + resource "google_compute_ssl_policy" "custom-ssl-policy" { name = "${var.ssl_policy["name"]}" min_tls_version = "${var.ssl_policy["min_tls_version"]}" @@ -575,13 +579,14 @@ resource "google_logging_organization_sink" "my-sink" { } resource "google_storage_bucket" "bucket" { - name = "appengine-static-content" + name = "inspec-gcp-static-${var.gcp_project_id}" + project = var.gcp_project_id } resource "google_storage_bucket_object" "object" { - name = "hello-world.zip" - bucket = "${google_storage_bucket.bucket.name}" - source = "./test-fixtures/appengine/hello-world.zip" + name = "hello-world.zip" + bucket = "${google_storage_bucket.bucket.name}" + source = "../configuration/hello-world.zip" } resource "google_app_engine_standard_app_version" "default" { @@ -604,3 +609,12 @@ resource "google_app_engine_standard_app_version" "default" { port = "${var.standardappversion["port"]}" } } + +resource "google_ml_engine_model" "inspec-gcp-model" { + project = var.gcp_project_id + name = var.ml_model["name"] + description = var.ml_model["description"] + regions = ["${var.ml_model["region"]}"] + online_prediction_logging = var.ml_model["online_prediction_logging"] + online_prediction_console_logging = var.ml_model["online_prediction_console_logging"] +} diff --git a/test/integration/configuration/mm-attributes.yml b/test/integration/configuration/mm-attributes.yml index d5476c8ea..5eb93f675 100644 --- a/test/integration/configuration/mm-attributes.yml +++ b/test/integration/configuration/mm-attributes.yml @@ -232,4 +232,10 @@ standardappversion: runtime: nodejs10 entrypoint: "node ./app.js" port: "8080" - \ No newline at end of file + +ml_model: + name: ml_model + region: us-central1 + description: My awesome ML model + online_prediction_logging: true + online_prediction_console_logging: true diff --git a/test/integration/verify/controls/google_ml_engine_model.rb b/test/integration/verify/controls/google_ml_engine_model.rb new file mode 100644 index 000000000..32bfea54f --- /dev/null +++ b/test/integration/verify/controls/google_ml_engine_model.rb @@ -0,0 +1,41 @@ +# ---------------------------------------------------------------------------- +# +# *** AUTO GENERATED CODE *** AUTO GENERATED CODE *** +# +# ---------------------------------------------------------------------------- +# +# This file is automatically generated by Magic Modules and manual +# changes will be clobbered when the file is regenerated. +# +# Please read more about how to change this file in README.md and +# CONTRIBUTING.md located at the root of this package. +# +# ---------------------------------------------------------------------------- + +title 'Test GCP google_ml_engine_model resource.' + +gcp_project_id = attribute(:gcp_project_id, default: 'gcp_project_id', description: 'The GCP project identifier.') +gcp_location = attribute(:gcp_location, default: 'gcp_location', description: 'The GCP project region.') +ml_model = attribute('ml_model', default: { + "name": "ml_model", + "region": "us-central1", + "description": "My awesome ML model", + "online_prediction_logging": true, + "online_prediction_console_logging": true +}, description: 'Machine learning model definition') +control 'google_ml_engine_model-1.0' do + impact 1.0 + title 'google_ml_engine_model resource test' + + describe google_ml_engine_model(project: gcp_project_id, name: ml_model['name']) do + it { should exist } + its('description') { should cmp ml_model['description'] } + its('regions') { should include ml_model['region'] } + its('online_prediction_logging') { should cmp ml_model['online_prediction_logging'] } + its('online_prediction_console_logging') { should cmp ml_model['online_prediction_console_logging'] } + end + + describe google_ml_engine_model(project: gcp_project_id, name: 'nonexistent') do + it { should_not exist } + end +end diff --git a/test/integration/verify/controls/google_ml_engine_models.rb b/test/integration/verify/controls/google_ml_engine_models.rb new file mode 100644 index 000000000..7bddc2333 --- /dev/null +++ b/test/integration/verify/controls/google_ml_engine_models.rb @@ -0,0 +1,35 @@ +# ---------------------------------------------------------------------------- +# +# *** AUTO GENERATED CODE *** AUTO GENERATED CODE *** +# +# ---------------------------------------------------------------------------- +# +# This file is automatically generated by Magic Modules and manual +# changes will be clobbered when the file is regenerated. +# +# Please read more about how to change this file in README.md and +# CONTRIBUTING.md located at the root of this package. +# +# ---------------------------------------------------------------------------- + +title 'Test GCP google_ml_engine_models resource.' + +gcp_project_id = attribute(:gcp_project_id, default: 'gcp_project_id', description: 'The GCP project identifier.') +gcp_location = attribute(:gcp_location, default: 'gcp_location', description: 'The GCP project region.') +ml_model = attribute('ml_model', default: { + "name": "ml_model", + "region": "us-central1", + "description": "My awesome ML model", + "online_prediction_logging": true, + "online_prediction_console_logging": true +}, description: 'Machine learning model definition') +control 'google_ml_engine_models-1.0' do + impact 1.0 + title 'google_ml_engine_models resource test' + + describe google_ml_engine_models(project: gcp_project_id) do + its('descriptions') { should include ml_model['description'] } + its('online_prediction_loggings') { should include ml_model['online_prediction_logging'] } + its('online_prediction_console_loggings') { should include ml_model['online_prediction_console_logging'] } + end +end From 6ca5879070080450620c6e81983de5d26495b9b0 Mon Sep 17 00:00:00 2001 From: Chef Expeditor Date: Mon, 16 Sep 2019 11:17:12 +0000 Subject: [PATCH 3/3] Bump version to 0.15.0 by Chef Expeditor Obvious fix; these changes are the result of automation not creative thinking. --- CHANGELOG.md | 9 +++++++-- VERSION | 2 +- inspec.yml | 2 +- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 316519fcb..5efc9d575 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,15 @@ # Change Log - + +## [v0.15.0](https://github.com/inspec/inspec-gcp/tree/v0.15.0) (2019-09-16) + +#### Merged Pull Requests +- Appengine, ML engine, project IAM resources [#178](https://github.com/inspec/inspec-gcp/pull/178) ([slevenick](https://github.com/slevenick)) + + ## [v0.14.0](https://github.com/inspec/inspec-gcp/tree/v0.14.0) (2019-09-16) #### Merged Pull Requests - Project iam policy [#177](https://github.com/inspec/inspec-gcp/pull/177) ([slevenick](https://github.com/slevenick)) - ## [v0.13.2](https://github.com/inspec/inspec-gcp/tree/v0.13.2) (2019-08-29) diff --git a/VERSION b/VERSION index 0548fb4e9..7092c7c46 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.14.0 \ No newline at end of file +0.15.0 \ No newline at end of file diff --git a/inspec.yml b/inspec.yml index 1f1ddfa53..16ac48a2f 100644 --- a/inspec.yml +++ b/inspec.yml @@ -4,7 +4,7 @@ maintainer: spaterson@chef.io,russell.seymour@turtlesystems.co.uk summary: This resource pack provides compliance resources_old_ignore for Google Cloud Platform copyright: spaterson@chef.io,russell.seymour@turtlesystems.co.uk copyright_email: spaterson@chef.io,russell.seymour@turtlesystems.co.uk -version: 0.14.0 +version: 0.15.0 license: Apache-2.0 inspec_version: '>= 4.7.3' supports: