diff --git a/cmd/gen-filters/main.go b/cmd/gen-filters/main.go new file mode 100644 index 0000000..92d4339 --- /dev/null +++ b/cmd/gen-filters/main.go @@ -0,0 +1,281 @@ +package main + +import ( + "bytes" + "fmt" + "go/format" + "go/types" + "io" + "os" + "regexp" + "slices" + "sort" + "strings" + "text/template" + + "golang.org/x/tools/go/packages" +) + +var tagRegex = regexp.MustCompile(`([a-z]+):"([^"]+)"\s*`) + +type opDef struct { + jsonName string + goFieldName string + goType types.Type +} + +type fieldDef struct { + goName string + goType types.Type + jsonName string + operators []opDef +} + +func resolveInnerType(t types.Type) (resolved types.Type, isPointer bool, isSlice bool) { + resolved = t + pointer, isPointer := resolved.(*types.Pointer) + if isPointer { + resolved = pointer.Elem() + } + slice, isSlice := resolved.(*types.Slice) + if isSlice { + resolved = slice.Elem() + } + return +} + +func addStringParser(w io.Writer, t types.Type) { + defer fmt.Fprintln(w) + + typeName := t.String() + switch typeName { + case "string": + fmt.Fprint(w, `v := value.ValueString()`) + return + case "time.Time": + fmt.Fprint(w, `v, err := time.Parse(time.RFC3339, value.ValueString())`) + case "bool": + fmt.Fprint(w, `v, err := strconv.ParseBool(value.ValueString())`) + case "int64": + fmt.Fprint(w, `v, err := strconv.ParseInt(value.ValueString(), 10, 64)`) + case "int32": + fmt.Fprint(w, `v64, err := strconv.ParseInt(value.ValueString(), 10, 32)`) + fmt.Fprintf(w, ` + if err != nil { + return diag.NewErrorDiagnostic( + "Value Parse Error", + fmt.Sprintf("failed to parse as %s value: %%s", value.ValueString()), + ) + }`, typeName) + fmt.Fprintln(w) + fmt.Fprint(w, `v := int32(v64)`) + return + case "int": + fmt.Fprint(w, `v, err := strconv.Atoi(value.ValueString())`) + case "github.com/google/uuid.UUID": + fmt.Fprint(w, `v, err := uuid.Parse(value.ValueString())`) + default: + panic(fmt.Errorf("unhandled type: %s", typeName)) + } + fmt.Fprintf(w, ` + if err != nil { + return diag.NewErrorDiagnostic( + "Value Parse Error", + fmt.Sprintf("failed to parse as %s value: %%s", value.ValueString()), + ) + }`, typeName) +} + +type wrapperOpts struct { + Logic string + ShortName string + FullName string + + Fields []string + Operators []string +} + +var wrapper = template.Must(template.New("wrapper"). + Funcs(template.FuncMap{"StringsJoin": strings.Join}). + Parse(`// Generated code. DO NOT EDIT! +package provider + +var ( + {{ .ShortName }}Fields = []string{"{{ StringsJoin .Fields "\",\"" }}"} + {{ .ShortName }}Operators = []string{"{{ StringsJoin .Operators "\",\"" }}"} +) + +func set{{ .ShortName }}FromFilter(filter Filter, params *client.{{ .FullName }}) diag.Diagnostic { + name := filter.Name.ValueString() + op := filter.Operator.ValueString() + if op == "" { + op = "eq" + } + value := filter.Value + {{ .Logic }} + return nil +} +`)) + +// these have a special meaning and are set manually. +var specialFields = []string{"limit", "offset", "ordering"} + +func main() { + paramsName := os.Args[1] + + cfg := &packages.Config{ + Mode: packages.NeedTypes, + } + pkgs, err := packages.Load(cfg, "github.com/ffddorf/terraform-provider-netbox-bgp/client") + if err != nil { + panic(err) + } + pkg := pkgs[0] + scope := pkg.Types.Scope() + + paramsObj := scope.Lookup(paramsName) + if paramsObj == nil { + panic(fmt.Errorf("type not found: %s", paramsName)) + } + + paramsType := paramsObj.(*types.TypeName) //nolint:forcetypeassert + paramsNamed := paramsType.Type().(*types.Named) //nolint:forcetypeassert + params := paramsNamed.Underlying().(*types.Struct) //nolint:forcetypeassert + fields := make(map[string]*fieldDef) + for i := range params.NumFields() { + f := params.Field(i) + tag := params.Tag(i) + tagKVs := tagRegex.FindAllStringSubmatch(tag, -1) + for _, m := range tagKVs { + if m[1] == "json" { + jsonBaseName, _, _ := strings.Cut(m[2], ",") + jsonName, op, hasSuffix := strings.Cut(jsonBaseName, "__") + if !hasSuffix { + op = "eq" + jsonName = jsonBaseName + } + def, ok := fields[jsonName] + if !ok { + def = &fieldDef{ + jsonName: jsonName, + } + fields[jsonName] = def + } + if !hasSuffix { + // update field def + def.goName = f.Name() + def.goType = f.Type() + } + def.operators = append(def.operators, opDef{ + jsonName: op, + goFieldName: f.Name(), + goType: f.Type(), + }) + break + } + } + } + + var output strings.Builder + output.WriteString(`switch name {`) + output.WriteByte('\n') + + fieldList := make([]*fieldDef, 0, len(fields)) + for _, field := range fields { + fieldList = append(fieldList, field) + } + slices.SortFunc(fieldList, func(a, b *fieldDef) int { + return strings.Compare(a.jsonName, b.jsonName) + }) + + allFields := make([]string, 0, len(fieldList)) + operatorSet := make(map[string]struct{}) + for _, field := range fieldList { + if slices.Contains(specialFields, field.jsonName) { + continue + } + + allFields = append(allFields, field.jsonName) + + fmt.Fprintf(&output, `case "%s":`, field.jsonName) + output.WriteByte('\n') + + fieldType, _, _ := resolveInnerType(field.goType) + if fieldType == nil { + fmt.Printf("type on %s resolved to no type: %#v\n", field.jsonName, field.goType) + continue + } + addStringParser(&output, fieldType) + + output.WriteString(`switch op {`) + output.WriteByte('\n') + + for _, op := range field.operators { + operatorSet[op.jsonName] = struct{}{} + + fmt.Fprintf(&output, `case "%s":`, op.jsonName) + output.WriteByte('\n') + + opType, isPointer, isSlice := resolveInnerType(op.goType) + if fieldType != opType { + addStringParser(&output, opType) + } + + fmt.Fprintf(&output, `params.%s = `, op.goFieldName) + if isSlice { + if isPointer { + output.WriteString("appendPointerSlice(") + } else { + output.WriteString("append(") + } + fmt.Fprintf(&output, "params.%s, v)", op.goFieldName) + } else { + if isPointer { + output.WriteString("&v") + } else { + output.WriteString("v") + } + } + + output.WriteByte('\n') + } + + output.WriteString(` default: + return unexpectedOperator(op, name) + }`) + output.WriteByte('\n') + } + + output.WriteString(` default: + return diag.NewErrorDiagnostic( + "Unexpected filter name", + fmt.Sprintf("Did not recognize field name: %s", name), + ) + }`) + + allOperators := make([]string, 0, len(operatorSet)) + for op := range operatorSet { + allOperators = append(allOperators, op) + } + sort.Strings(allOperators) + + shortName := strings.TrimPrefix(paramsName, "PluginsBgp") + var wrapped bytes.Buffer + err = wrapper.Execute(&wrapped, wrapperOpts{ + Logic: output.String(), + ShortName: shortName, + FullName: paramsName, + Fields: allFields, + Operators: allOperators, + }) + if err != nil { + panic(err) + } + + formatted, err := format.Source(wrapped.Bytes()) + if err != nil { + panic(err) + } + + _, _ = io.Copy(os.Stdout, bytes.NewReader(formatted)) +} diff --git a/docs/data-sources/sessions.md b/docs/data-sources/sessions.md new file mode 100644 index 0000000..bfab916 --- /dev/null +++ b/docs/data-sources/sessions.md @@ -0,0 +1,177 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "netboxbgp_sessions Data Source - netboxbgp" +subcategory: "" +description: |- + Data source to query for multiple BGP sessions by arbitrary parameters +--- + +# netboxbgp_sessions (Data Source) + +Data source to query for multiple BGP sessions by arbitrary parameters + + + + +## Schema + +### Optional + +- `filters` (Attributes List) (see [below for nested schema](#nestedatt--filters)) +- `limit` (Number) +- `ordering` (String) + +### Read-Only + +- `sessions` (List of Object) (see [below for nested schema](#nestedatt--sessions)) + + +### Nested Schema for `filters` + +Required: + +- `name` (String) +- `value` (String) + +Optional: + +- `operator` (String) + + + +### Nested Schema for `sessions` + +Read-Only: + +- `comments` (String) +- `description` (String) +- `device` (Object) (see [below for nested schema](#nestedobjatt--sessions--device)) +- `export_policy_ids` (List of Number) +- `id` (Number) +- `import_policy_ids` (List of Number) +- `local_address` (Object) (see [below for nested schema](#nestedobjatt--sessions--local_address)) +- `local_as` (Object) (see [below for nested schema](#nestedobjatt--sessions--local_as)) +- `name` (String) +- `peer_group` (Object) (see [below for nested schema](#nestedobjatt--sessions--peer_group)) +- `prefix_list_in` (Object) (see [below for nested schema](#nestedobjatt--sessions--prefix_list_in)) +- `prefix_list_out` (Object) (see [below for nested schema](#nestedobjatt--sessions--prefix_list_out)) +- `remote_address` (Object) (see [below for nested schema](#nestedobjatt--sessions--remote_address)) +- `remote_as` (Object) (see [below for nested schema](#nestedobjatt--sessions--remote_as)) +- `site` (Object) (see [below for nested schema](#nestedobjatt--sessions--site)) +- `status` (String) +- `tags` (List of String) +- `tenant` (Object) (see [below for nested schema](#nestedobjatt--sessions--tenant)) + + +### Nested Schema for `sessions.device` + +Read-Only: + +- `display` (String) +- `id` (Number) +- `name` (String) +- `url` (String) + + + +### Nested Schema for `sessions.local_address` + +Read-Only: + +- `address` (String) +- `display` (String) +- `family` (Number) +- `id` (Number) +- `url` (String) + + + +### Nested Schema for `sessions.local_as` + +Read-Only: + +- `asn` (Number) +- `display` (String) +- `id` (Number) +- `url` (String) + + + +### Nested Schema for `sessions.peer_group` + +Read-Only: + +- `description` (String) +- `display` (String) +- `id` (Number) +- `name` (String) +- `url` (String) + + + +### Nested Schema for `sessions.prefix_list_in` + +Read-Only: + +- `display` (String) +- `id` (Number) +- `name` (String) +- `url` (String) + + + +### Nested Schema for `sessions.prefix_list_out` + +Read-Only: + +- `display` (String) +- `id` (Number) +- `name` (String) +- `url` (String) + + + +### Nested Schema for `sessions.remote_address` + +Read-Only: + +- `address` (String) +- `display` (String) +- `family` (Number) +- `id` (Number) +- `url` (String) + + + +### Nested Schema for `sessions.remote_as` + +Read-Only: + +- `asn` (Number) +- `display` (String) +- `id` (Number) +- `url` (String) + + + +### Nested Schema for `sessions.site` + +Read-Only: + +- `display` (String) +- `id` (Number) +- `name` (String) +- `slug` (String) +- `url` (String) + + + +### Nested Schema for `sessions.tenant` + +Read-Only: + +- `display` (String) +- `id` (Number) +- `name` (String) +- `slug` (String) +- `url` (String) diff --git a/go.mod b/go.mod index d5bc501..22181e7 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,7 @@ require ( github.com/oapi-codegen/oapi-codegen/v2 v2.3.0 github.com/oapi-codegen/runtime v1.1.1 github.com/sethvargo/go-envconfig v1.1.0 + golang.org/x/tools v0.23.0 ) require ( @@ -82,12 +83,11 @@ require ( go.abhg.dev/goldmark/frontmatter v0.2.0 // indirect golang.org/x/crypto v0.25.0 // indirect golang.org/x/exp v0.0.0-20230809150735-7b3493d9a819 // indirect - golang.org/x/mod v0.17.0 // indirect - golang.org/x/net v0.25.0 // indirect + golang.org/x/mod v0.19.0 // indirect + golang.org/x/net v0.27.0 // indirect golang.org/x/sync v0.7.0 // indirect golang.org/x/sys v0.22.0 // indirect golang.org/x/text v0.16.0 // indirect - golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect google.golang.org/appengine v1.6.8 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de // indirect google.golang.org/grpc v1.63.2 // indirect diff --git a/go.sum b/go.sum index c4c14cc..d626633 100644 --- a/go.sum +++ b/go.sum @@ -246,6 +246,8 @@ golang.org/x/exp v0.0.0-20230809150735-7b3493d9a819/go.mod h1:FXUEEKJgO7OQYeo8N0 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8= +golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= @@ -253,6 +255,8 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= +golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -289,6 +293,8 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg= +golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= diff --git a/internal/provider/bgpsession_datasource.go b/internal/provider/bgpsession_datasource.go index 3cacd96..bf28eaa 100644 --- a/internal/provider/bgpsession_datasource.go +++ b/internal/provider/bgpsession_datasource.go @@ -8,7 +8,6 @@ import ( "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" ) @@ -40,8 +39,8 @@ type SessionDataSourceModel struct { RemoteAS *NestedASN `tfsdk:"remote_as"` PeerGroup *NestedBGPPeerGroup `tfsdk:"peer_group"` - ImportPolicyIDs types.List `tfsdk:"import_policy_ids"` - ExportPolicyIDs types.List `tfsdk:"export_policy_ids"` + ImportPolicyIDs []types.Int64 `tfsdk:"import_policy_ids"` + ExportPolicyIDs []types.Int64 `tfsdk:"export_policy_ids"` PrefixListIn *NestedPrefixList `tfsdk:"prefix_list_in"` PrefixListOut *NestedPrefixList `tfsdk:"prefix_list_out"` @@ -54,18 +53,14 @@ func (m *SessionDataSourceModel) FillFromAPIModel(ctx context.Context, resp *cli 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) - for _, d := range ds { - diags.Append(diag.WithPath(path.Root("export_policy_ids"), d)) + if resp.ExportPolicies != nil { + for _, id := range *resp.ExportPolicies { + m.ExportPolicyIDs = append(m.ExportPolicyIDs, types.Int64Value(int64(id))) } } 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)) + for _, id := range *resp.ImportPolicies { + m.ImportPolicyIDs = append(m.ImportPolicyIDs, types.Int64Value(int64(id))) } } m.LocalAddress = NestedIPAddressFromAPI(&resp.LocalAddress) @@ -89,78 +84,79 @@ func (d *SessionDataSource) Metadata(ctx context.Context, req datasource.Metadat resp.TypeName = req.ProviderTypeName + "_session" } +var sessionDataSchema = 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) 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, - }, + Attributes: sessionDataSchema, } } diff --git a/internal/provider/bgpsession_resource_test.go b/internal/provider/bgpsession_resource_test.go index 638d71c..913da1f 100644 --- a/internal/provider/bgpsession_resource_test.go +++ b/internal/provider/bgpsession_resource_test.go @@ -29,6 +29,12 @@ func testNum(t *testing.T) uint64 { return h.Sum64() } +func testIP(t *testing.T, offset uint64) string { + num := testNum(t) + shortNum := num % 250 + return fmt.Sprintf("203.0.113.%d", shortNum+offset) +} + func baseResources(t *testing.T) string { num := testNum(t) shortNum := num % 250 @@ -70,14 +76,14 @@ resource "netbox_device_interface" "test" { } resource "netbox_ip_address" "local" { - ip_address = "203.0.113.%[2]d/24" + ip_address = "%[2]s/24" status = "active" interface_id = netbox_device_interface.test.id object_type = "dcim.interface" } resource "netbox_ip_address" "remote" { - ip_address = "203.0.113.%[3]d/24" + ip_address = "%[3]s/24" status = "active" } @@ -88,7 +94,7 @@ resource "netbox_rir" "test" { resource "netbox_asn" "test" { asn = %[4]d rir_id = netbox_rir.test.id -}`, testName(t), shortNum, shortNum+1, shortNum+1337) +}`, testName(t), testIP(t, 0), testIP(t, 1), shortNum+1337) } func TestAccSessionResource(t *testing.T) { diff --git a/internal/provider/bgpsessions_datasource.go b/internal/provider/bgpsessions_datasource.go new file mode 100644 index 0000000..b185962 --- /dev/null +++ b/internal/provider/bgpsessions_datasource.go @@ -0,0 +1,164 @@ +package provider + +import ( + "context" + "fmt" + "net/http" + + "github.com/ffddorf/terraform-provider-netbox-bgp/client" + "github.com/hashicorp/terraform-plugin-framework/attr" + "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" +) + +//go:generate sh -c "go run github.com/ffddorf/terraform-provider-netbox-bgp/cmd/gen-filters PluginsBgpBgpsessionListParams > bgpsessions_filters.gen.go && go run golang.org/x/tools/cmd/goimports -w bgpsessions_filters.gen.go" + +// Ensure provider defined types fully satisfy framework interfaces. +var _ datasource.DataSource = &SessionsDataSource{} + +func NewSessionsDataSource() datasource.DataSource { + return &SessionsDataSource{} +} + +type SessionsDataSource struct { + client *client.Client +} + +type SessionsDataSourceModel struct { + Filters Filters `tfsdk:"filters"` + Limit types.Int64 `tfsdk:"limit"` + Ordering types.String `tfsdk:"ordering"` + Sessions []SessionDataSourceModel `tfsdk:"sessions"` +} + +func (d *SessionsDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_sessions" +} + +func (d *SessionsDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { + sessionAttrs := map[string]attr.Type{} + for attrName, attrSchema := range sessionDataSchema { + sessionAttrs[attrName] = attrSchema.GetType() + } + + resp.Schema = schema.Schema{ + MarkdownDescription: "Data source to query for multiple BGP sessions by arbitrary parameters", + Attributes: map[string]schema.Attribute{ + "filters": schema.ListNestedAttribute{ + NestedObject: FiltersSchema(BgpsessionListParamsFields, BgpsessionListParamsOperators), + Optional: true, + }, + "limit": schema.Int64Attribute{ + Optional: true, + }, + "ordering": schema.StringAttribute{ + Optional: true, + }, + "sessions": schema.ListAttribute{ + ElementType: types.ObjectType{ + AttrTypes: sessionAttrs, + }, + Computed: true, + }, + }, + } +} + +func (d *SessionsDataSource) 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 unexpectedOperator(op, name string) diag.Diagnostic { + return diag.NewErrorDiagnostic( + "Unexpected operator", + fmt.Sprintf(`The operator "%s" does not work with the field name "%s"`, op, name), + ) +} + +func (d *SessionsDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var data SessionsDataSourceModel + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + // construct filters + var params client.PluginsBgpBgpsessionListParams + for i, filter := range data.Filters { + if d := setBgpsessionListParamsFromFilter(filter, ¶ms); d != nil { + resp.Diagnostics.Append(diag.WithPath(path.Root("filters").AtListIndex(i), d)) + } + } + if resp.Diagnostics.HasError() { + return + } + + params.Limit = fromInt64Value(data.Limit) + params.Ordering = data.Ordering.ValueStringPointer() + + nextHTTPReq, err := client.NewPluginsBgpBgpsessionListRequest(d.client.Server, ¶ms) + for nextHTTPReq != nil { + if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("failed to create session list request: %s", err)) + return + } + + var httpRes *http.Response + httpRes, err = doPlainReq(ctx, nextHTTPReq, d.client) + if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("failed to retrieve sessions: %s", err)) + return + } + var res *client.PluginsBgpBgpsessionListResponse + res, err = client.ParsePluginsBgpBgpsessionListResponse(httpRes) + if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("failed to parse sessions: %s", err)) + return + } + if res.JSON200 == nil { + resp.Diagnostics.AddError("Client Error", httpError(httpRes, res.Body)) + return + } + if res.JSON200.Results != nil { + for _, sess := range *res.JSON200.Results { + m := SessionDataSourceModel{} + m.FillFromAPIModel(ctx, &sess, resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + data.Sessions = append(data.Sessions, m) + } + } + + // if there was a limit configured, only return elements up to the limit + if !data.Limit.IsNull() && len(data.Sessions) >= int(data.Limit.ValueInt64()) { + break + } + + if res.JSON200.Next == nil || *res.JSON200.Next == "" { + break + } + + // handle pagination, query next results + nextHTTPReq, err = http.NewRequest(http.MethodGet, *res.JSON200.Next, nil) + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} diff --git a/internal/provider/bgpsessions_datasource_test.go b/internal/provider/bgpsessions_datasource_test.go new file mode 100644 index 0000000..3fd3b81 --- /dev/null +++ b/internal/provider/bgpsessions_datasource_test.go @@ -0,0 +1,119 @@ +package provider + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +) + +func testSessions(t *testing.T) string { + return fmt.Sprintf(`%s + resource "netboxbgp_session" "test1" { + name = "Session 1" + 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 + } + + resource "netbox_ip_address" "remote2" { + ip_address = "%s/24" + status = "active" + } + + resource "netboxbgp_session" "test2" { + name = "Session 2" + status = "planned" + device_id = netbox_device.test.id + local_address_id = netbox_ip_address.local.id + remote_address_id = netbox_ip_address.remote2.id + local_as_id = netbox_asn.test.id + remote_as_id = netbox_asn.test.id + } + + resource "netbox_ip_address" "remote3" { + ip_address = "%s/24" + status = "active" + } + + resource "netboxbgp_session" "test3" { + name = "Session 3" + status = "active" + device_id = netbox_device.test.id + local_address_id = netbox_ip_address.local.id + remote_address_id = netbox_ip_address.remote3.id + local_as_id = netbox_asn.test.id + remote_as_id = netbox_asn.test.id + }`, baseResources(t), testIP(t, 2), testIP(t, 3)) +} + +func TestAccSessionsDataSource(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 + data "netboxbgp_sessions" "test_active" { + depends_on = [ + netboxbgp_session.test1, + netboxbgp_session.test2, + netboxbgp_session.test3, + ] + + filters = [ + { name: "status", value: "active" } + ] + + ordering = "name" + } + + data "netboxbgp_sessions" "test_limit" { + depends_on = [ + netboxbgp_session.test1, + netboxbgp_session.test2, + netboxbgp_session.test3, + ] + + limit = 2 + ordering = "name" + } + `, testSessions(t)), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue( + "data.netboxbgp_sessions.test_active", + tfjsonpath.New("sessions"), + knownvalue.ListExact([]knownvalue.Check{ + knownvalue.ObjectPartial(map[string]knownvalue.Check{ + "name": knownvalue.StringExact("Session 1"), + }), + knownvalue.ObjectPartial(map[string]knownvalue.Check{ + "name": knownvalue.StringExact("Session 3"), + }), + }), + ), + statecheck.ExpectKnownValue( + "data.netboxbgp_sessions.test_limit", + tfjsonpath.New("sessions"), + knownvalue.ListExact([]knownvalue.Check{ + knownvalue.ObjectPartial(map[string]knownvalue.Check{ + "name": knownvalue.StringExact("Session 1"), + }), + knownvalue.ObjectPartial(map[string]knownvalue.Check{ + "name": knownvalue.StringExact("Session 2"), + }), + }), + ), + }, + }, + }, + }) +} diff --git a/internal/provider/bgpsessions_filters.gen.go b/internal/provider/bgpsessions_filters.gen.go new file mode 100644 index 0000000..09fbc40 --- /dev/null +++ b/internal/provider/bgpsessions_filters.gen.go @@ -0,0 +1,508 @@ +// Generated code. DO NOT EDIT! +package provider + +import ( + "fmt" + "strconv" + "time" + + "github.com/ffddorf/terraform-provider-netbox-bgp/client" + "github.com/google/uuid" + "github.com/hashicorp/terraform-plugin-framework/diag" +) + +var ( + BgpsessionListParamsFields = []string{"by_local_address", "by_remote_address", "created", "created_by_request", "description", "device", "device_id", "export_policies", "id", "import_policies", "last_updated", "local_address", "local_address_id", "local_as", "local_as_id", "modified_by_request", "name", "peer_group", "q", "remote_address", "remote_address_id", "remote_as", "remote_as_id", "site", "site_id", "status", "tag", "tenant", "updated_by_request"} + BgpsessionListParamsOperators = []string{"empty", "eq", "gt", "gte", "ic", "ie", "iew", "isw", "lt", "lte", "n", "nic", "nie", "niew", "nisw"} +) + +func setBgpsessionListParamsFromFilter(filter Filter, params *client.PluginsBgpBgpsessionListParams) diag.Diagnostic { + name := filter.Name.ValueString() + op := filter.Operator.ValueString() + if op == "" { + op = "eq" + } + value := filter.Value + switch name { + case "by_local_address": + v := value.ValueString() + switch op { + case "eq": + params.ByLocalAddress = &v + default: + return unexpectedOperator(op, name) + } + case "by_remote_address": + v := value.ValueString() + switch op { + case "eq": + params.ByRemoteAddress = &v + default: + return unexpectedOperator(op, name) + } + case "created": + v, err := time.Parse(time.RFC3339, value.ValueString()) + if err != nil { + return diag.NewErrorDiagnostic( + "Value Parse Error", + fmt.Sprintf("failed to parse as time.Time value: %s", value.ValueString()), + ) + } + switch op { + case "eq": + params.Created = appendPointerSlice(params.Created, v) + case "empty": + params.CreatedEmpty = appendPointerSlice(params.CreatedEmpty, v) + case "gt": + params.CreatedGt = appendPointerSlice(params.CreatedGt, v) + case "gte": + params.CreatedGte = appendPointerSlice(params.CreatedGte, v) + case "lt": + params.CreatedLt = appendPointerSlice(params.CreatedLt, v) + case "lte": + params.CreatedLte = appendPointerSlice(params.CreatedLte, v) + case "n": + params.CreatedN = appendPointerSlice(params.CreatedN, v) + default: + return unexpectedOperator(op, name) + } + case "created_by_request": + v, err := uuid.Parse(value.ValueString()) + if err != nil { + return diag.NewErrorDiagnostic( + "Value Parse Error", + fmt.Sprintf("failed to parse as github.com/google/uuid.UUID value: %s", value.ValueString()), + ) + } + switch op { + case "eq": + params.CreatedByRequest = &v + default: + return unexpectedOperator(op, name) + } + case "description": + v := value.ValueString() + switch op { + case "eq": + params.Description = appendPointerSlice(params.Description, v) + case "empty": + v, err := strconv.ParseBool(value.ValueString()) + if err != nil { + return diag.NewErrorDiagnostic( + "Value Parse Error", + fmt.Sprintf("failed to parse as bool value: %s", value.ValueString()), + ) + } + params.DescriptionEmpty = &v + case "ic": + params.DescriptionIc = appendPointerSlice(params.DescriptionIc, v) + case "ie": + params.DescriptionIe = appendPointerSlice(params.DescriptionIe, v) + case "iew": + params.DescriptionIew = appendPointerSlice(params.DescriptionIew, v) + case "isw": + params.DescriptionIsw = appendPointerSlice(params.DescriptionIsw, v) + case "n": + params.DescriptionN = appendPointerSlice(params.DescriptionN, v) + case "nic": + params.DescriptionNic = appendPointerSlice(params.DescriptionNic, v) + case "nie": + params.DescriptionNie = appendPointerSlice(params.DescriptionNie, v) + case "niew": + params.DescriptionNiew = appendPointerSlice(params.DescriptionNiew, v) + case "nisw": + params.DescriptionNisw = appendPointerSlice(params.DescriptionNisw, v) + default: + return unexpectedOperator(op, name) + } + case "device": + v := value.ValueString() + switch op { + case "eq": + params.Device = appendPointerSlice(params.Device, v) + case "n": + params.DeviceN = appendPointerSlice(params.DeviceN, v) + default: + return unexpectedOperator(op, name) + } + case "device_id": + v, err := strconv.Atoi(value.ValueString()) + if err != nil { + return diag.NewErrorDiagnostic( + "Value Parse Error", + fmt.Sprintf("failed to parse as int value: %s", value.ValueString()), + ) + } + switch op { + case "eq": + params.DeviceId = appendPointerSlice(params.DeviceId, v) + case "n": + params.DeviceIdN = appendPointerSlice(params.DeviceIdN, v) + default: + return unexpectedOperator(op, name) + } + case "export_policies": + v, err := strconv.Atoi(value.ValueString()) + if err != nil { + return diag.NewErrorDiagnostic( + "Value Parse Error", + fmt.Sprintf("failed to parse as int value: %s", value.ValueString()), + ) + } + switch op { + case "eq": + params.ExportPolicies = appendPointerSlice(params.ExportPolicies, v) + case "n": + params.ExportPoliciesN = appendPointerSlice(params.ExportPoliciesN, v) + default: + return unexpectedOperator(op, name) + } + case "id": + v64, err := strconv.ParseInt(value.ValueString(), 10, 32) + if err != nil { + return diag.NewErrorDiagnostic( + "Value Parse Error", + fmt.Sprintf("failed to parse as int32 value: %s", value.ValueString()), + ) + } + v := int32(v64) + switch op { + case "eq": + params.Id = appendPointerSlice(params.Id, v) + case "empty": + v, err := strconv.ParseBool(value.ValueString()) + if err != nil { + return diag.NewErrorDiagnostic( + "Value Parse Error", + fmt.Sprintf("failed to parse as bool value: %s", value.ValueString()), + ) + } + params.IdEmpty = &v + case "gt": + params.IdGt = appendPointerSlice(params.IdGt, v) + case "gte": + params.IdGte = appendPointerSlice(params.IdGte, v) + case "lt": + params.IdLt = appendPointerSlice(params.IdLt, v) + case "lte": + params.IdLte = appendPointerSlice(params.IdLte, v) + case "n": + params.IdN = appendPointerSlice(params.IdN, v) + default: + return unexpectedOperator(op, name) + } + case "import_policies": + v, err := strconv.Atoi(value.ValueString()) + if err != nil { + return diag.NewErrorDiagnostic( + "Value Parse Error", + fmt.Sprintf("failed to parse as int value: %s", value.ValueString()), + ) + } + switch op { + case "eq": + params.ImportPolicies = appendPointerSlice(params.ImportPolicies, v) + case "n": + params.ImportPoliciesN = appendPointerSlice(params.ImportPoliciesN, v) + default: + return unexpectedOperator(op, name) + } + case "last_updated": + v, err := time.Parse(time.RFC3339, value.ValueString()) + if err != nil { + return diag.NewErrorDiagnostic( + "Value Parse Error", + fmt.Sprintf("failed to parse as time.Time value: %s", value.ValueString()), + ) + } + switch op { + case "eq": + params.LastUpdated = appendPointerSlice(params.LastUpdated, v) + case "empty": + params.LastUpdatedEmpty = appendPointerSlice(params.LastUpdatedEmpty, v) + case "gt": + params.LastUpdatedGt = appendPointerSlice(params.LastUpdatedGt, v) + case "gte": + params.LastUpdatedGte = appendPointerSlice(params.LastUpdatedGte, v) + case "lt": + params.LastUpdatedLt = appendPointerSlice(params.LastUpdatedLt, v) + case "lte": + params.LastUpdatedLte = appendPointerSlice(params.LastUpdatedLte, v) + case "n": + params.LastUpdatedN = appendPointerSlice(params.LastUpdatedN, v) + default: + return unexpectedOperator(op, name) + } + case "local_address": + v := value.ValueString() + switch op { + case "eq": + params.LocalAddress = appendPointerSlice(params.LocalAddress, v) + case "n": + params.LocalAddressN = appendPointerSlice(params.LocalAddressN, v) + default: + return unexpectedOperator(op, name) + } + case "local_address_id": + v, err := strconv.Atoi(value.ValueString()) + if err != nil { + return diag.NewErrorDiagnostic( + "Value Parse Error", + fmt.Sprintf("failed to parse as int value: %s", value.ValueString()), + ) + } + switch op { + case "eq": + params.LocalAddressId = appendPointerSlice(params.LocalAddressId, v) + case "n": + params.LocalAddressIdN = appendPointerSlice(params.LocalAddressIdN, v) + default: + return unexpectedOperator(op, name) + } + case "local_as": + v, err := strconv.ParseInt(value.ValueString(), 10, 64) + if err != nil { + return diag.NewErrorDiagnostic( + "Value Parse Error", + fmt.Sprintf("failed to parse as int64 value: %s", value.ValueString()), + ) + } + switch op { + case "eq": + params.LocalAs = appendPointerSlice(params.LocalAs, v) + case "n": + params.LocalAsN = appendPointerSlice(params.LocalAsN, v) + default: + return unexpectedOperator(op, name) + } + case "local_as_id": + v, err := strconv.Atoi(value.ValueString()) + if err != nil { + return diag.NewErrorDiagnostic( + "Value Parse Error", + fmt.Sprintf("failed to parse as int value: %s", value.ValueString()), + ) + } + switch op { + case "eq": + params.LocalAsId = appendPointerSlice(params.LocalAsId, v) + case "n": + params.LocalAsIdN = appendPointerSlice(params.LocalAsIdN, v) + default: + return unexpectedOperator(op, name) + } + case "modified_by_request": + v, err := uuid.Parse(value.ValueString()) + if err != nil { + return diag.NewErrorDiagnostic( + "Value Parse Error", + fmt.Sprintf("failed to parse as github.com/google/uuid.UUID value: %s", value.ValueString()), + ) + } + switch op { + case "eq": + params.ModifiedByRequest = &v + default: + return unexpectedOperator(op, name) + } + case "name": + v := value.ValueString() + switch op { + case "eq": + params.Name = appendPointerSlice(params.Name, v) + case "empty": + v, err := strconv.ParseBool(value.ValueString()) + if err != nil { + return diag.NewErrorDiagnostic( + "Value Parse Error", + fmt.Sprintf("failed to parse as bool value: %s", value.ValueString()), + ) + } + params.NameEmpty = &v + case "ic": + params.NameIc = appendPointerSlice(params.NameIc, v) + case "ie": + params.NameIe = appendPointerSlice(params.NameIe, v) + case "iew": + params.NameIew = appendPointerSlice(params.NameIew, v) + case "isw": + params.NameIsw = appendPointerSlice(params.NameIsw, v) + case "n": + params.NameN = appendPointerSlice(params.NameN, v) + case "nic": + params.NameNic = appendPointerSlice(params.NameNic, v) + case "nie": + params.NameNie = appendPointerSlice(params.NameNie, v) + case "niew": + params.NameNiew = appendPointerSlice(params.NameNiew, v) + case "nisw": + params.NameNisw = appendPointerSlice(params.NameNisw, v) + default: + return unexpectedOperator(op, name) + } + case "peer_group": + v, err := strconv.Atoi(value.ValueString()) + if err != nil { + return diag.NewErrorDiagnostic( + "Value Parse Error", + fmt.Sprintf("failed to parse as int value: %s", value.ValueString()), + ) + } + switch op { + case "eq": + params.PeerGroup = appendPointerSlice(params.PeerGroup, v) + case "n": + params.PeerGroupN = appendPointerSlice(params.PeerGroupN, v) + default: + return unexpectedOperator(op, name) + } + case "q": + v := value.ValueString() + switch op { + case "eq": + params.Q = &v + default: + return unexpectedOperator(op, name) + } + case "remote_address": + v := value.ValueString() + switch op { + case "eq": + params.RemoteAddress = appendPointerSlice(params.RemoteAddress, v) + case "n": + params.RemoteAddressN = appendPointerSlice(params.RemoteAddressN, v) + default: + return unexpectedOperator(op, name) + } + case "remote_address_id": + v, err := strconv.Atoi(value.ValueString()) + if err != nil { + return diag.NewErrorDiagnostic( + "Value Parse Error", + fmt.Sprintf("failed to parse as int value: %s", value.ValueString()), + ) + } + switch op { + case "eq": + params.RemoteAddressId = appendPointerSlice(params.RemoteAddressId, v) + case "n": + params.RemoteAddressIdN = appendPointerSlice(params.RemoteAddressIdN, v) + default: + return unexpectedOperator(op, name) + } + case "remote_as": + v, err := strconv.ParseInt(value.ValueString(), 10, 64) + if err != nil { + return diag.NewErrorDiagnostic( + "Value Parse Error", + fmt.Sprintf("failed to parse as int64 value: %s", value.ValueString()), + ) + } + switch op { + case "eq": + params.RemoteAs = appendPointerSlice(params.RemoteAs, v) + case "n": + params.RemoteAsN = appendPointerSlice(params.RemoteAsN, v) + default: + return unexpectedOperator(op, name) + } + case "remote_as_id": + v, err := strconv.Atoi(value.ValueString()) + if err != nil { + return diag.NewErrorDiagnostic( + "Value Parse Error", + fmt.Sprintf("failed to parse as int value: %s", value.ValueString()), + ) + } + switch op { + case "eq": + params.RemoteAsId = appendPointerSlice(params.RemoteAsId, v) + case "n": + params.RemoteAsIdN = appendPointerSlice(params.RemoteAsIdN, v) + default: + return unexpectedOperator(op, name) + } + case "site": + v := value.ValueString() + switch op { + case "eq": + params.Site = appendPointerSlice(params.Site, v) + case "n": + params.SiteN = appendPointerSlice(params.SiteN, v) + default: + return unexpectedOperator(op, name) + } + case "site_id": + v, err := strconv.Atoi(value.ValueString()) + if err != nil { + return diag.NewErrorDiagnostic( + "Value Parse Error", + fmt.Sprintf("failed to parse as int value: %s", value.ValueString()), + ) + } + switch op { + case "eq": + params.SiteId = appendPointerSlice(params.SiteId, v) + case "n": + params.SiteIdN = appendPointerSlice(params.SiteIdN, v) + default: + return unexpectedOperator(op, name) + } + case "status": + v := value.ValueString() + switch op { + case "eq": + params.Status = &v + case "n": + params.StatusN = &v + default: + return unexpectedOperator(op, name) + } + case "tag": + v := value.ValueString() + switch op { + case "eq": + params.Tag = appendPointerSlice(params.Tag, v) + case "n": + params.TagN = appendPointerSlice(params.TagN, v) + default: + return unexpectedOperator(op, name) + } + case "tenant": + v, err := strconv.Atoi(value.ValueString()) + if err != nil { + return diag.NewErrorDiagnostic( + "Value Parse Error", + fmt.Sprintf("failed to parse as int value: %s", value.ValueString()), + ) + } + switch op { + case "eq": + params.Tenant = &v + case "n": + params.TenantN = &v + default: + return unexpectedOperator(op, name) + } + case "updated_by_request": + v, err := uuid.Parse(value.ValueString()) + if err != nil { + return diag.NewErrorDiagnostic( + "Value Parse Error", + fmt.Sprintf("failed to parse as github.com/google/uuid.UUID value: %s", value.ValueString()), + ) + } + switch op { + case "eq": + params.UpdatedByRequest = &v + default: + return unexpectedOperator(op, name) + } + default: + return diag.NewErrorDiagnostic( + "Unexpected filter name", + fmt.Sprintf("Did not recognize field name: %s", name), + ) + } + return nil +} diff --git a/internal/provider/filter.go b/internal/provider/filter.go new file mode 100644 index 0000000..c0b8f23 --- /dev/null +++ b/internal/provider/filter.go @@ -0,0 +1,38 @@ +package provider + +import ( + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +type Filters []Filter + +type Filter struct { + Name types.String `tfsdk:"name"` + Operator types.String `tfsdk:"operator"` + Value types.String `tfsdk:"value"` +} + +func FiltersSchema(fields, ops []string) schema.NestedAttributeObject { + return schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "name": schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + stringvalidator.OneOf(fields...), + }, + }, + "operator": schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + stringvalidator.OneOf(ops...), + }, + }, + "value": schema.StringAttribute{ + Required: true, + }, + }, + } +} diff --git a/internal/provider/provider.go b/internal/provider/provider.go index c1c4eeb..e512f9c 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -177,6 +177,7 @@ func (p *NetboxBGPProvider) Resources(ctx context.Context) []func() resource.Res func (p *NetboxBGPProvider) DataSources(ctx context.Context) []func() datasource.DataSource { return []func() datasource.DataSource{ NewSessionDataSource, + NewSessionsDataSource, } } diff --git a/internal/provider/utils.go b/internal/provider/utils.go index 03a3710..33cbcdc 100644 --- a/internal/provider/utils.go +++ b/internal/provider/utils.go @@ -6,6 +6,7 @@ import ( "net/http" "strconv" + "github.com/ffddorf/terraform-provider-netbox-bgp/client" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" @@ -71,3 +72,23 @@ func importByInt64ID(ctx context.Context, req resource.ImportStateRequest, resp resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), id)...) } + +func appendPointerSlice[T any](s *[]T, vals ...T) *[]T { + if s == nil { + val := make([]T, 0, len(vals)) + s = &val + } + newS := append(*s, vals...) + return &newS +} + +func doPlainReq(ctx context.Context, req *http.Request, c *client.Client) (*http.Response, error) { + req = req.WithContext(ctx) + for _, e := range c.RequestEditors { + if err := e(ctx, req); err != nil { + return nil, err + } + } + + return c.Client.Do(req) +} diff --git a/tools/tools.go b/tools/tools.go index d447da4..5b0fdb4 100644 --- a/tools/tools.go +++ b/tools/tools.go @@ -11,4 +11,7 @@ import ( // API client generation _ "github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen" + + // For generated code + _ "golang.org/x/tools/cmd/goimports" )