Skip to content

Commit

Permalink
Merge pull request #759 from Juniper/feat/758-freeform-blueprint
Browse files Browse the repository at this point in the history
Add Freeform Blueprint resource and data source
  • Loading branch information
chrismarget-j authored Aug 1, 2024
2 parents fbe9dc1 + a349f7c commit cee3ab9
Show file tree
Hide file tree
Showing 10 changed files with 648 additions and 0 deletions.
92 changes: 92 additions & 0 deletions apstra/data_source_freeform_blueprint.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package tfapstra

import (
"context"
"fmt"
"github.com/Juniper/terraform-provider-apstra/apstra/freeform"

"github.com/hashicorp/terraform-plugin-framework/types"

"github.com/Juniper/apstra-go-sdk/apstra"
"github.com/Juniper/terraform-provider-apstra/apstra/utils"
"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/path"
)

var (
_ datasource.DataSourceWithConfigure = &dataSourceFreeformBlueprint{}
_ datasourceWithSetClient = &dataSourceFreeformBlueprint{}
_ datasourceWithSetFfBpClientFunc = &dataSourceFreeformBlueprint{}
)

type dataSourceFreeformBlueprint struct {
client *apstra.Client
getBpClientFunc func(context.Context, string) (*apstra.FreeformClient, error)
}

func (o *dataSourceFreeformBlueprint) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_freeform_blueprint"
}

func (o *dataSourceFreeformBlueprint) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
configureDataSource(ctx, o, req, resp)
}

func (o *dataSourceFreeformBlueprint) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: docCategoryFreeform + "This data source looks up summary details of a Freeform Blueprint.\n\n" +
"At least one optional attribute is required.",
Attributes: freeform.Blueprint{}.DataSourceAttributes(),
}
}

func (o *dataSourceFreeformBlueprint) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
var config freeform.Blueprint
resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
if resp.Diagnostics.HasError() {
return
}

var err error
var apiData *apstra.BlueprintStatus

switch {
case !config.Name.IsNull():
apiData, err = o.client.GetBlueprintStatusByName(ctx, config.Name.ValueString())
if utils.IsApstra404(err) {
resp.Diagnostics.AddAttributeError(
path.Root("name"),
"Blueprint not found",
fmt.Sprintf("Blueprint with name %q not found", config.Name.ValueString()))
return
}
case !config.Id.IsNull():
apiData, err = o.client.GetBlueprintStatus(ctx, apstra.ObjectId(config.Id.ValueString()))
if utils.IsApstra404(err) {
resp.Diagnostics.AddAttributeError(
path.Root("name"),
"Blueprint not found",
fmt.Sprintf("Blueprint with ID %q not found", config.Id.ValueString()))
return
}
}
if err != nil { // catch errors other than 404 from above
resp.Diagnostics.AddError("Error retrieving Blueprint Status", err.Error())
return
}

config.Id = types.StringValue(apiData.Id.String())
config.Name = types.StringValue(apiData.Label)

// set state
resp.Diagnostics.Append(resp.State.Set(ctx, &config)...)
}

func (o *dataSourceFreeformBlueprint) setClient(client *apstra.Client) {
o.client = client
}

