Skip to content
Open
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
70 changes: 69 additions & 1 deletion mmv1/products/dataplex/EntryLink.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ description: |
EntryLink represents a link between two Entries.
references:
guides:
'Official Documentation': 'https://cloud.google.com/dataplex/docs' # Update with specific EntryLink docs when available
'Official Documentation': 'https://cloud.google.com/dataplex/docs' # Update with specific EntryLink docs when available
api: 'https://cloud.google.com/dataplex/docs/reference/rest/v1/projects.locations.entryGroups.entryLinks'
docs:
base_url: 'projects/{{project}}/locations/{{location}}/entryGroups/{{entry_group_id}}/entryLinks/{{entry_link_id}}'
Expand All @@ -26,6 +26,12 @@ create_url: 'projects/{{project}}/locations/{{location}}/entryGroups/{{entry_gro
immutable: true
import_format:
- 'projects/{{project}}/locations/{{location}}/entryGroups/{{entry_group_id}}/entryLinks/{{entry_link_id}}'
custom_code:
constants: templates/terraform/constants/dataplex_entry_link.go.tmpl
decoder: templates/terraform/decoders/dataplex_entry_link.go.tmpl
encoder: templates/terraform/encoders/dataplex_entry_link.go.tmpl
error_retry_predicates:
- 'transport_tpg.IsDataplex1PEntryNotFoundError'
timeouts:
insert_minutes: 20
delete_minutes: 20
Expand All @@ -42,6 +48,16 @@ examples:
entry_link_name: 'my_entry_link_full'
test_env_vars:
project_number: 'PROJECT_NUMBER'
- name: 'dataplex_entry_link_with_aspect'
primary_resource_id: 'full_entry_link_with_aspect'
primary_resource_name: 'fmt.Sprintf("tf-test-entry-link-full-with-aspect%s", context["random_suffix"])'
ignore_read_extra:
Copy link
Contributor

Choose a reason for hiding this comment

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

Why do we need this? The ffield should be readable... can you remove this

- 'aspects'
vars:
entry_link_name: 'my_entry_link_full_with_aspect'
test_env_vars:
project_number: 'PROJECT_NUMBER'
project_id: 'PROJECT_NAME'
parameters:
- name: 'entryGroupId'
type: String
Expand Down Expand Up @@ -109,3 +125,55 @@ properties:
enum_values:
- 'SOURCE'
- 'TARGET'
- name: 'aspects'
type: Array
custom_flatten: 'templates/terraform/custom_flatten/dataplex_entry_link_aspects.go.tmpl'
description: |
The Aspects attached to the Entry Link.
item_type:
type: NestedObject
properties:
- name: 'aspectKey'
type: String
required: true
description: |
The map keys of the Aspects which the service should modify.
It should be the aspect type reference in the format `{project_number}.{location_id}.{aspect_type_id}`.
- name: 'aspect'
type: NestedObject
required: true
properties:
- name: 'aspectType'
type: String
output: true
description: |
The resource name of the type used to create this Aspect.

- name: 'path'
type: String
output: true
description: |
The path in the entry link under which the aspect is attached.

- name: 'createTime'
type: Time
output: true
description: |
The time when the Aspect was created.

- name: 'updateTime'
type: Time
output: true
description: |
The time when the Aspect was last modified.

- name: 'data'
type: String
required: true
state_func: 'func(v interface{}) string { s, _ := structure.NormalizeJsonString(v); return s }'
custom_flatten: 'templates/terraform/custom_flatten/json_schema.tmpl'
custom_expand: 'templates/terraform/custom_expand/json_schema.tmpl'
validation:
function: 'validation.StringIsJSON'
description: |
The content of the aspect in JSON form, according to its aspect type schema. The maximum size of the field is 120KB (encoded as UTF-8).
152 changes: 152 additions & 0 deletions mmv1/templates/terraform/constants/dataplex_entry_link.go.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
var (
entryLinkProjectNumberRegex = regexp.MustCompile(`^projects\/[1-9]\d*\/.+$`)
)

// EntryLinkProjectNumberValidation checks if the input string conforms to the pattern:
// "projects/<project-number>/<anything>"
func EntryLinkProjectNumberValidation(i interface{}, k string) (warnings []string, errors []error) {
v, ok := i.(string)

if !ok {
errors = append(errors, fmt.Errorf("expected type of field %q to be string, but got %T", k, i))
return warnings, errors
}

if !entryLinkProjectNumberRegex.MatchString(v) {
errors = append(errors, fmt.Errorf(
"field %q has an invalid format: %q. Expected format: 'projects/<project-number>/<anything>'. Please note that project IDs are not supported.",
k, v,
))
}

return warnings, errors
}

