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)) }