Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added new resource "WasmPlugin" #12275

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
189 changes: 189 additions & 0 deletions mmv1/products/networkservices/WasmPlugin.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
# Copyright 2024 Google Inc.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

---
name: 'WasmPlugin'
description: |
WasmPlugin is a resource representing a service executing a customer-provided Wasm module.
min_version: 'beta'
references:
guides:
'Configure a route extension': 'https://cloud.google.com/service-extensions/docs/create-plugin'
api: 'https://cloud.google.com/service-extensions/docs/reference/rest/v1beta1/projects.locations.wasmPlugins'
docs:
base_url: 'projects/{{project}}/locations/{{location}}/wasmPlugins'
self_link: 'projects/{{project}}/locations/{{location}}/wasmPlugins/{{name}}'
create_url: 'projects/{{project}}/locations/{{location}}/wasmPlugins?wasmPluginId={{name}}'
create_verb: 'POST'
update_verb: 'PATCH'
update_mask: true
read_query_params: '?view=WASM_PLUGIN_VIEW_FULL'
timeouts:
insert_minutes: 20
update_minutes: 20
delete_minutes: 20
async:
actions: ['create', 'delete', 'update']
type: 'OpAsync'
operation:
base_url: '{{op_id}}'
result:
resource_inside_response: false
custom_code:
examples:
- name: 'wasm_plugin_basic'
primary_resource_id: 'wasm_plugin'
min_version: 'beta'
vars:
wasm_plugin_name: 'my-wasm-plugin'
parameters:
- name: 'location'
type: String
description: |
The location of the traffic extension
url_param_only: true
required: true
immutable: true
- name: 'name'
type: String
description: |
Identifier. Name of the WasmPlugin resource.
url_param_only: true
required: true
immutable: true
properties:
- name: 'createTime'
type: Time
description: 'Output only. The timestamp when the resource was created.'
output: true
- name: 'updateTime'
type: Time
description: 'Output only. The timestamp when the resource was updated.'
output: true
- name: 'description'
type: String
description: |
Optional. A human-readable description of the resource.
- name: 'labels'
type: KeyValueLabels
description: 'Optional. Set of labels associated with the WasmPlugin resource.'
- name: 'mainVersionId'
type: ResourceRef
resource: 'WasmPluginVersion'
description: |
Optional. The ID of the WasmPluginVersion resource that is the currently serving one. The version referred to must be a child of this WasmPlugin resource and should be listed in the "versions" field.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a google_network_services_wasm_plugin_version resource on its way?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am currently trying to implement a version resource but I am having some issues since main_version_id appears to be required for creation (despite the API docs describing it as optional) and needs to refer to a version listed in this plugin resource during request. Trying to list a initial version in the plugin resource and config additional versions as resources inevitably causes permadiffs.

Either way, I think this field would be better off as a String since it may not necessarily be used with a future versions resource.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just checked the API proto definition, and main_version_id is annotated with both (google.api.field_behavior) = OPTIONAL and a validator.rule that makes it required (and the latter wins).

The main reason I asked about google_network_services_wasm_plugin_version is if that resource can manage versions and so can a writeable versions field in google_network_services_wasm_plugin then there's no single canonical place to configure versions, which doesn't play well with Terraform.

Thinking about it a little more, I'm not sure that google_network_services_wasm_plugin_version would be possible, at least not without a lot of custom code or a sketchy user experience. Since the parent google_network_services_wasm_plugin needs to exist before creating a google_network_services_wasm_plugin_version child, the configuration sequence would be:

  1. Configure a google_network_services_wasm_plugin with an empty main_version_id (assuming that the validator.rule is incorrect and can be removed from the API)
  2. Create a google_network_services_wasm_plugin_version
  3. Update the plugin from step 1 to set main_version_id to the version from step 2.