// FilterEntryLinkAspects filters the aspects in res based on aspectKeySet.
// It returns an error if type assertions fail.
func FilterEntryLinkAspects(aspectKeySet map[string]struct{}, res map[string]interface{}) error {
aspectsRaw, ok := res["aspects"]
if !ok || aspectsRaw == nil {
return nil
}

aspectsMap, ok := aspectsRaw.(map[string]interface{})
if !ok {
return fmt.Errorf("FilterEntryLinkAspects: 'aspects' field is not a map[string]interface{}, got %T", aspectsRaw)
}

for key := range aspectsMap {
if _, keep := aspectKeySet[key]; !keep {
delete(aspectsMap, key)
}
}
return nil
}

// AddEntryLinkAspectsToSet adds aspect keys from the aspects interface to the aspectKeySet.
// It returns an error if type assertions fail or expected keys are missing.
func AddEntryLinkAspectsToSet(aspectKeySet map[string]struct{}, aspects interface{}) error {
if aspects == nil {
return nil
}
aspectsSlice, ok := aspects.([]interface{})
if !ok {
return fmt.Errorf("AddEntryLinkAspectsToSet: input 'aspects' is not a []interface{}, got %T", aspects)
}

for i, aspectItemRaw := range aspectsSlice {
aspectMap, ok := aspectItemRaw.(map[string]interface{})
if !ok {
return fmt.Errorf("AddEntryLinkAspectsToSet: item at index %d is not a map[string]interface{}, got %T", i, aspectItemRaw)
}

keyRaw, keyExists := aspectMap["aspect_key"]
if !keyExists {
return fmt.Errorf("AddEntryLinkAspectsToSet: 'aspect_key' not found in aspect item at index %d", i)
}

keyString, ok := keyRaw.(string)
if !ok {
return fmt.Errorf("AddEntryLinkAspectsToSet: 'aspect_key' in item at index %d is not a string, got %T", i, keyRaw)
}
aspectKeySet[keyString] = struct{}{}
}
return nil
}

// InverseTransformEntryLinkAspects converts the "aspects" map back to a slice of maps,
// re-inserting the "aspectKey". Modifies obj in-place.
// It returns an error if type assertions fail.
func InverseTransformEntryLinkAspects(res map[string]interface{}) error {
aspectsRaw, ok := res["aspects"]
if !ok || aspectsRaw == nil {
return nil
}

originalMap, ok := aspectsRaw.(map[string]interface{})
if !ok {
return fmt.Errorf("InverseTransformEntryLinkAspects: 'aspects' field is not a map[string]interface{}, got %T", aspectsRaw)
}

newSlice := make([]interface{}, 0, len(originalMap))

for key, value := range originalMap {
innerMap, ok := value.(map[string]interface{})
if !ok {
return fmt.Errorf("InverseTransformEntryLinkAspects: value for key '%s' is not a map[string]interface{}, got %T", key, value)
}
box := make(map[string]interface{}, 2)
box["aspectKey"] = key
box["aspect"] = innerMap
newSlice = append(newSlice, box)
}
res["aspects"] = newSlice
return nil
}

