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

Add RDE metadata #1018

Merged
merged 40 commits into from
Nov 28, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 2 additions & 0 deletions .changes/v3.11.0/1018-improvements.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
* Add `metadata_entry` attribute to `vcd_rde` resource and data source to manage metadata entries of type
`String`, `Number` and `Bool` in Runtime Defined Entities [GH-1018]
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ require (
github.com/hashicorp/go-version v1.6.0
github.com/hashicorp/terraform-plugin-sdk/v2 v2.29.0
github.com/kr/pretty v0.2.1
github.com/vmware/go-vcloud-director/v2 v2.22.0-alpha.13
github.com/vmware/go-vcloud-director/v2 v2.22.0-alpha.14
)

require (
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,8 @@ github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9
github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc=
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
github.com/vmware/go-vcloud-director/v2 v2.22.0-alpha.13 h1:qC0F1lLLgkpt4SgInIUh3qmDD+tSKkMITbfx9OnpkSs=
github.com/vmware/go-vcloud-director/v2 v2.22.0-alpha.13/go.mod h1:QPxGFgrUcSyzy9IlpwDE4UNT3tsOy2047tJOPEJ4nlw=
github.com/vmware/go-vcloud-director/v2 v2.22.0-alpha.14 h1:xsyLC+kVS57PeC4HoVXk798ScVryDs+a2ym3t1BPVB0=
github.com/vmware/go-vcloud-director/v2 v2.22.0-alpha.14/go.mod h1:QPxGFgrUcSyzy9IlpwDE4UNT3tsOy2047tJOPEJ4nlw=
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/zclconf/go-cty v1.14.0 h1:/Xrd39K7DXbHzlisFP9c4pHao4yyf+/Ug9LEz+Y/yhc=
Expand Down
6 changes: 6 additions & 0 deletions vcd/datasource_vcd_rde.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ func datasourceVcdRde() *schema.Resource {
Description: "Specifies whether the entity is correctly resolved or not. One of PRE_CREATED, RESOLVED or RESOLUTION_ERROR",
Computed: true,
},
"metadata_entry": openApiMetadataEntryDatasourceSchema("Runtime Defined Entity"),
},
}
}
Expand Down Expand Up @@ -83,6 +84,11 @@ func datasourceVcdRdeRead(_ context.Context, d *schema.ResourceData, meta interf
dSet(d, "owner_user_id", rde.DefinedEntity.Owner.ID)
}

err = updateOpenApiMetadataInState(d, rde)
if err != nil {
return diag.Errorf("could not set metadata for the Runtime Defined Entity: %s", err)
}

d.SetId(rde.DefinedEntity.ID)

return nil
Expand Down
14 changes: 7 additions & 7 deletions vcd/metadata_common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -608,13 +608,13 @@ func getMetadataEntryHcl(key, value, typedValue, userAccess, isSystem string) st
isSystemAttr = fmt.Sprintf("is_system = \"%s\"", isSystem)
}
return fmt.Sprintf(`
metadata_entry {
%s
%s
%s
%s
%s
}
metadata_entry {
%s
%s
%s
%s
%s
}
`, keyAttr, valueAttr, typeAttr, userAccAttr, isSystemAttr)
}

Expand Down
335 changes: 335 additions & 0 deletions vcd/metadata_openapi.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,335 @@
package vcd

import (
"fmt"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
"github.com/vmware/go-vcloud-director/v2/govcd"
"github.com/vmware/go-vcloud-director/v2/types/v56"
"github.com/vmware/go-vcloud-director/v2/util"
"reflect"
"strconv"
)

// openApiMetadataEntryDatasourceSchema returns the schema associated to the OpenAPI metadata_entry for a given data source.
// The description will refer to the object type given as input.
func openApiMetadataEntryDatasourceSchema(resourceType string) *schema.Schema {
return &schema.Schema{
Type: schema.TypeSet,
Computed: true,
Description: fmt.Sprintf("Metadata entries from the given %s", resourceType),
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"id": {
Type: schema.TypeString,
Computed: true,
Description: "ID of the metadata entry",
},
"key": {
Type: schema.TypeString,
Computed: true,
Description: "Key of this metadata entry",
},
"value": {
Type: schema.TypeString,
Computed: true,
Description: "Value of this metadata entry",
},
"type": {
Type: schema.TypeString,
Computed: true,
Description: fmt.Sprintf("Type of this metadata entry. One of: '%s', '%s', '%s'", types.OpenApiMetadataStringEntry, types.OpenApiMetadataNumberEntry, types.OpenApiMetadataBooleanEntry),
},
"readonly": {
Type: schema.TypeBool,
Computed: true,
Description: "True if the metadata entry is read only",
},
"domain": {
Type: schema.TypeString,
Computed: true,
Description: "Only meaningful for providers. Allows them to share entries with their tenants. One of: `TENANT`, `PROVIDER`",
},
"namespace": {
Type: schema.TypeString,
Computed: true,
Description: "Namespace of the metadata entry",
},
"persistent": {
Type: schema.TypeBool,
Computed: true,
Description: "Persistent metadata entries can be copied over on some entity operation",
},
},
},
}
}