func (o *dataSourceFreeformBlueprint) setBpClientFunc(f func(context.Context, string) (*apstra.FreeformClient, error)) {
o.getBpClientFunc = f
}
1 change: 1 addition & 0 deletions apstra/export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ var (
ResourceDatacenterGenericSystem = resourceDatacenterGenericSystem{}
ResourceDatacenterIpLinkAddressing = resourceDatacenterIpLinkAddressing{}
ResourceDatacenterRoutingZone = resourceDatacenterRoutingZone{}
ResourceFreeformBlueprint = resourceFreeformBlueprint{}
ResourceFreeformConfigTemplate = resourceFreeformConfigTemplate{}
ResourceFreeformLink = resourceFreeformLink{}
ResourceFreeformPropertySet = resourceFreeformPropertySet{}
Expand Down
107 changes: 107 additions & 0 deletions apstra/freeform/blueprint.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package freeform

import (
"context"
"fmt"
"github.com/Juniper/apstra-go-sdk/apstra"
"github.com/Juniper/terraform-provider-apstra/apstra/constants"
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
dataSourceSchema "github.com/hashicorp/terraform-plugin-framework/datasource/schema"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/path"
resourceSchema "github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/types"
)

type Blueprint struct {
Id types.String `tfsdk:"id"`
Name types.String `tfsdk:"name"`
}

func (o Blueprint) DataSourceAttributes() map[string]dataSourceSchema.Attribute {
return map[string]dataSourceSchema.Attribute{
"id": dataSourceSchema.StringAttribute{
MarkdownDescription: "ID of the Blueprint. Required when `name` is omitted.",
Computed: true,
Optional: true,
Validators: []validator.String{
stringvalidator.LengthAtLeast(1),
stringvalidator.ExactlyOneOf(path.Expressions{
path.MatchRelative(),
path.MatchRoot("name"),
}...),
},
},
"name": dataSourceSchema.StringAttribute{
MarkdownDescription: "Name of the Blueprint. Required when `id` is omitted.",
Computed: true,
Optional: true,
Validators: []validator.String{stringvalidator.LengthAtLeast(1)},
},
}
}

func (o Blueprint) ResourceAttributes() map[string]resourceSchema.Attribute {
return map[string]resourceSchema.Attribute{
"id": resourceSchema.StringAttribute{
MarkdownDescription: "Blueprint ID assigned by Apstra.",
Computed: true,
PlanModifiers: []planmodifier.String{stringplanmodifier.UseStateForUnknown()},
},
"name": resourceSchema.StringAttribute{
MarkdownDescription: "Blueprint name.",
Required: true,
Validators: []validator.String{stringvalidator.LengthAtLeast(1)},
},
}
}

func (o *Blueprint) SetName(ctx context.Context, bpClient *apstra.FreeformClient, state *Blueprint, diags *diag.Diagnostics) {
if o.Name.Equal(state.Name) {
// nothing to do
return
}

// struct used for GET and PATCH
type node struct {
Label string `json:"label,omitempty"`
Id apstra.ObjectId `json:"id,omitempty"`
}

// GET target
response := &struct {
Nodes map[string]node `json:"nodes"`
}{}

err := bpClient.Client().GetNodes(ctx, bpClient.Id(), apstra.NodeTypeMetadata, response)
if err != nil {
diags.AddError(
fmt.Sprintf(constants.ErrApiGetWithTypeAndId, "Blueprint Node", bpClient.Id()),
err.Error(),
)
return
}
if len(response.Nodes) != 1 {
diags.AddError(fmt.Sprintf("wrong number of %s nodes", apstra.NodeTypeMetadata.String()),
fmt.Sprintf("expecting 1 got %d nodes", len(response.Nodes)))
return
}

// pull the only value from the map
var nodeId apstra.ObjectId
for _, v := range response.Nodes {
nodeId = v.Id
}

err = bpClient.Client().PatchNode(ctx, bpClient.Id(), nodeId, &node{Label: o.Name.ValueString()}, nil)
if err != nil {
diags.AddError(
fmt.Sprintf(constants.ErrApiGetWithTypeAndId, bpClient.Id(), nodeId),
err.Error(),
)
return
}
}
2 changes: 2 additions & 0 deletions apstra/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -550,6 +550,7 @@ func (p *Provider) DataSources(_ context.Context) []func() datasource.DataSource
func() datasource.DataSource { return &dataSourceDatacenterVirtualNetwork{} },
func() datasource.DataSource { return &dataSourceDatacenterVirtualNetworks{} },
func() datasource.DataSource { return &dataSourceDeviceConfig{} },
func() datasource.DataSource { return &dataSourceFreeformBlueprint{} },
func() datasource.DataSource { return &dataSourceFreeformConfigTemplate{} },
func() datasource.DataSource { return &dataSourceFreeformLink{} },
func() datasource.DataSource { return &dataSourceFreeformPropertySet{} },
Expand Down Expand Up @@ -610,6 +611,7 @@ func (p *Provider) Resources(_ context.Context) []func() resource.Resource {
func() resource.Resource { return &resourceDatacenterIpLinkAddressing{} },
func() resource.Resource { return &resourceDatacenterVirtualNetwork{} },
func() resource.Resource { return &resourceDeviceAllocation{} },
func() resource.Resource { return &resourceFreeformBlueprint{} },
func() resource.Resource { return &resourceFreeformConfigTemplate{} },
func() resource.Resource { return &resourceFreeformLink{} },
func() resource.Resource { return &resourceFreeformPropertySet{} },
Expand Down
171 changes: 171 additions & 0 deletions apstra/resource_freeform_blueprint.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
package tfapstra

import (
"context"
"fmt"
"github.com/Juniper/terraform-provider-apstra/apstra/freeform"

"github.com/Juniper/apstra-go-sdk/apstra"
"github.com/Juniper/terraform-provider-apstra/apstra/utils"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
)

var (
_ resource.ResourceWithConfigure = &resourceFreeformBlueprint{}
_ resourceWithSetClient = &resourceFreeformBlueprint{}
_ resourceWithSetFfBpClientFunc = &resourceFreeformBlueprint{}
_ resourceWithSetBpLockFunc = &resourceFreeformBlueprint{}
_ resourceWithSetBpUnlockFunc = &resourceFreeformBlueprint{}
)

type resourceFreeformBlueprint struct {
client *apstra.Client
getBpClientFunc func(context.Context, string) (*apstra.FreeformClient, error)
lockFunc func(context.Context, string) error
unlockFunc func(context.Context, string) error
}

func (o *resourceFreeformBlueprint) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_freeform_blueprint"
}

func (o *resourceFreeformBlueprint) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
configureResource(ctx, o, req, resp)
}

