generated from hashicorp/terraform-provider-scaffolding
/
util_terraform.go
200 lines (178 loc) · 7.95 KB
/
util_terraform.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
package provider
import (
"context"
"fmt"
"io"
"net/http"
"strings"
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
)
// UpdateTerraformObjectWithAttr adds an Attribute to a Terraform object
func UpdateTerraformObjectWithAttr(ctx context.Context, obj types.Object, key string, value attr.Value) (types.Object, diag.Diagnostics) {
attrTypes := obj.AttributeTypes(ctx)
attrValues := obj.Attributes()
attrValues[key] = value
return types.ObjectValue(attrTypes, attrValues)
}
func CompareTerraformAttrToString(attr attr.Value, s string) bool {
if sAttr, ok := attr.(types.String); ok {
return sAttr.ValueString() == s
}
return false
}
// MergeTerraformObjects combines two Terraform Objects replacing any null or unknown attribute values in `old` with
// matching attributes from `new`. Object type attributes are handled recursively to avoid overwriting existing
// nested attributes in the old Object. Full type information must be specified.
//
// The reason for this function is to handle situations where a remote resource was created but not all configuration
// was performed successfully. Instead of deleting the misconfigured resource, we can warn the user, and allow them
// to fix the configuration. In the case of Pulsar namespaces, it's possible that a namespace has been created, but
// not all of the policy configuration was completed successfully. If the user is warned of the issues, they can
// re-sync their remote state, and then decide how to proceed, either changing the configuration or deleting the namespace.
func MergeTerraformObjects(old, new types.Object, attributeTypes map[string]attr.Type) (types.Object, diag.Diagnostics) {
diags := diag.Diagnostics{}
if attributeTypes == nil {
diags.AddWarning("Failed to merge state objects", "No type information provided for object: "+old.String())
return old, diags
}
if old.IsNull() || old.IsUnknown() {
return basetypes.NewObjectValueMust(attributeTypes, new.Attributes()), diags
}
oldAttributes := old.Attributes()
newAttributes := new.Attributes()
attributes := map[string]attr.Value{}
for name, newValue := range newAttributes {
oldValue, ok := oldAttributes[name]
if !ok || oldValue.IsNull() || oldValue.IsUnknown() {
attributes[name] = newValue
continue
}
if oldObjValue, ok := oldValue.(types.Object); ok {
newObjValue, ok := newValue.(types.Object)
if !ok {
diags.AddWarning("Non matching types for attribute: "+name,
fmt.Sprintf("Existing object attribute can't be replaced with type `%v`", newValue.Type(context.Background()).String()))
continue
}
typeInfo, ok := attributeTypes[name].(types.ObjectType)
if !ok {
diags.AddWarning("Missing type information for attribute "+name, "No type information found when merging objects")
continue
}
newObjValue, mergeDiags := MergeTerraformObjects(oldObjValue, newObjValue, typeInfo.AttributeTypes())
diags.Append(mergeDiags...)
if diags.HasError() {
return old, diags
}
attributes[name] = newObjValue
continue
} else if _, ok := oldValue.(basetypes.MapValue); ok {
newMapValue, ok := newValue.(basetypes.MapValue)
if !ok {
diags.AddWarning("Missing type information for attribute "+name, "No type information found when merging objects")
continue
}
attributes[name] = newMapValue
continue
}
attributes[name] = oldValue
}
return basetypes.NewObjectValue(attributeTypes, attributes)
}
// HTTPResponseDiagErr takes an HTTP response and error and creates a Terraform Error Diagnostic if there is an error
func HTTPResponseDiagErr(resp *http.Response, err error, errorSummary string) diag.Diagnostics {
diags := diag.Diagnostics{}
if err != nil {
diags.AddError(errorSummary, err.Error())
} else if resp.StatusCode >= 300 {
bodyBytes, err := io.ReadAll(resp.Body)
if err != nil {
details := fmt.Sprintf("Received status code: '%v', with error: %s", resp.StatusCode, err.Error())
diags.AddError(errorSummary, details)
} else {
details := fmt.Sprintf("Received status code: '%v', with message: %s", resp.StatusCode, string(bodyBytes))
diags.AddError(errorSummary, details)
}
}
return diags
}
// HTTPResponseDiagErrWithBody takes an HTTP status code, body, and error and creates a Terraform Error Diagnostic if there is an error
func HTTPResponseDiagErrWithBody(statusCode int, body []byte, err error, errorSummary string) diag.Diagnostics {
diags := diag.Diagnostics{}
if err != nil {
diags.AddError(errorSummary, err.Error())
} else if statusCode >= 300 {
details := fmt.Sprintf("Received status code: '%v', with message: %s", statusCode, body)
diags.AddError(errorSummary, details)
}
return diags
}
// HTTPResponseDiagWarn takes an HTTP response and error and creates a Terraform Warn Diagnostic if there is an error
// or if the status code is not in the 2xx range
func HTTPResponseDiagWarn(resp *http.Response, err error, errorSummary string) diag.Diagnostics {
diags := diag.Diagnostics{}
if err != nil {
diags.AddWarning(errorSummary, err.Error())
} else if resp.StatusCode >= 300 {
bodyBytes, err := io.ReadAll(resp.Body)
if err != nil {
details := fmt.Sprintf("Received status code: '%v', with error: %s", resp.StatusCode, err.Error())
diags.AddWarning(errorSummary, details)
} else {
details := fmt.Sprintf("Received status code: '%v', with message: %s", resp.StatusCode, string(bodyBytes))
diags.AddWarning(errorSummary, details)
}
}
return diags
}
// HTTPResponseDiagWarnWithBody takes an HTTP status code, body, and error and creates a Terraform Error Diagnostic if there is an error
func HTTPResponseDiagWarnWithBody(statusCode int, body []byte, err error, errorSummary string) diag.Diagnostics {
diags := diag.Diagnostics{}
if err != nil {
diags.AddWarning(errorSummary, err.Error())
} else if statusCode >= 300 {
details := fmt.Sprintf("Received status code: '%v', with message: %s", statusCode, body)
diags.AddWarning(errorSummary, details)
}
return diags
}
// planModifierStringValueChanged is a terraform plan modifier function to use with 'RequiresReplaceIf' to check if a string value
// changed from one value to another, not including null values.
func planModifierStringValueChanged() stringplanmodifier.RequiresReplaceIfFunc {
return func(ctx context.Context, req planmodifier.StringRequest, resp *stringplanmodifier.RequiresReplaceIfFuncResponse) {
if !req.StateValue.IsNull() && !req.ConfigValue.IsNull() && !req.StateValue.Equal(req.ConfigValue) {
resp.RequiresReplace = true
}
}
}
// planModifierRemoveDashes returns the configured string with all dashes removed
func planModifierRemoveDashes() planmodifier.String {
return removeDashesModifier{}
}
// removeDashesModifier implements the plan modifier.
type removeDashesModifier struct{}
// Description returns a human-readable description of the plan modifier.
func (m removeDashesModifier) Description(_ context.Context) string {
return "Remove dashes from a string value"
}
// MarkdownDescription returns a markdown description of the plan modifier.
func (m removeDashesModifier) MarkdownDescription(_ context.Context) string {
return "Remove dashes from a string value"
}
// PlanModifyString implements the plan modification logic.
func (m removeDashesModifier) PlanModifyString(ctx context.Context, req planmodifier.StringRequest, resp *planmodifier.StringResponse) {
// Do nothing if there is no planned value.
if req.PlanValue.IsNull() || req.PlanValue.IsUnknown() {
return
}
// Do nothing if there is a no configuration value, otherwise interpolation gets messed up.
if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() {
return
}
resp.PlanValue = types.StringValue(strings.ReplaceAll(req.PlanValue.ValueString(), "-", ""))
}