// openApiMetadataEntryResourceSchema returns the schema associated to the OpenAPI metadata_entry for a given resource.
// The description will refer to the object name given as input.
func openApiMetadataEntryResourceSchema(resourceType string) *schema.Schema {
return &schema.Schema{
Type: schema.TypeSet,
Optional: true,
Description: fmt.Sprintf("Metadata entries for the given %s", resourceType),
MaxItems: 50, // As per the documentation
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"id": {
Type: schema.TypeString,
Computed: true,
Description: "ID of the metadata entry",
},
"key": {
Type: schema.TypeString,
Required: true,
Description: "Key of this metadata entry. Required if the metadata entry is not empty",
},
"value": {
Type: schema.TypeString,
Required: true,
Description: "Value of this metadata entry. Required if the metadata entry is not empty",
},
"type": {
Type: schema.TypeString,
Optional: true,
Default: types.OpenApiMetadataStringEntry,
Description: fmt.Sprintf("Type of this metadata entry. One of: '%s', '%s', '%s'", types.OpenApiMetadataStringEntry, types.OpenApiMetadataNumberEntry, types.OpenApiMetadataBooleanEntry),
ValidateFunc: validation.StringInSlice([]string{types.OpenApiMetadataStringEntry, types.OpenApiMetadataNumberEntry, types.OpenApiMetadataBooleanEntry}, false),
},
"readonly": {
Type: schema.TypeBool,
Optional: true,
Default: false,
Description: "True if the metadata entry is read only",
},
"domain": {
Type: schema.TypeString,
Optional: true,
Default: "TENANT",
Description: "Only meaningful for providers. Allows them to share entries with their tenants. Currently, accepted values are: `TENANT`, `PROVIDER`",
ValidateFunc: validation.StringInSlice([]string{"TENANT", "PROVIDER"}, false),
},
"namespace": {
Type: schema.TypeString,
Optional: true,
Description: "Namespace of the metadata entry",
},
"persistent": {
Type: schema.TypeBool,
Optional: true,
Description: "Persistent metadata entries can be copied over on some entity operation",
},
},
},
}
}

// openApiMetadataCompatible allows to consider all structs that implement OpenAPI metadata handling to be the same type
type openApiMetadataCompatible interface {
GetMetadata() ([]*govcd.OpenApiMetadataEntry, error)
GetMetadataByKey(domain, namespace, key string) (*govcd.OpenApiMetadataEntry, error)
GetMetadataById(id string) (*govcd.OpenApiMetadataEntry, error)
AddMetadata(metadataEntry types.OpenApiMetadataEntry) (*govcd.OpenApiMetadataEntry, error)
}