func (o *resourceFreeformBlueprint) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
MarkdownDescription: docCategoryFreeform + "This resource instantiates a Freeform Blueprint.",
Attributes: freeform.Blueprint{}.ResourceAttributes(),
}
}

func (o *resourceFreeformBlueprint) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
// Retrieve values from plan.
var plan freeform.Blueprint
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
if resp.Diagnostics.HasError() {
return
}

// Create the blueprint.
id, err := o.client.CreateFreeformBlueprint(ctx, plan.Name.ValueString())
if err != nil {
resp.Diagnostics.AddError("failed creating Blueprint", err.Error())
return
}

plan.Id = types.StringValue(id.String())

// Set state.
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
}

func (o *resourceFreeformBlueprint) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
// Get current state.
var state freeform.Blueprint
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}

// get a client for the freeform reference design
bp, err := o.getBpClientFunc(ctx, state.Id.ValueString())
if err != nil {
if utils.IsApstra404(err) {
resp.State.RemoveResource(ctx)
return
}
resp.Diagnostics.AddError("failed to create blueprint client", err.Error())
return
}

// Retrieve the blueprint status
apiData, err := bp.Client().GetBlueprintStatus(ctx, apstra.ObjectId(state.Id.ValueString()))
if err != nil {
// no 404 check or RemoveResource() here because Apstra's /api/blueprints
// endpoint may return bogus 404s due to race condition (?)
resp.Diagnostics.AddError(fmt.Sprintf("failed fetching blueprint %s status", state.Id), err.Error())
return
}

state.Name = types.StringValue(apiData.Label)

// Set state.
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
}

// Update resource
func (o *resourceFreeformBlueprint) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
// Retrieve plan.
var plan freeform.Blueprint
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
if resp.Diagnostics.HasError() {
return
}

// Retrieve state.
var state freeform.Blueprint
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}

// get a client for the freeform reference design
bp, err := o.getBpClientFunc(ctx, plan.Id.ValueString())
if err != nil {
resp.Diagnostics.AddError("failed to create blueprint client", err.Error())
return
}

// Update the blueprint name if necessary
plan.SetName(ctx, bp, &state, &resp.Diagnostics)
if resp.Diagnostics.HasError() {
return
}

// Set state
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
}

// Delete resource
func (o *resourceFreeformBlueprint) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var state freeform.Blueprint
diags := req.State.Get(ctx, &state)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}

// Delete the blueprint
err := o.client.DeleteBlueprint(ctx, apstra.ObjectId(state.Id.ValueString()))
if err != nil {
if !utils.IsApstra404(err) { // 404 is okay, but we do not return because we must unlock
resp.Diagnostics.AddError("error deleting Blueprint", err.Error())
}
}

// Unlock the blueprint mutex.
err = o.unlockFunc(ctx, state.Id.ValueString())
if err != nil {
resp.Diagnostics.AddError("error unlocking blueprint mutex", err.Error())
}
}

func (o *resourceFreeformBlueprint) setClient(client *apstra.Client) {
o.client = client
}

func (o *resourceFreeformBlueprint) setBpClientFunc(f func(context.Context, string) (*apstra.FreeformClient, error)) {
o.getBpClientFunc = f
}

func (o *resourceFreeformBlueprint) setBpLockFunc(f func(context.Context, string) error) {
o.lockFunc = f
}

func (o *resourceFreeformBlueprint) setBpUnlockFunc(f func(context.Context, string) error) {
o.unlockFunc = f
}
Loading

0 comments on commit cee3ab9

Please sign in to comment.