Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[minor_change] Introduction of a provider level flag to prevent creat…
Browse files Browse the repository at this point in the history
…ion of objects that are already existing in APIC configuration
akinross committed Jun 21, 2024
1 parent 1c74ae2 commit 7f78d42
Showing 63 changed files with 3,704 additions and 112 deletions.
5 changes: 5 additions & 0 deletions aci/provider.go
Original file line number Diff line number Diff line change
@@ -69,6 +69,11 @@ func Provider() *schema.Provider {
Optional: true,
Description: "Global annotation for the provider. This can also be set as the ACI_ANNOTATION environment variable.",
},
"allow_existing_on_create": &schema.Schema{
Type: schema.TypeBool,
Description: "Allow existing objects to be managed. This can also be set as the ACI_ALLOW_EXISTING_ON_CREATE environment variable.",
Optional: true,
},
},

ResourcesMap: map[string]*schema.Resource{
10 changes: 9 additions & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
@@ -136,5 +136,13 @@ NOTE: either 'password' OR 'private_key' and 'cert_name' must be provided for th
- `validate_relation_dn` (Boolean) Flag to validate if a object with entered relation Dn exists in the APIC.
- Default: `true`
- Environment variable: `ACI_VAL_REL_DN`
- `annotation` (String) Global annotation for the provider.
- `annotation` (String) Global annotation for the provider.
- Default: `orchestrator:terraform`
- Environment variable: `ACI_ANNOTATION`
- `allow_existing_on_create` (Boolean) Global flag to validate existing objects in APIC are allowed to be managed.
- Default: `true`
- Environment variable: `ACI_ALLOW_EXISTING_ON_CREATE`

~> The existence of a object in APIC can only be verified when the distinguished name (DN) can be constructed during plan. The verification <b>cannot</b> take place when a `parent_dn` attribute or any of the `naming` attributes ( attributes that are required to construct the DN ) of a resource are unknown ( known after apply ) during plan. An example of a unknown attribute input would be to use a DN reference ( `aci_tenant.example.id` ) to a resource that is being configured in the same plan. The DN of the object <b>cannot</b> be determined prior to apply and thus the existence of the object in APIC <b>cannot</b> be verified during plan. In these type cases the verification will be performed during the apply operation.

-> Setting this flag will introduce an additional API call during plan phase for newly defined resources in the configuration.
10 changes: 9 additions & 1 deletion gen/templates/index.md.tmpl
Original file line number Diff line number Diff line change
@@ -119,5 +119,13 @@ NOTE: either 'password' OR 'private_key' and 'cert_name' must be provided for th
- `validate_relation_dn` (Boolean) Flag to validate if a object with entered relation Dn exists in the APIC.
- Default: `true`
- Environment variable: `ACI_VAL_REL_DN`
- `annotation` (String) Global annotation for the provider.
- `annotation` (String) Global annotation for the provider.
- Default: `orchestrator:terraform`
- Environment variable: `ACI_ANNOTATION`
- `allow_existing_on_create` (Boolean) Global flag to validate existing objects in APIC are allowed to be managed.
- Default: `true`
- Environment variable: `ACI_ALLOW_EXISTING_ON_CREATE`

~> The existence of a object in APIC can only be verified when the distinguished name (DN) can be constructed during plan. The verification <b>cannot</b> take place when a `parent_dn` attribute or any of the `naming` attributes ( attributes that are required to construct the DN ) of a resource are unknown ( known after apply ) during plan. An example of a unknown attribute input would be to use a DN reference ( `aci_tenant.example.id` ) to a resource that is being configured in the same plan. The DN of the object <b>cannot</b> be determined prior to apply and thus the existence of the object in APIC <b>cannot</b> be verified during plan. In these type cases the verification will be performed during the apply operation.

-> Setting this flag will introduce an additional API call during plan phase for newly defined resources in the configuration.
28 changes: 28 additions & 0 deletions gen/templates/provider.go.tmpl
Original file line number Diff line number Diff line change
@@ -27,6 +27,7 @@ import (
)

var globalAnnotation string
var globalAllowExistingOnCreate bool

// Ensure AciProvider satisfies various provider interfaces.
var _ provider.Provider = &AciProvider{}
@@ -52,6 +53,7 @@ type AciProviderModel struct {
ValidateRelationDn types.String `tfsdk:"validate_relation_dn"`
MaxRetries types.String `tfsdk:"retries"`
Annotation types.String `tfsdk:"annotation"`
AllowExistingOnCreate types.Bool `tfsdk:"allow_existing_on_create"`
}

func (p *AciProvider) Metadata(ctx context.Context, req provider.MetadataRequest, resp *provider.MetadataResponse) {
@@ -123,6 +125,10 @@ func (p *AciProvider) Schema(ctx context.Context, req provider.SchemaRequest, re
Description: "Global annotation for the provider. This can also be set as the ACI_ANNOTATION environment variable.",
Optional: true,
},
"allow_existing_on_create": schema.BoolAttribute{
Description: "Allow existing objects to be managed. This can also be set as the ACI_ALLOW_EXISTING_ON_CREATE environment variable.",
Optional: true,
},
},
}
}
@@ -148,6 +154,7 @@ func (p *AciProvider) Configure(ctx context.Context, req provider.ConfigureReque
validateRelationDn := stringToBool(resp, "insecure", getStringAttribute(data.ValidateRelationDn, "ACI_VAL_REL_DN"), true)
maxRetries := stringToInt(resp, "retries", getStringAttribute(data.MaxRetries, "ACI_RETRIES"), 2)
setGlobalAnnotation(data.Annotation, "ACI_ANNOTATION")
setGlobalAllowExistingOnCreate(resp, data.AllowExistingOnCreate, "ACI_ALLOW_EXISTING_ON_CREATE")

if username == "" {
resp.Diagnostics.AddError(
@@ -224,6 +231,27 @@ func New(version string) func() provider.Provider {
}
}

func setGlobalAllowExistingOnCreate(resp *provider.ConfigureResponse, attribute basetypes.BoolValue, envKey string) {

if attribute.IsNull() {
envValue, found := os.LookupEnv(envKey)
if found {
boolValue, err := strconv.ParseBool(envValue)
if err != nil {
resp.Diagnostics.AddError(
fmt.Sprintf("Invalid input '%s'", envValue),
fmt.Sprintf("A boolean value must be provided for %s", envKey),
)
}
globalAllowExistingOnCreate = boolValue
} else {
globalAllowExistingOnCreate = true
}
} else {
globalAllowExistingOnCreate = attribute.ValueBool()
}
}

func setGlobalAnnotation(attribute basetypes.StringValue, envKey string) {

if attribute.IsNull() {
41 changes: 32 additions & 9 deletions gen/templates/resource.go.tmpl
Original file line number Diff line number Diff line change
@@ -448,18 +448,30 @@ func set{{ .ResourceClassName }}LegacyAttributes(ctx context.Context, diags *dia
}
{{- end }}
}
{{- end }}

func (r *{{.ResourceClassName}}Resource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
if !req.Plan.Raw.IsNull() {
var planData, stateData, configData *{{.ResourceClassName}}ResourceModel
var planData, stateData{{ if .LegacyAttributes}}, configData{{end}} *{{.ResourceClassName}}ResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &planData)...)
resp.Diagnostics.Append(req.State.Get(ctx, &stateData)...)
resp.Diagnostics.Append(req.Config.Get(ctx, &configData)...)
{{ if .LegacyAttributes}}resp.Diagnostics.Append(req.Config.Get(ctx, &configData)...){{end}}

if resp.Diagnostics.HasError() {
return
}

if stateData == nil && !globalAllowExistingOnCreate {{if .HasParent }}&& !planData.ParentDn.IsUnknown() {{end}}{{range .Properties}}{{if .IsNaming}}&& !planData.{{ .Name }}.IsUnknown() {{end}}{{end}}{
var createCheckData *{{.ResourceClassName}}ResourceModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &createCheckData)...)
set{{.ResourceClassName}}Id(ctx, createCheckData)
CheckDn(ctx, &resp.Diagnostics, r.client, "{{.PkgName}}", createCheckData.Id.ValueString())
if resp.Diagnostics.HasError() {
return
}
}

{{ if .LegacyAttributes}}
{{ range .LegacyAttributes}}{{$SetName := .Name}}
{{- if and (ne .ReplacedBy.AttributeName "") (eq (getMigrationType .ValueType) "String") (isNewAttributeStringType .ReplacedBy.AttributeName) }}
if !configData.{{ .Name }}.IsNull() {
@@ -706,11 +718,10 @@ func (r *{{.ResourceClassName}}Resource) ModifyPlan(ctx context.Context, req res
{{- end }}
}
{{ end }}

resp.Diagnostics.Append(resp.Plan.Set(ctx, &planData)...)
{{ end }}
}
}
{{- end}}

