-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #759 from Juniper/feat/758-freeform-blueprint
Add Freeform Blueprint resource and data source
- Loading branch information
Showing
10 changed files
with
648 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
Oops, something went wrong.