As you've discovered, step 3 is what makes things a PITA. There's a couple of ways to work around the problem:

  • The easiest is not to implement google_network_services_wasm_plugin_version and make the versions field of google_network_services_wasm_plugin required instead.
  • If main_version_id really is optional, remove the validator.rule (and wait for the subsequent API rollout). Then, implement google_network_services_wasm_plugin_version with an additional virtual boolean field, something like main_version. When a google_network_services_wasm_plugin_version is configured with main_version = true a custom post-create or post-update step would PATCH the parent plugin resource to set main_version to the version in question.

With the latter, main_version_id and versions in google_network_services_wasm_plugin can be output only or omitted entirely. You'll need custom encoder for main_version_id and versions for updates so that they don't get zeroed out when updating the other fields in the resource.

I think the separate version resource option is a more idiomatically Terraform approach and a better user experience, but it will be a fair amount of additional work to create and maintain. I'll leave it up to you which you prefer.

Copy link
Contributor Author

@matheusaleixo-cit matheusaleixo-cit Nov 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

main_version_id being required seems to be the intended behavior for the API, with versions being managed together with the WasmPlugin, so I will be keeping the versions implementation on the google_network_services_wasm_plugin resource.

- name: 'logConfig'
type: NestedObject
description: |
Optional. Specifies the logging options for the activity performed by this plugin. If logging is enabled, plugin logs are exported to Cloud Logging.
Note that the settings relate to the logs generated by using logging statements in your Wasm code.
properties:
- name: 'enable'
type: Boolean
description: |
Optional. Specifies whether to enable logging for activity by this plugin.
- name: 'sampleRate'
type: Double
validation:
function: 'validation.FloatBetween(0, 1)'
description: |
Non-empty default. Configures the sampling rate of activity logs, where 1.0 means all logged activity is reported and 0.0 means no activity is reported.
A floating point value between 0.0 and 1.0 indicates that a percentage of log messages is stored.
The default value when logging is enabled is 1.0. The value of the field must be between 0 and 1 (inclusive).
This field can be specified only if logging is enabled for this plugin.
default_from_api: true
- name: 'minLogLevel'
type: Enum
description: |
Non-empty default. Specificies the lowest level of the plugin logs that are exported to Cloud Logging. This setting relates to the logs generated by using logging statements in your Wasm code.
This field is can be set only if logging is enabled for the plugin.
If the field is not provided when logging is enabled, it is set to INFO by default.
default_from_api: true
enum_values:
- 'LOG_LEVEL_UNSPECIFIED'
- 'TRACE'
- 'DEBUG'
- 'INFO'
- 'WARN'
- 'ERROR'
- 'CRITICAL'
- name: 'versions'
type: Map
description: |
Optional. All versions of this WasmPlugin resource in the key-value format. The key is the resource ID, and the value is the VersionDetails object.
key_name: 'version_name'
key_description: 'Name of the WasmPluginVersion'
# custom_expand is used for both preventing empty maps being created on update and to remove output fields that are being incorrecty included in the expand when inside a map
custom_expand: 'templates/terraform/custom_expand/wasm_plugin_skip_empty_versions.go.tmpl'
value_type:
name: value
type: NestedObject
properties:
- name: 'createTime'
type: Time
description: 'Output only. The timestamp when the resource was created.'
output: true
- name: 'updateTime'
type: Time
description: 'Output only. The timestamp when the resource was updated.'
output: true
- name: 'description'
type: String
description: |
Optional. A human-readable description of the resource.
- name: 'labels'
type: KeyValuePairs
description: 'Optional. Set of labels associated with the WasmPlugin resource.'
- name: 'imageUri'
type: String
description: |
Optional. URI of the container image containing the plugin, stored in the Artifact Registry. When a new WasmPluginVersion resource is created, the digest of the container image is saved in the imageDigest field.
When downloading an image, the digest value is used instead of an image tag.
- name: 'imageDigest'
type: String
description: |
Output only. The resolved digest for the image specified in the image field. The digest is resolved during the creation of WasmPluginVersion resource.
This field holds the digest value, regardless of whether a tag or digest was originally specified in the image field.
output: true
- name: 'pluginConfigDigest'
type: String
description: |
Output only. This field holds the digest (usually checksum) value for the plugin configuration.
The value is calculated based on the contents of pluginConfigData or the container image defined by the pluginConfigUri field.
output: true
- name: 'pluginConfigData'
type: String
description: |
A base64-encoded string containing the configuration for the plugin. The configuration is provided to the plugin at runtime through the ON_CONFIGURE callback.
When a new WasmPluginVersion resource is created, the digest of the contents is saved in the pluginConfigDigest field.
Conflics with pluginConfigUri.
conflicts:
- pluginConfigUri
- name: 'pluginConfigUri'
type: String
description: |
URI of the plugin configuration stored in the Artifact Registry. The configuration is provided to the plugin at runtime through the ON_CONFIGURE callback.
The container image must contain only a single file with the name plugin.config.
When a new WasmPluginVersion resource is created, the digest of the container image is saved in the pluginConfigDigest field.
Conflics with pluginConfigData.
conflicts:
- pluginConfigData
- name: 'usedBy'
type: Array
description: |
Output only. List of all extensions that use this WasmPlugin resource.
output: true
item_type:
name: 'name'
type: string
description: 'Output only. Full name of the resource'
2 changes: 1 addition & 1 deletion mmv1/products/networkservices/product.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ name: 'NetworkServices'
display_name: 'Network services'
versions:
- name: 'beta'
base_url: 'https://networkservices.googleapis.com/v1/'
base_url: 'https://networkservices.googleapis.com/v1beta1/'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change is what's causing the new test failures. I think (based on looking at your API service config) that the wasm endpoints are being served on /v1/ so you should be able to revert.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/v1/ may not be available until Q1 2025. We need to stay with v1beta1.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The discovery doc says that the wasm plugin endpoints are available under /v1/ so it might be worth trying. If it works, it'll save you from having to fix all the tests that are broken when connecting to /v1beta1/