// createOrUpdateOpenApiMetadataEntryInVcd creates or updates OpenAPI metadata entries in VCD for the given resource, only if the attribute
// metadata_entry has been set or updated in the state.
func createOrUpdateOpenApiMetadataEntryInVcd(d *schema.ResourceData, resource openApiMetadataCompatible) error {
if !d.HasChange("metadata_entry") {
return nil
}

oldRaw, newRaw := d.GetChange("metadata_entry")
metadataToAdd, metadataToUpdate, metadataToDelete, err := getOpenApiMetadataOperations(oldRaw.(*schema.Set).List(), newRaw.(*schema.Set).List())
if err != nil {
return fmt.Errorf("could not calculate the needed metadata operations: %s", err)
}

for _, entry := range metadataToDelete {
toDelete, err := resource.GetMetadataByKey(entry.KeyValue.Domain, entry.KeyValue.Namespace, entry.KeyValue.Key) // Refreshes ETags
if err != nil {
return fmt.Errorf("error reading metadata with namespace '%s' and key '%s': %s", entry.KeyValue.Namespace, entry.KeyValue.Key, err)
}
err = toDelete.Delete()
if err != nil {
return fmt.Errorf("error deleting metadata with namespace '%s' and key '%s': %s", entry.KeyValue.Namespace, entry.KeyValue.Key, err)
}
}

for _, entry := range metadataToUpdate {
toUpdate, err := resource.GetMetadataByKey(entry.KeyValue.Domain, entry.KeyValue.Namespace, entry.KeyValue.Key) // Refreshes ETags
if err != nil {
return fmt.Errorf("error reading metadata with namespace '%s' and key '%s': %s", entry.KeyValue.Namespace, entry.KeyValue.Key, err)
}
err = toUpdate.Update(entry.KeyValue.Value.Value, entry.IsPersistent)
if err != nil {
return fmt.Errorf("error updating metadata with namespace '%s' and key '%s': %s", entry.KeyValue.Namespace, entry.KeyValue.Key, err)
}
}

for _, metadataEntry := range metadataToAdd {
_, err := resource.AddMetadata(metadataEntry)
if err != nil {
return fmt.Errorf("error adding metadata entry: %s", err)
}
}
return nil
}

// getOpenApiMetadataOperations retrieves the metadata that needs to be added, to be updated and to be deleted depending
// on the old and new attribute values from Terraform state.
func getOpenApiMetadataOperations(oldMetadata []interface{}, newMetadata []interface{}) ([]types.OpenApiMetadataEntry, []types.OpenApiMetadataEntry, []types.OpenApiMetadataEntry, error) {
oldMetadataEntries, err := getOpenApiMetadataEntryMap(oldMetadata)
if err != nil {
return nil, nil, nil, err
}
newMetadataEntries, err := getOpenApiMetadataEntryMap(newMetadata)
if err != nil {
return nil, nil, nil, err
}

var metadataToRemove []types.OpenApiMetadataEntry
for oldNamespacedKey := range oldMetadataEntries {
if _, ok := newMetadataEntries[oldNamespacedKey]; !ok {
metadataToRemove = append(metadataToRemove, oldMetadataEntries[oldNamespacedKey])
}
}

var metadataToCreate []types.OpenApiMetadataEntry
metadataToUpdateMap := map[string]types.OpenApiMetadataEntry{}
for newNamespacedKey, newEntry := range newMetadataEntries {
if oldEntry, ok := oldMetadataEntries[newNamespacedKey]; ok {
if reflect.DeepEqual(oldEntry, newEntry) {
continue
}
// If a metadata property that is not "Value" or "IsPersistent" is changed, it needs to be recreated
if oldEntry.IsReadOnly != newEntry.IsReadOnly || oldEntry.KeyValue.Namespace != newEntry.KeyValue.Namespace ||
oldEntry.KeyValue.Domain != newEntry.KeyValue.Domain || oldEntry.KeyValue.Value.Type != newEntry.KeyValue.Value.Type {
util.Logger.Printf("[DEBUG] entry with namespace '%s' and key '%s' is being deleted and re-created", oldEntry.KeyValue.Namespace, oldEntry.KeyValue.Key)
metadataToRemove = append(metadataToRemove, oldMetadataEntries[newNamespacedKey])
metadataToCreate = append(metadataToCreate, newMetadataEntries[newNamespacedKey])
} else {
// Only "Value" / "IsPersistent" is changed, it can be updated
metadataToUpdateMap[newNamespacedKey] = newEntry
}

}
}
var metadataToUpdate []types.OpenApiMetadataEntry
for _, v := range metadataToUpdateMap {
metadataToUpdate = append(metadataToUpdate, v)
}

for newNamespacedKey, newEntry := range newMetadataEntries {
_, alreadyExisting := oldMetadataEntries[newNamespacedKey]
_, beingUpdated := metadataToUpdateMap[newNamespacedKey]
if !alreadyExisting && !beingUpdated {
metadataToCreate = append(metadataToCreate, newEntry)
}
}

return metadataToCreate, metadataToUpdate, metadataToRemove, nil
}

