From e3a288fbc5a175cd8f1dbdec52de7a0daf2609ea Mon Sep 17 00:00:00 2001 From: Marcus Weiner Date: Sat, 20 Jul 2024 17:08:57 +0200 Subject: [PATCH 1/7] Improve conversion helper --- internal/provider/bgpsession_resource.go | 12 ++++++------ internal/provider/utils.go | 7 +++++-- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/internal/provider/bgpsession_resource.go b/internal/provider/bgpsession_resource.go index f69492c..2515322 100644 --- a/internal/provider/bgpsession_resource.go +++ b/internal/provider/bgpsession_resource.go @@ -77,13 +77,13 @@ func (m *SessionResourceModel) ToAPIModel(ctx context.Context, diags diag.Diagno p.Status = &status } if !m.SiteID.IsNull() { - p.Site = toIntPointer(m.SiteID.ValueInt64()) + p.Site = toIntPointer(m.SiteID.ValueInt64Pointer()) } if !m.TenantID.IsNull() { - p.Tenant = toIntPointer(m.TenantID.ValueInt64()) + p.Tenant = toIntPointer(m.TenantID.ValueInt64Pointer()) } if !m.DeviceID.IsNull() { - p.Device = toIntPointer(m.DeviceID.ValueInt64()) + p.Device = toIntPointer(m.DeviceID.ValueInt64Pointer()) } if !m.LocalAddressID.IsNull() { p.LocalAddress = int(m.LocalAddressID.ValueInt64()) @@ -98,7 +98,7 @@ func (m *SessionResourceModel) ToAPIModel(ctx context.Context, diags diag.Diagno p.RemoteAs = int(m.RemoteASID.ValueInt64()) } if !m.PeerGroupID.IsNull() { - p.PeerGroup = toIntPointer(m.PeerGroupID.ValueInt64()) + p.PeerGroup = toIntPointer(m.PeerGroupID.ValueInt64Pointer()) } if !m.ImportPolicyIDs.IsNull() { policies, ds := toIntListPointer(ctx, m.ImportPolicyIDs) @@ -115,10 +115,10 @@ func (m *SessionResourceModel) ToAPIModel(ctx context.Context, diags diag.Diagno p.ExportPolicies = &policies } if !m.PrefixListInID.IsNull() { - p.PrefixListIn = toIntPointer(m.PrefixListInID.ValueInt64()) + p.PrefixListIn = toIntPointer(m.PrefixListInID.ValueInt64Pointer()) } if !m.PrefixListOutID.IsNull() { - p.PrefixListOut = toIntPointer(m.PrefixListOutID.ValueInt64()) + p.PrefixListOut = toIntPointer(m.PrefixListOutID.ValueInt64Pointer()) } p.Tags = TagsForAPIModel(ctx, m.Tags, diags) diff --git a/internal/provider/utils.go b/internal/provider/utils.go index 0323660..5bdc10c 100644 --- a/internal/provider/utils.go +++ b/internal/provider/utils.go @@ -12,8 +12,11 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" ) -func toIntPointer(from int64) *int { - val := int(from) +func toIntPointer(from *int64) *int { + if from == nil { + return nil + } + val := int(*from) return &val } From eb68ba15b8ecefe36f9b8aa9d46656075f440182 Mon Sep 17 00:00:00 2001 From: Marcus Weiner Date: Sat, 20 Jul 2024 17:38:35 +0200 Subject: [PATCH 2/7] Add session data source --- internal/provider/bgpsession_datasource.go | 220 ++++++++++++++ internal/provider/common.go | 335 +++++++++++++++++++++ internal/provider/provider.go | 4 +- internal/provider/utils.go | 14 + 4 files changed, 572 insertions(+), 1 deletion(-) create mode 100644 internal/provider/bgpsession_datasource.go create mode 100644 internal/provider/common.go diff --git a/internal/provider/bgpsession_datasource.go b/internal/provider/bgpsession_datasource.go new file mode 100644 index 0000000..029c027 --- /dev/null +++ b/internal/provider/bgpsession_datasource.go @@ -0,0 +1,220 @@ +package provider + +import ( + "context" + "fmt" + + "github.com/ffddorf/terraform-provider-netbox-bgp/client" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Ensure provider defined types fully satisfy framework interfaces. +var _ datasource.DataSource = &SessionDataSource{} + +func NewSessionDataSource() datasource.DataSource { + return &SessionDataSource{} +} + +type SessionDataSource struct { + client *client.Client +} + +type SessionDataSourceModel struct { + ID types.Int64 `tfsdk:"id"` + Name types.String `tfsdk:"name"` + Description types.String `tfsdk:"description"` + Comments types.String `tfsdk:"comments"` + Status types.String `tfsdk:"status"` + + Site NestedSite `tfsdk:"site"` + Tenant NestedTenant `tfsdk:"tenant"` + Device NestedDevice `tfsdk:"device"` + + LocalAddress NestedIPAddress `tfsdk:"local_address"` + RemoteAddress NestedIPAddress `tfsdk:"remote_address"` + LocalAS NestedASN `tfsdk:"local_as"` + RemoteAS NestedASN `tfsdk:"remote_as"` + PeerGroup NestedBGPPeerGroup `tfsdk:"peer_group"` + + ImportPolicyIDs types.List `tfsdk:"import_policy_ids"` + ExportPolicyIDs types.List `tfsdk:"export_policy_ids"` + + PrefixListIn NestedPrefixList `tfsdk:"prefix_list_in"` + PrefixListOut NestedPrefixList `tfsdk:"prefix_list_out"` + + Tags types.List `tfsdk:"tags"` +} + +func (m *SessionDataSourceModel) FillFromAPIModel(ctx context.Context, resp *client.BGPSession, diags diag.Diagnostics) { + if resp.Id != nil { + m.ID = types.Int64Value(int64(*resp.Id)) + } + if resp.Comments != nil && *resp.Comments != "" { + m.Comments = types.StringPointerValue(resp.Comments) + } + if resp.Description != nil && *resp.Description != "" { + m.Description = types.StringPointerValue(resp.Description) + } + m.Device.FillFromAPI(resp.Device) + if resp.ExportPolicies != nil && len(*resp.ExportPolicies) > 0 { + var ds diag.Diagnostics + m.ExportPolicyIDs, ds = types.ListValueFrom(ctx, types.Int64Type, resp.ExportPolicies) + for _, d := range ds { + diags.Append(diag.WithPath(path.Root("export_policy_ids"), d)) + } + } + if resp.ImportPolicies != nil && len(*resp.ImportPolicies) > 0 { + var ds diag.Diagnostics + m.ImportPolicyIDs, ds = types.ListValueFrom(ctx, types.Int64Type, resp.ImportPolicies) + for _, d := range ds { + diags.Append(diag.WithPath(path.Root("import_policy_ids"), d)) + } + } + m.LocalAddress.FillFromAPI(&resp.LocalAddress) + m.LocalAS.FillFromAPI(&resp.LocalAs) + if resp.Name != nil { + m.Name = types.StringPointerValue(resp.Name) + } + m.PeerGroup.FillFromAPI(resp.PeerGroup) + m.PrefixListIn.FillFromAPI(resp.PrefixListIn) + m.PrefixListOut.FillFromAPI(resp.PrefixListOut) + m.RemoteAddress.FillFromAPI(&resp.RemoteAddress) + m.RemoteAS.FillFromAPI(&resp.RemoteAs) + m.Site.FillFromAPI(resp.Site) + if resp.Status != nil { + m.Status = types.StringPointerValue((*string)(resp.Status.Value)) + } + m.Tenant.FillFromAPI(resp.Tenant) + + m.Tags = TagsFromAPI(ctx, resp.Tags, diags) + + // todo: custom fields +} + +func (d *SessionDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_session" +} + +func (d *SessionDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: "BGP Session data source", + + Attributes: map[string]schema.Attribute{ + "id": schema.Int64Attribute{ + MarkdownDescription: "ID of the resource in Netbox to use for lookup", + Required: true, + }, + "name": schema.StringAttribute{ + Computed: true, + }, + "description": schema.StringAttribute{ + Computed: true, + }, + "comments": schema.StringAttribute{ + Computed: true, + }, + "status": schema.StringAttribute{ + Computed: true, + MarkdownDescription: `One of: "active", "failed", "offline", "planned"`, + }, + "site": schema.SingleNestedAttribute{ + Computed: true, + Attributes: (*NestedSite)(nil).SchemaAttributes(), + }, + "tenant": schema.SingleNestedAttribute{ + Computed: true, + Attributes: (*NestedTenant)(nil).SchemaAttributes(), + }, + "device": schema.SingleNestedAttribute{ + Computed: true, + Attributes: (*NestedDevice)(nil).SchemaAttributes(), + }, + "local_address": schema.SingleNestedAttribute{ + Computed: true, + Attributes: (*NestedIPAddress)(nil).SchemaAttributes(), + }, + "remote_address": schema.SingleNestedAttribute{ + Computed: true, + Attributes: (*NestedIPAddress)(nil).SchemaAttributes(), + }, + "local_as": schema.SingleNestedAttribute{ + Computed: true, + Attributes: (*NestedASN)(nil).SchemaAttributes(), + }, + "remote_as": schema.SingleNestedAttribute{ + Computed: true, + Attributes: (*NestedASN)(nil).SchemaAttributes(), + }, + "peer_group": schema.SingleNestedAttribute{ + Computed: true, + Attributes: (*NestedBGPPeerGroup)(nil).SchemaAttributes(), + }, + "import_policy_ids": schema.ListAttribute{ + ElementType: types.Int64Type, + Computed: true, + }, + "export_policy_ids": schema.ListAttribute{ + ElementType: types.Int64Type, + Computed: true, + }, + "prefix_list_in": schema.SingleNestedAttribute{ + Computed: true, + Attributes: (*NestedPrefixList)(nil).SchemaAttributes(), + }, + "prefix_list_out": schema.SingleNestedAttribute{ + Computed: true, + Attributes: (*NestedPrefixList)(nil).SchemaAttributes(), + }, + TagFieldName: TagSchema, + }, + } +} + +func (d *SessionDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + // Prevent panic if the provider has not been configured. + if req.ProviderData == nil { + return + } + + data, ok := req.ProviderData.(*configuredProvider) + if !ok { + resp.Diagnostics.AddError( + "Unexpected Resource Configure Type", + fmt.Sprintf("Expected *configuredProvider, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + return + } + + d.client = data.Client +} + +func (d *SessionDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var data SessionDataSourceModel + + // Read Terraform configuration data into the model + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + + httpRes, err := d.client.PluginsBgpBgpsessionRetrieve(ctx, int(data.ID.ValueInt64())) + if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("failed to retrieve session: %s", err)) + return + } + res, err := client.ParsePluginsBgpSessionRetrieveResponse(httpRes) + if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("failed to parse session: %s", err)) + return + } + if res.JSON200 == nil { + resp.Diagnostics.AddError("Client Error", httpError(httpRes, res.Body)) + return + } + + data.FillFromAPIModel(ctx, res.JSON200, resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } +} diff --git a/internal/provider/common.go b/internal/provider/common.go new file mode 100644 index 0000000..e9e5565 --- /dev/null +++ b/internal/provider/common.go @@ -0,0 +1,335 @@ +package provider + +import ( + "github.com/ffddorf/terraform-provider-netbox-bgp/client" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +type NestedSite struct { + Display types.String `tfsdk:"display"` + ID types.Int64 `tfsdk:"id"` + Name types.String `tfsdk:"name"` + Slug types.String `tfsdk:"slug"` + URL types.String `tfsdk:"url"` +} + +type NestedASN struct { + ASN types.Int64 `tfsdk:"asn"` + Display types.String `tfsdk:"display"` + ID types.Int64 `tfsdk:"id"` + URL types.String `tfsdk:"url"` +} + +type NestedIPAddress struct { + Address types.String `tfsdk:"address"` + Display types.String `tfsdk:"display"` + Family types.Int64 `tfsdk:"family"` + ID types.Int64 `tfsdk:"id"` + URL types.String `tfsdk:"url"` +} + +type NestedDevice struct { + Display types.String `tfsdk:"display"` + ID types.Int64 `tfsdk:"id"` + Name types.String `tfsdk:"name"` + URL types.String `tfsdk:"url"` +} + +type NestedBGPPeerGroup struct { + Description types.String `tfsdk:"description"` + Display types.String `tfsdk:"display"` + ID types.Int64 `tfsdk:"id"` + Name types.String `tfsdk:"name"` + URL types.String `tfsdk:"url"` +} + +type NestedPrefixList struct { + Display types.String `tfsdk:"display"` + ID types.Int64 `tfsdk:"id"` + Name types.String `tfsdk:"name"` + URL types.String `tfsdk:"url"` +} + +type NestedTenant struct { + Display types.String `tfsdk:"display"` + ID types.Int64 `tfsdk:"id"` + Name types.String `tfsdk:"name"` + Slug types.String `tfsdk:"slug"` + URL types.String `tfsdk:"url"` +} + +func (tfo NestedSite) ToAPIModel() client.NestedSite { + return client.NestedSite{ + Id: toIntPointer(tfo.ID.ValueInt64Pointer()), + Url: tfo.URL.ValueStringPointer(), + Display: tfo.Display.ValueStringPointer(), + Name: tfo.Name.ValueString(), + Slug: tfo.Slug.ValueString(), + } +} + +func (tfo NestedASN) ToAPIModel() client.NestedASN { + return client.NestedASN{ + Id: toIntPointer(tfo.ID.ValueInt64Pointer()), + Url: tfo.URL.ValueStringPointer(), + Display: tfo.Display.ValueStringPointer(), + Asn: tfo.ASN.ValueInt64(), + } +} + +func (tfo NestedIPAddress) ToAPIModel() client.NestedIPAddress { + return client.NestedIPAddress{ + Id: toIntPointer(tfo.ID.ValueInt64Pointer()), + Url: tfo.URL.ValueStringPointer(), + Display: tfo.Display.ValueStringPointer(), + Family: toIntPointer(tfo.Family.ValueInt64Pointer()), + Address: tfo.Address.ValueString(), + } +} + +func (tfo NestedDevice) ToAPIModel() client.NestedDevice { + return client.NestedDevice{ + Id: toIntPointer(tfo.ID.ValueInt64Pointer()), + Url: tfo.URL.ValueStringPointer(), + Display: tfo.Display.ValueStringPointer(), + Name: tfo.Name.ValueStringPointer(), + } +} + +func (tfo NestedBGPPeerGroup) ToAPIModel() client.NestedBGPPeerGroup { + return client.NestedBGPPeerGroup{ + Id: toIntPointer(tfo.ID.ValueInt64Pointer()), + Url: tfo.URL.ValueStringPointer(), + Display: tfo.Display.ValueStringPointer(), + Name: tfo.Name.ValueString(), + Description: tfo.Description.ValueStringPointer(), + } +} + +func (tfo NestedPrefixList) ToAPIModel() client.NestedPrefixList { + return client.NestedPrefixList{ + Id: toIntPointer(tfo.ID.ValueInt64Pointer()), + Url: tfo.URL.ValueStringPointer(), + Display: tfo.Display.ValueStringPointer(), + Name: tfo.Name.ValueString(), + } +} + +func (tfo NestedTenant) ToAPIModel() client.NestedTenant { + return client.NestedTenant{ + Id: toIntPointer(tfo.ID.ValueInt64Pointer()), + Url: tfo.URL.ValueStringPointer(), + Display: tfo.Display.ValueStringPointer(), + Name: tfo.Name.ValueString(), + Slug: tfo.Slug.ValueString(), + } +} + +func (tfo *NestedSite) FillFromAPI(resp *client.NestedSite) { + if resp == nil { + return + } + tfo.ID = types.Int64Value(int64(*resp.Id)) + tfo.URL = maybeStringValue(resp.Url) + tfo.Display = maybeStringValue(resp.Display) + tfo.Name = types.StringValue(resp.Name) + tfo.Slug = types.StringValue(resp.Slug) +} + +func (tfo *NestedASN) FillFromAPI(resp *client.NestedASN) { + if resp == nil { + return + } + tfo.ID = types.Int64Value(int64(*resp.Id)) + tfo.URL = maybeStringValue(resp.Url) + tfo.Display = maybeStringValue(resp.Display) + tfo.ASN = types.Int64Value(resp.Asn) +} + +func (tfo *NestedIPAddress) FillFromAPI(resp *client.NestedIPAddress) { + if resp == nil { + return + } + tfo.ID = types.Int64Value(int64(*resp.Id)) + tfo.URL = maybeStringValue(resp.Url) + tfo.Display = maybeStringValue(resp.Display) + tfo.Family = maybeInt64Value(resp.Family) + tfo.Address = types.StringValue(resp.Address) +} + +func (tfo *NestedDevice) FillFromAPI(resp *client.NestedDevice) { + if resp == nil { + return + } + tfo.ID = types.Int64Value(int64(*resp.Id)) + tfo.URL = maybeStringValue(resp.Url) + tfo.Display = maybeStringValue(resp.Display) + tfo.Name = maybeStringValue(resp.Name) +} + +func (tfo *NestedBGPPeerGroup) FillFromAPI(resp *client.NestedBGPPeerGroup) { + if resp == nil { + return + } + tfo.ID = types.Int64Value(int64(*resp.Id)) + tfo.URL = maybeStringValue(resp.Url) + tfo.Display = maybeStringValue(resp.Display) + tfo.Name = types.StringValue(resp.Name) + tfo.Description = maybeStringValue(resp.Description) +} + +func (tfo *NestedPrefixList) FillFromAPI(resp *client.NestedPrefixList) { + if resp == nil { + return + } + tfo.ID = types.Int64Value(int64(*resp.Id)) + tfo.URL = maybeStringValue(resp.Url) + tfo.Display = maybeStringValue(resp.Display) + tfo.Name = types.StringValue(resp.Name) +} + +func (tfo *NestedTenant) FillFromAPI(resp *client.NestedTenant) { + if resp == nil { + return + } + tfo.ID = types.Int64Value(int64(*resp.Id)) + tfo.URL = maybeStringValue(resp.Url) + tfo.Display = maybeStringValue(resp.Display) + tfo.Name = types.StringValue(resp.Name) + tfo.Slug = types.StringValue(resp.Slug) +} + +func (*NestedSite) SchemaAttributes() map[string]schema.Attribute { + return map[string]schema.Attribute{ + "id": schema.Int64Attribute{ + Computed: true, + }, + "display": schema.StringAttribute{ + Computed: true, + }, + "url": schema.StringAttribute{ + Optional: true, + }, + "name": schema.StringAttribute{ + Required: true, + }, + "slug": schema.StringAttribute{ + Required: true, + }, + } +} + +func (*NestedASN) SchemaAttributes() map[string]schema.Attribute { + return map[string]schema.Attribute{ + "id": schema.Int64Attribute{ + Computed: true, + }, + "display": schema.StringAttribute{ + Computed: true, + }, + "url": schema.StringAttribute{ + Optional: true, + }, + "name": schema.StringAttribute{ + Required: true, + }, + "asn": schema.Int64Attribute{ + Required: true, + }, + } +} + +func (*NestedIPAddress) SchemaAttributes() map[string]schema.Attribute { + return map[string]schema.Attribute{ + "id": schema.Int64Attribute{ + Computed: true, + }, + "display": schema.StringAttribute{ + Computed: true, + }, + "url": schema.StringAttribute{ + Optional: true, + }, + "family": schema.Int64Attribute{ + Optional: true, + }, + "address": schema.StringAttribute{ + Required: true, + }, + } +} + +func (*NestedDevice) SchemaAttributes() map[string]schema.Attribute { + return map[string]schema.Attribute{ + "id": schema.Int64Attribute{ + Computed: true, + }, + "display": schema.StringAttribute{ + Computed: true, + }, + "url": schema.StringAttribute{ + Optional: true, + }, + "name": schema.StringAttribute{ + Optional: true, + }, + } +} + +func (*NestedBGPPeerGroup) SchemaAttributes() map[string]schema.Attribute { + return map[string]schema.Attribute{ + "id": schema.Int64Attribute{ + Computed: true, + }, + "display": schema.StringAttribute{ + Computed: true, + }, + "url": schema.StringAttribute{ + Optional: true, + }, + "name": schema.StringAttribute{ + Required: true, + }, + "description": schema.StringAttribute{ + Optional: true, + }, + } +} + +func (*NestedPrefixList) SchemaAttributes() map[string]schema.Attribute { + return map[string]schema.Attribute{ + "id": schema.Int64Attribute{ + Computed: true, + }, + "display": schema.StringAttribute{ + Computed: true, + }, + "url": schema.StringAttribute{ + Optional: true, + }, + "name": schema.StringAttribute{ + Required: true, + }, + } +} + +func (*NestedTenant) SchemaAttributes() map[string]schema.Attribute { + return map[string]schema.Attribute{ + "id": schema.Int64Attribute{ + Computed: true, + }, + "display": schema.StringAttribute{ + Computed: true, + }, + "url": schema.StringAttribute{ + Optional: true, + }, + "name": schema.StringAttribute{ + Required: true, + }, + "slug": schema.StringAttribute{ + Required: true, + }, + } +} diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 99c55cd..883b583 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -168,7 +168,9 @@ func (p *NetboxBGPProvider) Resources(ctx context.Context) []func() resource.Res } func (p *NetboxBGPProvider) DataSources(ctx context.Context) []func() datasource.DataSource { - return []func() datasource.DataSource{} + return []func() datasource.DataSource{ + NewSessionDataSource, + } } func New(version string) func() provider.Provider { diff --git a/internal/provider/utils.go b/internal/provider/utils.go index 5bdc10c..7f56d32 100644 --- a/internal/provider/utils.go +++ b/internal/provider/utils.go @@ -34,6 +34,20 @@ func toIntListPointer(ctx context.Context, from types.List) ([]int, diag.Diagnos return out, diags } +func maybeStringValue(in *string) types.String { + if in == nil { + return types.StringNull() + } + return types.StringPointerValue(in) +} + +func maybeInt64Value(in *int) types.Int64 { + if in == nil { + return types.Int64Null() + } + return types.Int64Value(int64(*in)) +} + func httpError(res *http.Response, body []byte) string { return fmt.Sprintf("Bad response: Status %d with content type \"%s\"\n%s", res.StatusCode, res.Header.Get("Content-Type"), string(body)) } From 862420f28620cfe839279e5555335a527c5a176e Mon Sep 17 00:00:00 2001 From: Marcus Weiner Date: Sat, 20 Jul 2024 20:14:28 +0200 Subject: [PATCH 3/7] Simplify model conversions --- internal/provider/bgpsession_resource.go | 92 ++++++++---------------- internal/provider/utils.go | 10 +++ 2 files changed, 38 insertions(+), 64 deletions(-) diff --git a/internal/provider/bgpsession_resource.go b/internal/provider/bgpsession_resource.go index 2515322..cef49ae 100644 --- a/internal/provider/bgpsession_resource.go +++ b/internal/provider/bgpsession_resource.go @@ -63,43 +63,21 @@ type SessionResourceModel struct { func (m *SessionResourceModel) ToAPIModel(ctx context.Context, diags diag.Diagnostics) client.WritableBGPSessionRequest { p := client.WritableBGPSessionRequest{} - if !m.Name.IsNull() { - p.Name = m.Name.ValueStringPointer() - } - if !m.Description.IsNull() { - p.Description = m.Description.ValueStringPointer() - } - if !m.Comments.IsNull() { - p.Comments = m.Comments.ValueStringPointer() - } + p.Name = m.Name.ValueStringPointer() + p.Description = m.Description.ValueStringPointer() + p.Comments = m.Comments.ValueStringPointer() if !m.Status.IsNull() { status := client.WritableBGPSessionRequestStatus(m.Status.ValueString()) p.Status = &status } - if !m.SiteID.IsNull() { - p.Site = toIntPointer(m.SiteID.ValueInt64Pointer()) - } - if !m.TenantID.IsNull() { - p.Tenant = toIntPointer(m.TenantID.ValueInt64Pointer()) - } - if !m.DeviceID.IsNull() { - p.Device = toIntPointer(m.DeviceID.ValueInt64Pointer()) - } - if !m.LocalAddressID.IsNull() { - p.LocalAddress = int(m.LocalAddressID.ValueInt64()) - } - if !m.RemoteAddressID.IsNull() { - p.RemoteAddress = int(m.RemoteAddressID.ValueInt64()) - } - if !m.LocalASID.IsNull() { - p.LocalAs = int(m.LocalASID.ValueInt64()) - } - if !m.RemoteASID.IsNull() { - p.RemoteAs = int(m.RemoteASID.ValueInt64()) - } - if !m.PeerGroupID.IsNull() { - p.PeerGroup = toIntPointer(m.PeerGroupID.ValueInt64Pointer()) - } + p.Site = fromInt64Value(m.SiteID) + p.Tenant = fromInt64Value(m.TenantID) + p.Device = fromInt64Value(m.DeviceID) + p.LocalAddress = *fromInt64Value(m.LocalAddressID) + p.RemoteAddress = *fromInt64Value(m.RemoteAddressID) + p.LocalAs = *fromInt64Value(m.LocalASID) + p.RemoteAs = *fromInt64Value(m.RemoteASID) + p.PeerGroup = fromInt64Value(m.PeerGroupID) if !m.ImportPolicyIDs.IsNull() { policies, ds := toIntListPointer(ctx, m.ImportPolicyIDs) for _, d := range ds { @@ -114,12 +92,8 @@ func (m *SessionResourceModel) ToAPIModel(ctx context.Context, diags diag.Diagno } p.ExportPolicies = &policies } - if !m.PrefixListInID.IsNull() { - p.PrefixListIn = toIntPointer(m.PrefixListInID.ValueInt64Pointer()) - } - if !m.PrefixListOutID.IsNull() { - p.PrefixListOut = toIntPointer(m.PrefixListOutID.ValueInt64Pointer()) - } + p.PrefixListIn = fromInt64Value(m.PrefixListInID) + p.PrefixListOut = fromInt64Value(m.PrefixListOutID) p.Tags = TagsForAPIModel(ctx, m.Tags, diags) @@ -129,17 +103,11 @@ func (m *SessionResourceModel) ToAPIModel(ctx context.Context, diags diag.Diagno } func (m *SessionResourceModel) FillFromAPIModel(ctx context.Context, resp *client.BGPSession, diags diag.Diagnostics) { - if resp.Id != nil { - m.ID = types.Int64Value(int64(*resp.Id)) - } - if resp.Comments != nil && *resp.Comments != "" { - m.Comments = types.StringPointerValue(resp.Comments) - } - if resp.Description != nil && *resp.Description != "" { - m.Description = types.StringPointerValue(resp.Description) - } + m.ID = maybeInt64Value(resp.Id) + m.Comments = maybeStringValue(resp.Comments) + m.Description = maybeStringValue(resp.Description) if resp.Device != nil { - m.DeviceID = types.Int64Value(int64(*resp.Device.Id)) + m.DeviceID = maybeInt64Value(resp.Device.Id) } if resp.ExportPolicies != nil && len(*resp.ExportPolicies) > 0 { var ds diag.Diagnostics @@ -155,30 +123,26 @@ func (m *SessionResourceModel) FillFromAPIModel(ctx context.Context, resp *clien diags.Append(diag.WithPath(path.Root("import_policy_ids"), d)) } } - m.LocalAddressID = types.Int64Value(int64(*resp.LocalAddress.Id)) - m.LocalASID = types.Int64Value(int64(*resp.LocalAs.Id)) - if resp.Name != nil { - m.Name = types.StringPointerValue(resp.Name) - } + m.LocalAddressID = maybeInt64Value(resp.LocalAddress.Id) + m.LocalASID = maybeInt64Value(resp.LocalAs.Id) + m.Name = maybeStringValue(resp.Name) if resp.PeerGroup != nil { - m.PeerGroupID = types.Int64Value(int64(*resp.PeerGroup.Id)) + m.PeerGroupID = maybeInt64Value(resp.PeerGroup.Id) } if resp.PrefixListIn != nil { - m.PrefixListInID = types.Int64Value(int64(*resp.PrefixListIn.Id)) + m.PrefixListInID = maybeInt64Value(resp.PrefixListIn.Id) } if resp.PrefixListOut != nil { - m.PrefixListOutID = types.Int64Value(int64(*resp.PrefixListOut.Id)) + m.PrefixListOutID = maybeInt64Value(resp.PrefixListOut.Id) } - m.RemoteAddressID = types.Int64Value(int64(*resp.RemoteAddress.Id)) - m.RemoteASID = types.Int64Value(int64(*resp.RemoteAs.Id)) + m.RemoteAddressID = maybeInt64Value(resp.RemoteAddress.Id) + m.RemoteASID = maybeInt64Value(resp.RemoteAs.Id) if resp.Site != nil { - m.SiteID = types.Int64Value(int64(*resp.Site.Id)) - } - if resp.Status != nil { - m.Status = types.StringPointerValue((*string)(resp.Status.Value)) + m.SiteID = maybeInt64Value(resp.Site.Id) } + m.Status = maybeStringValue((*string)(resp.Status.Value)) if resp.Tenant != nil { - m.TenantID = types.Int64Value(int64(*resp.Tenant.Id)) + m.TenantID = maybeInt64Value(resp.Tenant.Id) } m.Tags = TagsFromAPI(ctx, resp.Tags, diags) diff --git a/internal/provider/utils.go b/internal/provider/utils.go index 7f56d32..03a3710 100644 --- a/internal/provider/utils.go +++ b/internal/provider/utils.go @@ -38,6 +38,9 @@ func maybeStringValue(in *string) types.String { if in == nil { return types.StringNull() } + if *in == "" { + return types.StringNull() + } return types.StringPointerValue(in) } @@ -48,6 +51,13 @@ func maybeInt64Value(in *int) types.Int64 { return types.Int64Value(int64(*in)) } +func fromInt64Value(in types.Int64) *int { + if in.IsNull() { + return nil + } + return toIntPointer(in.ValueInt64Pointer()) +} + func httpError(res *http.Response, body []byte) string { return fmt.Sprintf("Bad response: Status %d with content type \"%s\"\n%s", res.StatusCode, res.Header.Get("Content-Type"), string(body)) } From 29da162548b34d9119430fcb8acffc0807857175 Mon Sep 17 00:00:00 2001 From: Marcus Weiner Date: Sat, 20 Jul 2024 20:14:47 +0200 Subject: [PATCH 4/7] Use pointers to nested structs --- internal/provider/bgpsession_datasource.go | 60 +++++++++------------- internal/provider/common.go | 42 ++++++++++----- 2 files changed, 53 insertions(+), 49 deletions(-) diff --git a/internal/provider/bgpsession_datasource.go b/internal/provider/bgpsession_datasource.go index 029c027..3695840 100644 --- a/internal/provider/bgpsession_datasource.go +++ b/internal/provider/bgpsession_datasource.go @@ -30,36 +30,30 @@ type SessionDataSourceModel struct { Comments types.String `tfsdk:"comments"` Status types.String `tfsdk:"status"` - Site NestedSite `tfsdk:"site"` - Tenant NestedTenant `tfsdk:"tenant"` - Device NestedDevice `tfsdk:"device"` + Site *NestedSite `tfsdk:"site"` + Tenant *NestedTenant `tfsdk:"tenant"` + Device *NestedDevice `tfsdk:"device"` - LocalAddress NestedIPAddress `tfsdk:"local_address"` - RemoteAddress NestedIPAddress `tfsdk:"remote_address"` - LocalAS NestedASN `tfsdk:"local_as"` - RemoteAS NestedASN `tfsdk:"remote_as"` - PeerGroup NestedBGPPeerGroup `tfsdk:"peer_group"` + LocalAddress *NestedIPAddress `tfsdk:"local_address"` + RemoteAddress *NestedIPAddress `tfsdk:"remote_address"` + LocalAS *NestedASN `tfsdk:"local_as"` + RemoteAS *NestedASN `tfsdk:"remote_as"` + PeerGroup *NestedBGPPeerGroup `tfsdk:"peer_group"` ImportPolicyIDs types.List `tfsdk:"import_policy_ids"` ExportPolicyIDs types.List `tfsdk:"export_policy_ids"` - PrefixListIn NestedPrefixList `tfsdk:"prefix_list_in"` - PrefixListOut NestedPrefixList `tfsdk:"prefix_list_out"` + PrefixListIn *NestedPrefixList `tfsdk:"prefix_list_in"` + PrefixListOut *NestedPrefixList `tfsdk:"prefix_list_out"` Tags types.List `tfsdk:"tags"` } func (m *SessionDataSourceModel) FillFromAPIModel(ctx context.Context, resp *client.BGPSession, diags diag.Diagnostics) { - if resp.Id != nil { - m.ID = types.Int64Value(int64(*resp.Id)) - } - if resp.Comments != nil && *resp.Comments != "" { - m.Comments = types.StringPointerValue(resp.Comments) - } - if resp.Description != nil && *resp.Description != "" { - m.Description = types.StringPointerValue(resp.Description) - } - m.Device.FillFromAPI(resp.Device) + m.ID = maybeInt64Value(resp.Id) + m.Comments = maybeStringValue(resp.Comments) + m.Description = maybeStringValue(resp.Description) + m.Device = NestedDeviceFromAPI(resp.Device) if resp.ExportPolicies != nil && len(*resp.ExportPolicies) > 0 { var ds diag.Diagnostics m.ExportPolicyIDs, ds = types.ListValueFrom(ctx, types.Int64Type, resp.ExportPolicies) @@ -74,21 +68,17 @@ func (m *SessionDataSourceModel) FillFromAPIModel(ctx context.Context, resp *cli diags.Append(diag.WithPath(path.Root("import_policy_ids"), d)) } } - m.LocalAddress.FillFromAPI(&resp.LocalAddress) - m.LocalAS.FillFromAPI(&resp.LocalAs) - if resp.Name != nil { - m.Name = types.StringPointerValue(resp.Name) - } - m.PeerGroup.FillFromAPI(resp.PeerGroup) - m.PrefixListIn.FillFromAPI(resp.PrefixListIn) - m.PrefixListOut.FillFromAPI(resp.PrefixListOut) - m.RemoteAddress.FillFromAPI(&resp.RemoteAddress) - m.RemoteAS.FillFromAPI(&resp.RemoteAs) - m.Site.FillFromAPI(resp.Site) - if resp.Status != nil { - m.Status = types.StringPointerValue((*string)(resp.Status.Value)) - } - m.Tenant.FillFromAPI(resp.Tenant) + m.LocalAddress = NestedIPAddressFromAPI(&resp.LocalAddress) + m.LocalAS = NestedASNFromAPI(&resp.LocalAs) + m.Name = maybeStringValue(resp.Name) + m.PeerGroup = NestedBGPPeerGroupFromAPI(resp.PeerGroup) + m.PrefixListIn = NestedPrefixListFromAPI(resp.PrefixListIn) + m.PrefixListOut = NestedPrefixListFromAPI(resp.PrefixListOut) + m.RemoteAddress = NestedIPAddressFromAPI(&resp.RemoteAddress) + m.RemoteAS = NestedASNFromAPI(&resp.RemoteAs) + m.Site = NestedSiteFromAPI(resp.Site) + m.Status = maybeStringValue((*string)(resp.Status.Value)) + m.Tenant = NestedTenantFromAPI(resp.Tenant) m.Tags = TagsFromAPI(ctx, resp.Tags, diags) diff --git a/internal/provider/common.go b/internal/provider/common.go index e9e5565..a04cfa0 100644 --- a/internal/provider/common.go +++ b/internal/provider/common.go @@ -126,78 +126,92 @@ func (tfo NestedTenant) ToAPIModel() client.NestedTenant { } } -func (tfo *NestedSite) FillFromAPI(resp *client.NestedSite) { +func NestedSiteFromAPI(resp *client.NestedSite) *NestedSite { if resp == nil { - return + return nil } + tfo := &NestedSite{} tfo.ID = types.Int64Value(int64(*resp.Id)) tfo.URL = maybeStringValue(resp.Url) tfo.Display = maybeStringValue(resp.Display) tfo.Name = types.StringValue(resp.Name) tfo.Slug = types.StringValue(resp.Slug) + return tfo } -func (tfo *NestedASN) FillFromAPI(resp *client.NestedASN) { +func NestedASNFromAPI(resp *client.NestedASN) *NestedASN { if resp == nil { - return + return nil } + tfo := &NestedASN{} tfo.ID = types.Int64Value(int64(*resp.Id)) tfo.URL = maybeStringValue(resp.Url) tfo.Display = maybeStringValue(resp.Display) tfo.ASN = types.Int64Value(resp.Asn) + return tfo } -func (tfo *NestedIPAddress) FillFromAPI(resp *client.NestedIPAddress) { +func NestedIPAddressFromAPI(resp *client.NestedIPAddress) *NestedIPAddress { if resp == nil { - return + return nil } + tfo := &NestedIPAddress{} tfo.ID = types.Int64Value(int64(*resp.Id)) tfo.URL = maybeStringValue(resp.Url) tfo.Display = maybeStringValue(resp.Display) tfo.Family = maybeInt64Value(resp.Family) tfo.Address = types.StringValue(resp.Address) + return tfo } -func (tfo *NestedDevice) FillFromAPI(resp *client.NestedDevice) { +func NestedDeviceFromAPI(resp *client.NestedDevice) *NestedDevice { if resp == nil { - return + return nil } + tfo := &NestedDevice{} tfo.ID = types.Int64Value(int64(*resp.Id)) tfo.URL = maybeStringValue(resp.Url) tfo.Display = maybeStringValue(resp.Display) tfo.Name = maybeStringValue(resp.Name) + return tfo } -func (tfo *NestedBGPPeerGroup) FillFromAPI(resp *client.NestedBGPPeerGroup) { +func NestedBGPPeerGroupFromAPI(resp *client.NestedBGPPeerGroup) *NestedBGPPeerGroup { if resp == nil { - return + return nil } + tfo := &NestedBGPPeerGroup{} tfo.ID = types.Int64Value(int64(*resp.Id)) tfo.URL = maybeStringValue(resp.Url) tfo.Display = maybeStringValue(resp.Display) tfo.Name = types.StringValue(resp.Name) tfo.Description = maybeStringValue(resp.Description) + return tfo } -func (tfo *NestedPrefixList) FillFromAPI(resp *client.NestedPrefixList) { +func NestedPrefixListFromAPI(resp *client.NestedPrefixList) *NestedPrefixList { if resp == nil { - return + return nil } + tfo := &NestedPrefixList{} tfo.ID = types.Int64Value(int64(*resp.Id)) tfo.URL = maybeStringValue(resp.Url) tfo.Display = maybeStringValue(resp.Display) tfo.Name = types.StringValue(resp.Name) + return tfo } -func (tfo *NestedTenant) FillFromAPI(resp *client.NestedTenant) { +func NestedTenantFromAPI(resp *client.NestedTenant) *NestedTenant { if resp == nil { - return + return nil } + tfo := &NestedTenant{} tfo.ID = types.Int64Value(int64(*resp.Id)) tfo.URL = maybeStringValue(resp.Url) tfo.Display = maybeStringValue(resp.Display) tfo.Name = types.StringValue(resp.Name) tfo.Slug = types.StringValue(resp.Slug) + return tfo } func (*NestedSite) SchemaAttributes() map[string]schema.Attribute { From 24a625d0b685ccfde6ff9e6a45f356e7caf8a871 Mon Sep 17 00:00:00 2001 From: Marcus Weiner Date: Sat, 20 Jul 2024 20:29:47 +0200 Subject: [PATCH 5/7] Ensure read attributes are set in the state --- internal/provider/bgpsession_datasource.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal/provider/bgpsession_datasource.go b/internal/provider/bgpsession_datasource.go index 3695840..3cacd96 100644 --- a/internal/provider/bgpsession_datasource.go +++ b/internal/provider/bgpsession_datasource.go @@ -207,4 +207,6 @@ func (d *SessionDataSource) Read(ctx context.Context, req datasource.ReadRequest if resp.Diagnostics.HasError() { return } + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) } From 745fe3437c79a972dd0264616c70e3a166b076d6 Mon Sep 17 00:00:00 2001 From: Marcus Weiner Date: Sat, 20 Jul 2024 20:30:10 +0200 Subject: [PATCH 6/7] Remove non-existent attribute from schema --- internal/provider/common.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/internal/provider/common.go b/internal/provider/common.go index a04cfa0..619c1ef 100644 --- a/internal/provider/common.go +++ b/internal/provider/common.go @@ -245,9 +245,6 @@ func (*NestedASN) SchemaAttributes() map[string]schema.Attribute { "url": schema.StringAttribute{ Optional: true, }, - "name": schema.StringAttribute{ - Required: true, - }, "asn": schema.Int64Attribute{ Required: true, }, From d67fdd906249847ccb10f7538f964e807cdadeab Mon Sep 17 00:00:00 2001 From: Marcus Weiner Date: Sat, 20 Jul 2024 20:30:27 +0200 Subject: [PATCH 7/7] Test data source --- .../provider/bgpsession_datasource_test.go | 41 +++++++++++++++++++ internal/provider/bgpsession_resource_test.go | 31 +++++++++----- 2 files changed, 62 insertions(+), 10 deletions(-) create mode 100644 internal/provider/bgpsession_datasource_test.go diff --git a/internal/provider/bgpsession_datasource_test.go b/internal/provider/bgpsession_datasource_test.go new file mode 100644 index 0000000..bb9fd7d --- /dev/null +++ b/internal/provider/bgpsession_datasource_test.go @@ -0,0 +1,41 @@ +package provider + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestAccSessionDataSource(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + ExternalProviders: testExternalProviders, + Steps: []resource.TestStep{ + // Read testing + { + Config: fmt.Sprintf(`%s + resource "netboxbgp_session" "test" { + name = "My session" + status = "active" + device_id = netbox_device.test.id + local_address_id = netbox_ip_address.local.id + remote_address_id = netbox_ip_address.remote.id + local_as_id = netbox_asn.test.id + remote_as_id = netbox_asn.test.id + } + + data "netboxbgp_session" "test" { + depends_on = [netboxbgp_session.test] + id = netboxbgp_session.test.id + } + `, baseResources(t)), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("data.netboxbgp_session.test", "name", "My session"), + resource.TestCheckResourceAttrPair("data.netboxbgp_session.test", "device.name", "netbox_device.test", "name"), + ), + }, + }, + }) +} diff --git a/internal/provider/bgpsession_resource_test.go b/internal/provider/bgpsession_resource_test.go index 6c38747..638d71c 100644 --- a/internal/provider/bgpsession_resource_test.go +++ b/internal/provider/bgpsession_resource_test.go @@ -2,6 +2,7 @@ package provider import ( "fmt" + "hash/fnv" "testing" "github.com/google/uuid" @@ -11,11 +12,26 @@ import ( "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" ) +var testExternalProviders = map[string]resource.ExternalProvider{ + "netbox": { + VersionConstraint: "~> 3.8.7", + Source: "registry.terraform.io/e-breuninger/netbox", + }, +} + func testName(t *testing.T) string { return t.Name() + "_" + uuid.NewString() } +func testNum(t *testing.T) uint64 { + h := fnv.New64() + fmt.Fprint(h, testName(t)) + return h.Sum64() +} + func baseResources(t *testing.T) string { + num := testNum(t) + shortNum := num % 250 return fmt.Sprintf(` resource "netbox_tag" "test" { name = "%[1]s" @@ -54,14 +70,14 @@ resource "netbox_device_interface" "test" { } resource "netbox_ip_address" "local" { - ip_address = "203.0.113.10/24" + ip_address = "203.0.113.%[2]d/24" status = "active" interface_id = netbox_device_interface.test.id object_type = "dcim.interface" } resource "netbox_ip_address" "remote" { - ip_address = "203.0.113.11/24" + ip_address = "203.0.113.%[3]d/24" status = "active" } @@ -70,21 +86,16 @@ resource "netbox_rir" "test" { } resource "netbox_asn" "test" { - asn = 1337 + asn = %[4]d rir_id = netbox_rir.test.id -}`, testName(t)) +}`, testName(t), shortNum, shortNum+1, shortNum+1337) } func TestAccSessionResource(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, - ExternalProviders: map[string]resource.ExternalProvider{ - "netbox": { - VersionConstraint: "~> 3.8.7", - Source: "registry.terraform.io/e-breuninger/netbox", - }, - }, + ExternalProviders: testExternalProviders, Steps: []resource.TestStep{ { Config: fmt.Sprintf(`%s