Copy link
Contributor Author

@matheusaleixo-cit matheusaleixo-cit Nov 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Running the v1 endpoints for wasmPlugins causes a 403 error: "WasmPlugins v1 api is not yet available. Please use the v1beta1 API." so I guess there is no way around that.

Most of the broken tests are related to NetworkServicesEdgeCache* resources, and the docs lists only v1 and v1alpha1 endpoints for these, so maybe we could use v1alpha1 endpoints for the beta provider?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's OK to use a higher-versioned endpoint (like using GA for beta as has been used here) but going the other way is not. This is the first time I've encountered a split where one resource is available in GA but not beta, while another resource is available in beta but not GA. I'll talk to the team and see if there's a precedent for how to handle the situation.

Copy link

@krz-filip krz-filip Nov 21, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Most of the broken tests are related to NetworkServicesEdgeCache* resources, and the docs lists only v1 and v1alpha1 endpoints for these, so maybe we could use v1alpha1 endpoints for the beta provider?

I can't image how NetworkServicesEdgeCache tests may be impacted, as it would require WasmAction resource (a connection between WasmPlugin and EdgeCacheService) which stays in v1alpha and is not a part of the current task (you have access to WasmAction API because your test projects are allowlisted).

At this moment WasmPlugin is intented to be used with LbTrafficExtension and LbRouteExtensions only.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't image how NetworkServicesEdgeCache tests may be impacted

Those tests are unchanged, but Magic Modules only supports a single base URL per version (beta or GA) and this PR changes the base URL for the beta version from /v1/ to /v1beta/.

@matheusaleixo-cit after discussing this with the rest of the team the solution is to move this new resource to a new product (say, networkservicesplugins). That should be a quick change:

  • Create the new mmv1/products/networkservicesplugins directory
  • Move WasmPlugin.yaml to it
  • Copy mmv1/products/networkservices/product.yaml to it, with the beta base url set to /v1beta
  • Revert the change to mmv1/products/networkservices/product.yaml