// getOpenApiMetadataEntryMap converts the input metadata attribute from Terraform state to a map composed by metadata
// namespaced keys (this is, namespace and key separated by '%%%') and their values.
func getOpenApiMetadataEntryMap(metadataAttribute []interface{}) (map[string]types.OpenApiMetadataEntry, error) {
metadataMap := map[string]types.OpenApiMetadataEntry{}
for _, rawItem := range metadataAttribute {
metadataEntry := rawItem.(map[string]interface{})

namespace := ""
if _, ok := metadataEntry["namespace"]; ok {
namespace = metadataEntry["namespace"].(string)
}

value, err := convertOpenApiMetadataValue(metadataEntry["type"].(string), metadataEntry["value"].(string))
if err != nil {
return nil, fmt.Errorf("error parsing the 'value' attribute '%s' from state: %s", metadataEntry["value"].(string), err)
}

// In OpenAPI, metadata is namespaced, hence it is possible to have same keys but in different namespaces.
// For that reason, we use "namespace+key" to unequivocally identify the metadata entries.
namespacedKey := fmt.Sprintf("%s%s", namespace, metadataEntry["key"].(string))
if _, ok := metadataMap[namespacedKey]; ok {
return nil, fmt.Errorf("metadata entry with namespace '%s' and key '%s' already exists", namespace, metadataEntry["key"])
}

metadataMap[namespacedKey] = types.OpenApiMetadataEntry{
IsReadOnly: metadataEntry["readonly"].(bool), // It is always populated as it has a default value
IsPersistent: metadataEntry["persistent"].(bool), // It is always populated as it has a default value
KeyValue: types.OpenApiMetadataKeyValue{
Domain: metadataEntry["domain"].(string), // It is always populated as it has a default value
Key: metadataEntry["key"].(string), // It is always populated as it is required
Value: types.OpenApiMetadataTypedValue{
Value: value,
Type: metadataEntry["type"].(string), // It is always populated as it has a default value
},
Namespace: namespace,
},
}
}
return metadataMap, nil
}

// updateOpenApiMetadataInState updates metadata_entry in the Terraform state for the given receiver object.
// This can be done as both are Computed, for compatibility reasons.
func updateOpenApiMetadataInState(d *schema.ResourceData, receiverObject openApiMetadataCompatible) error {
allMetadata, err := receiverObject.GetMetadata()
if err != nil {
return err
}

metadata := make([]interface{}, len(allMetadata))
for i, metadataEntryFromVcd := range allMetadata {
// We need to set the correct type, otherwise saving the state will fail
value := ""
switch metadataEntryFromVcd.MetadataEntry.KeyValue.Value.Type {
case types.OpenApiMetadataBooleanEntry:
value = fmt.Sprintf("%t", metadataEntryFromVcd.MetadataEntry.KeyValue.Value.Value.(bool))
case types.OpenApiMetadataNumberEntry:
value = fmt.Sprintf("%.0f", metadataEntryFromVcd.MetadataEntry.KeyValue.Value.Value.(float64))
case types.OpenApiMetadataStringEntry:
value = metadataEntryFromVcd.MetadataEntry.KeyValue.Value.Value.(string)
default:
return fmt.Errorf("not supported metadata type %s", metadataEntryFromVcd.MetadataEntry.KeyValue.Value.Type)
}

metadataEntry := map[string]interface{}{
"id": metadataEntryFromVcd.MetadataEntry.ID,
"key": metadataEntryFromVcd.MetadataEntry.KeyValue.Key,
"readonly": metadataEntryFromVcd.MetadataEntry.IsReadOnly,
"domain": metadataEntryFromVcd.MetadataEntry.KeyValue.Domain,
"namespace": metadataEntryFromVcd.MetadataEntry.KeyValue.Namespace,
"type": metadataEntryFromVcd.MetadataEntry.KeyValue.Value.Type,
"value": value,
"persistent": metadataEntryFromVcd.MetadataEntry.IsPersistent,
}
metadata[i] = metadataEntry
}

err = d.Set("metadata_entry", metadata)
return err
}

// convertOpenApiMetadataValue converts a metadata value from plain string to a correct typed value that can be sent
// in OpenAPI payloads.
func convertOpenApiMetadataValue(valueType, value string) (interface{}, error) {
var convertedValue interface{}
var err error
switch valueType {
case types.OpenApiMetadataStringEntry:
convertedValue = value
case types.OpenApiMetadataNumberEntry:
convertedValue, err = strconv.ParseFloat(value, 64)
case types.OpenApiMetadataBooleanEntry:
convertedValue, err = strconv.ParseBool(value)
default:
return nil, fmt.Errorf("unrecognized metadata type %s", valueType)
}
if err != nil {
return nil, fmt.Errorf("error parsing the value '%v': %s", value, err)
}
return convertedValue, nil
}
Loading