func (r *{{.ResourceClassName}}Resource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
tflog.Debug(ctx, "Start metadata of resource: aci_{{.ResourceName}}")
@@ -1011,6 +1022,13 @@ func (r *{{.ResourceClassName}}Resource) Create(ctx context.Context, req resourc
resp.Diagnostics.Append(req.Plan.Get(ctx, &stateData)...)
set{{.ResourceClassName}}Id(ctx, stateData)
getAndSet{{.ResourceClassName}}Attributes(ctx, &resp.Diagnostics, r.client, stateData)
if !globalAllowExistingOnCreate && !stateData.Id.IsNull() {
resp.Diagnostics.AddError(
"object already exists",
fmt.Sprintf("The {{.PkgName}} object with DN '%s' already exists", stateData.Id.ValueString()),
)
return
}
{{- end}}

var data *{{.ResourceClassName}}ResourceModel
@@ -1032,9 +1050,9 @@ func (r *{{.ResourceClassName}}Resource) Create(ctx context.Context, req resourc
data.{{ .ResourceClassName }}.ElementsAs(ctx, &{{.PkgName}}Plan, false)
stateData.{{ .ResourceClassName }}.ElementsAs(ctx, &{{.PkgName}}State, false)
{{- end}}
jsonPayload := get{{.ResourceClassName}}CreateJsonPayload(ctx, &resp.Diagnostics, data{{- range .Children}}, {{.PkgName}}Plan, {{.PkgName}}State{{- end}})
jsonPayload := get{{.ResourceClassName}}CreateJsonPayload(ctx, &resp.Diagnostics, true, data{{- range .Children}}, {{.PkgName}}Plan, {{.PkgName}}State{{- end}})
{{- else}}
jsonPayload := get{{.ResourceClassName}}CreateJsonPayload(ctx, &resp.Diagnostics, data)
jsonPayload := get{{.ResourceClassName}}CreateJsonPayload(ctx, &resp.Diagnostics, true, data)
{{- end}}

if resp.Diagnostics.HasError() {
@@ -1104,9 +1122,9 @@ func (r *{{.ResourceClassName}}Resource) Update(ctx context.Context, req resourc
data.{{ .ResourceClassName }}.ElementsAs(ctx, &{{.PkgName}}Plan, false)
stateData.{{ .ResourceClassName }}.ElementsAs(ctx, &{{.PkgName}}State, false)
{{- end}}
jsonPayload := get{{.ResourceClassName}}CreateJsonPayload(ctx, &resp.Diagnostics, data{{- range .Children}}, {{.PkgName}}Plan, {{.PkgName}}State{{- end}})
jsonPayload := get{{.ResourceClassName}}CreateJsonPayload(ctx, &resp.Diagnostics, false, data{{- range .Children}}, {{.PkgName}}Plan, {{.PkgName}}State{{- end}})
{{- else}}
jsonPayload := get{{.ResourceClassName}}CreateJsonPayload(ctx, &resp.Diagnostics, data)
jsonPayload := get{{.ResourceClassName}}CreateJsonPayload(ctx, &resp.Diagnostics, false, data)
{{- end}}

if resp.Diagnostics.HasError() {
@@ -1440,9 +1458,14 @@ func get{{$.ResourceClassName}}{{ .ResourceClassName }}ChildPayloads(ctx context
}
{{- end}}

func get{{.ResourceClassName}}CreateJsonPayload(ctx context.Context, diags *diag.Diagnostics, data *{{.ResourceClassName}}ResourceModel{{- range .Children}}, {{.PkgName}}Plan, {{.PkgName}}State []{{.ResourceClassName}}{{$.ResourceClassName}}ResourceModel{{- end}}) *container.Container {
func get{{.ResourceClassName}}CreateJsonPayload(ctx context.Context, diags *diag.Diagnostics, createType bool, data *{{.ResourceClassName}}ResourceModel{{- range .Children}}, {{.PkgName}}Plan, {{.PkgName}}State []{{.ResourceClassName}}{{$.ResourceClassName}}ResourceModel{{- end}}) *container.Container {
payloadMap := map[string]interface{}{}
payloadMap["attributes"] = map[string]string{}

if createType && !globalAllowExistingOnCreate {
payloadMap["attributes"].(map[string]string)["status"] = "created"
}

{{- if .HasChild}}
childPayloads := []map[string]interface{}{}
{{ range .Children}}
Loading

0 comments on commit 7f78d42

Please sign in to comment.