- name: 'ga'
base_url: 'https://networkservices.googleapis.com/v1/'
scopes:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
func expand{{$.GetPrefix}}{{$.TitlelizeProperty}}(v interface{}, d tpgresource.TerraformResourceData, config *transport_tpg.Config) (interface{}, error) {
if v == nil {
return map[string]interface{}{}, nil
}
m := make(map[string]interface{})
for _, raw := range v.(*schema.Set).List() {
original := raw.(map[string]interface{})
transformed := make(map[string]interface{})

// Ensure we don't send empty versions
if tpgresource.IsEmptyValue(reflect.ValueOf(original["version_name"])) {
continue
}

transformedDescription, err := expandNetworkServicesWasmPluginVersionsFields(original["description"], d, config)
if err != nil {
return nil, err
} else if val := reflect.ValueOf(transformedDescription); val.IsValid() && !tpgresource.IsEmptyValue(val) {
transformed["description"] = transformedDescription
}

transformedLabels, err := expandNetworkServicesWasmPluginVersionsLabels(original["labels"], d, config)
if err != nil {
return nil, err
} else if val := reflect.ValueOf(transformedLabels); val.IsValid() && !tpgresource.IsEmptyValue(val) {
transformed["labels"] = transformedLabels
}

transformedImageUri, err := expandNetworkServicesWasmPluginVersionsFields(original["image_uri"], d, config)
if err != nil {
return nil, err
} else if val := reflect.ValueOf(transformedImageUri); val.IsValid() && !tpgresource.IsEmptyValue(val) {
transformed["imageUri"] = transformedImageUri
}

transformedPluginConfigData, err := expandNetworkServicesWasmPluginVersionsFields(original["plugin_config_data"], d, config)
if err != nil {
return nil, err
} else if val := reflect.ValueOf(transformedPluginConfigData); val.IsValid() && !tpgresource.IsEmptyValue(val) {
transformed["pluginConfigData"] = transformedPluginConfigData
}

transformedPluginConfigUri, err := expandNetworkServicesWasmPluginVersionsFields(original["plugin_config_uri"], d, config)
if err != nil {
return nil, err
} else if val := reflect.ValueOf(transformedPluginConfigUri); val.IsValid() && !tpgresource.IsEmptyValue(val) {
transformed["pluginConfigUri"] = transformedPluginConfigUri
}

transformedVersionName, err := tpgresource.ExpandString(original["version_name"], d, config)
if err != nil {
return nil, err
}
m[transformedVersionName] = transformed
}
return m, nil
}

func expandNetworkServicesWasmPluginVersionsFields(v interface{}, d tpgresource.TerraformResourceData, config *transport_tpg.Config) (interface{}, error) {
return v, nil
}

func expandNetworkServicesWasmPluginVersionsLabels(v interface{}, d tpgresource.TerraformResourceData, config *transport_tpg.Config) (map[string]string, error) {
if v == nil {
return map[string]string{}, nil
}
m := make(map[string]string)
for k, val := range v.(map[string]interface{}) {
m[k] = val.(string)
}
return m, nil
}
30 changes: 30 additions & 0 deletions mmv1/templates/terraform/examples/wasm_plugin_basic.tf.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
data "google_project" "project" { provider = google-beta }

resource "google_network_services_wasm_plugin" "{{$.PrimaryResourceId}}" {
provider = google-beta
name = "{{index $.Vars "wasm_plugin_name"}}"
description = "my wasm plugin"
location = "global"

main_version_id = "v1"

labels = {
test_label = "test_value"
}
log_config {
enable = true
sample_rate = 1
min_log_level = "WARN"
}

versions {
version_name = "v1"
description = "v1 version of my wasm plugin"
image_uri = "us-central1-docker.pkg.dev/${data.google_project.project.name}/svextensionplugin/my-wasm-plugin:prod"

labels = {
test_label = "test_value"
}
}

}
Loading
Loading