// TransformEntryLinkAspects concisely transforms the "aspects" slice within obj into a map.
// Modifies obj in-place.
// It returns an error if type assertions fail or expected keys are missing.
func TransformEntryLinkAspects(obj map[string]interface{}) error {
aspectsRaw, ok := obj["aspects"]
if !ok || aspectsRaw == nil {
return nil
}

originalSlice, ok := aspectsRaw.([]interface{})
if !ok {
return fmt.Errorf("TransformEntryLinkAspects: 'aspects' field is not a []interface{}, got %T", aspectsRaw)
}

newMap := make(map[string]interface{}, len(originalSlice))
for i, item := range originalSlice {
aspectMap, ok := item.(map[string]interface{})
if !ok {
return fmt.Errorf("TransformEntryLinkAspects: item in 'aspects' slice at index %d is not a map[string]interface{}, got %T", i, item)
}

keyRaw, keyExists := aspectMap["aspectKey"]
if !keyExists {
return fmt.Errorf("TransformEntryLinkAspects: 'aspectKey' not found in aspect item at index %d", i)
}
key, ok := keyRaw.(string)
if !ok {
return fmt.Errorf("TransformEntryLinkAspects: 'aspectKey' in item at index %d is not a string, got %T", i, keyRaw)
}

valueRaw, valueExists := aspectMap["aspect"]
if !valueExists {
newMap[key] = map[string]interface{}{"data": map[string]interface{}{}}
continue
}

value, ok := valueRaw.(map[string]interface{})
if ok {
newMap[key] = value
} else {
newMap[key] = map[string]interface{}{"data": map[string]interface{}{}}
}
}
obj["aspects"] = newMap
return nil
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// This file is a transposition of mmv1/templates/terraform/flatten_property_method.go.tmpl
// Most of the code is copied from there, with the exception of sorting logic.
func flatten{{$.GetPrefix}}{{$.TitlelizeProperty}}(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} {
if v == nil {
return v
}
l := v.([]interface{})
transformed := make([]map[string]interface{}, 0, len(l))
for _, raw := range l {
original := raw.(map[string]interface{})
if len(original) < 1 {
// Do not include empty json objects coming back from the api
continue
}
transformed = append(transformed, map[string]interface{}{

{{- range $prop := $.ItemType.UserProperties }}
{{- if not (or $prop.IgnoreRead $prop.WriteOnlyLegacy $prop.WriteOnly) }}
"{{ underscore $prop.Name }}": flatten{{$.GetPrefix}}{{$.TitlelizeProperty}}{{$prop.TitlelizeProperty}}(original["{{ $prop.ApiName }}"], d, config),
{{- end }}
{{- end }}
})
}

configData := []map[string]interface{}{}

for _, item := range d.Get("aspects").([]interface{}) {
configData = append(configData, item.(map[string]interface{}))
}

sorted, err := tpgresource.SortMapsByConfigOrder(configData, transformed, "aspect_key")
if err != nil {
log.Printf("[ERROR] Could not sort API response value: %s", err)
return v
}

return sorted
}

{{- if $.NestedProperties }}
{{- range $prop := $.NestedProperties }}
{{ template "flattenPropertyMethod" $prop -}}
{{- end }}
{{- end }}
31 changes: 31 additions & 0 deletions mmv1/templates/terraform/decoders/dataplex_entry_link.go.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
aspectKeysOfInterest := make(map[string]struct{})
var err error

if d.HasChange("aspects") {
currentAspects, futureAspects := d.GetChange("aspects")
err = AddEntryLinkAspectsToSet(aspectKeysOfInterest, currentAspects)
if err != nil {
return nil, err
}
err = AddEntryLinkAspectsToSet(aspectKeysOfInterest, futureAspects)
if err != nil {
return nil, err
}
} else {
err = AddEntryLinkAspectsToSet(aspectKeysOfInterest, d.Get("aspects"))
if err != nil {
return nil, err
}
}

err = FilterEntryLinkAspects(aspectKeysOfInterest, res)
if err != nil {
return nil, err
}

err = InverseTransformEntryLinkAspects(res)
if err != nil {
return nil, err
}

return res, nil
5 changes: 5 additions & 0 deletions mmv1/templates/terraform/encoders/dataplex_entry_link.go.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
if err := TransformEntryLinkAspects(obj); err != nil {
return nil, err
}

return obj, nil
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
resource "google_bigquery_dataset" "bq_dataset" {
dataset_id = "tf_test_dataset_%{random_suffix}"
project = "{{index $.TestEnvVars "project_number"}}"
location = "us-central1"
}

resource "google_bigquery_table" "table1" {
deletion_protection = false
dataset_id = google_bigquery_dataset.bq_dataset.dataset_id
table_id = "table1_%{random_suffix}"
project = "{{index $.TestEnvVars "project_number"}}"
schema = jsonencode([
{
name = "col1"
type = "STRING"
mode = "NULLABLE"
description = "Column 1"
}
])
}

resource "google_bigquery_table" "table2" {
deletion_protection = false
dataset_id = google_bigquery_dataset.bq_dataset.dataset_id
table_id = "table2_%{random_suffix}"
project = "{{index $.TestEnvVars "project_number"}}"
schema = jsonencode([
{
name = "colA"
type = "STRING"
mode = "NULLABLE"
description = "Column A"
}
])
}

resource "google_dataplex_entry_link" "full_entry_link_with_aspect" {
project = "{{index $.TestEnvVars "project_number"}}"
location = "us-central1"
entry_group_id = "@bigquery"
entry_link_id = "tf-test-full-entry-link%{random_suffix}"
entry_link_type = "projects/655216118709/locations/global/entryLinkTypes/schema-join"
entry_references {
name = "projects/%{project_number}/locations/us-central1/entryGroups/@bigquery/entries/bigquery.googleapis.com/projects/{{index $.TestEnvVars "project_id"}}/datasets/${google_bigquery_dataset.bq_dataset.dataset_id}/tables/${google_bigquery_table.table1.table_id}"
type = ""
}
entry_references {
name = "projects/%{project_number}/locations/us-central1/entryGroups/@bigquery/entries/bigquery.googleapis.com/projects/{{index $.TestEnvVars "project_id"}}/datasets/${google_bigquery_dataset.bq_dataset.dataset_id}/tables/${google_bigquery_table.table2.table_id}"
type = ""
}
aspects {
aspect_key = "655216118709.global.schema-join"
aspect {
data = jsonencode({
joins = []
userManaged = true
})
}
}
}
Loading
Loading