From e7fe37c4e538b42514eab8a8ae07cc5c4c63e789 Mon Sep 17 00:00:00 2001 From: danischm Date: Mon, 8 Jan 2024 16:14:57 +0100 Subject: [PATCH] Add serial number query option to device data source --- docs/data-sources/device.md | 4 +- gen/definitions/device.yaml | 1 + gen/generator.go | 35 +++++++++++------ gen/schema/schema.yaml | 2 +- gen/templates/data_source.go | 30 +++++++++------ .../data_source_catalystcenter_device.go | 38 ++++++++++++++++++- 6 files changed, 83 insertions(+), 27 deletions(-) diff --git a/docs/data-sources/device.md b/docs/data-sources/device.md index 373c1653..d4ab3e51 100644 --- a/docs/data-sources/device.md +++ b/docs/data-sources/device.md @@ -21,13 +21,13 @@ data "catalystcenter_device" "example" { ## Schema -### Required +### Optional - `id` (String) The id of the object +- `serial_number` (String) Device serial number ### Read-Only - `hostname` (String) Device hostname - `pid` (String) Device product ID -- `serial_number` (String) Device serial number - `stack` (Boolean) Device is a stacked switch diff --git a/gen/definitions/device.yaml b/gen/definitions/device.yaml index a6044b45..bbfca043 100644 --- a/gen/definitions/device.yaml +++ b/gen/definitions/device.yaml @@ -8,6 +8,7 @@ attributes: data_path: deviceInfo type: String mandatory: true + data_source_query: true description: Device serial number example: FOC12345678 - model_name: stack diff --git a/gen/generator.go b/gen/generator.go index 251f193c..6bc60dde 100644 --- a/gen/generator.go +++ b/gen/generator.go @@ -119,7 +119,6 @@ type YamlConfig struct { PutIdIncludePath string `yaml:"put_id_include_path"` PutIdQueryParam string `yaml:"put_id_query_param"` PutNoId bool `yaml:"put_no_id"` - DataSourceNameQuery bool `yaml:"data_source_name_query"` MinimumVersion string `yaml:"minimum_version"` DsDescription string `yaml:"ds_description"` ResDescription string `yaml:"res_description"` @@ -141,6 +140,7 @@ type YamlConfigAttribute struct { Id bool `yaml:"id"` Reference bool `yaml:"reference"` QueryParam bool `yaml:"query_param"` + DataSourceQuery bool `yaml:"data_source_query"` Mandatory bool `yaml:"mandatory"` WriteOnly bool `yaml:"write_only"` WriteChangesOnly bool `yaml:"write_changes_only"` @@ -270,19 +270,30 @@ func GetQueryParam(attributes []YamlConfigAttribute) YamlConfigAttribute { return YamlConfigAttribute{} } +// Templating helper function to return true if data source query attribute(s) are present +func HasDataSourceQuery(attributes []YamlConfigAttribute) bool { + for _, attr := range attributes { + if attr.DataSourceQuery { + return true + } + } + return false +} + // Map of templating functions var functions = template.FuncMap{ - "toGoName": ToGoName, - "camelCase": CamelCase, - "snakeCase": SnakeCase, - "sprintf": fmt.Sprintf, - "toLower": strings.ToLower, - "path": BuildPath, - "hasId": HasId, - "hasReference": HasReference, - "hasQueryParam": HasQueryParam, - "getId": GetId, - "getQueryParam": GetQueryParam, + "toGoName": ToGoName, + "camelCase": CamelCase, + "snakeCase": SnakeCase, + "sprintf": fmt.Sprintf, + "toLower": strings.ToLower, + "path": BuildPath, + "hasId": HasId, + "hasReference": HasReference, + "hasQueryParam": HasQueryParam, + "getId": GetId, + "getQueryParam": GetQueryParam, + "hasDataSourceQuery": HasDataSourceQuery, } func augmentAttribute(attr *YamlConfigAttribute) { diff --git a/gen/schema/schema.yaml b/gen/schema/schema.yaml index 78f199e0..3dd44823 100644 --- a/gen/schema/schema.yaml +++ b/gen/schema/schema.yaml @@ -24,7 +24,6 @@ id_from_attribute: bool(required=False) # Set to true if the ID is derived from put_id_include_path: str(required=False) # If PUT needs to have specific JSON path where ID should be inserted put_id_query_param: str(required=False) # If PUT needs to have specific query parameter where ID should be inserted put_no_id: bool(required=False) # Set to true if the PUT request does not require an ID -data_source_name_query: bool(required=False) # Set to true if the data source supports name queries minimum_version: str(required=False) # Define a minimum supported version ds_description: str(required=False) # Define a data source description res_description: str(required=False) # Define a resource description @@ -44,6 +43,7 @@ attribute: id: bool(required=False) # Set to true if the attribute is part of the ID reference: bool(required=False) # Set to true if the attribute is a reference being used in the path (URL) of the REST endpoint query_param: bool(required=False) # Set to true if the attribute is a query parameter and being used for GET, POST and PUT requests + data_source_query: bool(required=False) # Set to true if the attribute is an alternative query parameter for the data source mandatory: bool(required=False) # Set to true if the attribute is mandatory write_only: bool(required=False) # Set to true if the attribute is write-only, meaning we cannot read the value write_changes_only: bool(required=False) # Set to true if the attribute should only be written (included in PUT payload) if it has changed diff --git a/gen/templates/data_source.go b/gen/templates/data_source.go index b83b73f6..2ae95fb2 100644 --- a/gen/templates/data_source.go +++ b/gen/templates/data_source.go @@ -56,7 +56,7 @@ func (d *{{camelCase .Name}}DataSource) Metadata(_ context.Context, req datasour resp.TypeName = req.ProviderTypeName + "_{{snakeCase .Name}}" } -{{- $nameQuery := .DataSourceNameQuery}} +{{- $dataSourceQuery := hasDataSourceQuery .Attributes}} func (d *{{camelCase .Name}}DataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { resp.Schema = schema.Schema{ @@ -66,7 +66,7 @@ func (d *{{camelCase .Name}}DataSource) Schema(ctx context.Context, req datasour Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ MarkdownDescription: "The id of the object", - {{- if not .DataSourceNameQuery}} + {{- if not $dataSourceQuery}} Required: true, {{- else}} Optional: true, @@ -83,7 +83,7 @@ func (d *{{camelCase .Name}}DataSource) Schema(ctx context.Context, req datasour {{- if or .Reference .QueryParam}} Required: true, {{- else}} - {{- if and (eq .ModelName "name") ($nameQuery)}} + {{- if .DataSourceQuery}} Optional: true, {{- end}} Computed: true, @@ -146,12 +146,16 @@ func (d *{{camelCase .Name}}DataSource) Schema(ctx context.Context, req datasour } } -{{- if .DataSourceNameQuery}} +{{- if $dataSourceQuery}} func (d *{{camelCase .Name}}DataSource) ConfigValidators(ctx context.Context) []datasource.ConfigValidator { return []datasource.ConfigValidator{ datasourcevalidator.ExactlyOneOf( path.MatchRoot("id"), - path.MatchRoot("name"), + {{- range .Attributes}} + {{- if .DataSourceQuery}} + path.MatchRoot("{{.TfName}}"), + {{- end}} + {{- end}} ), } } @@ -179,18 +183,20 @@ func (d *{{camelCase .Name}}DataSource) Read(ctx context.Context, req datasource tflog.Debug(ctx, fmt.Sprintf("%s: Beginning Read", config.Id.String())) - {{- if .DataSourceNameQuery}} - if config.Id.IsNull() && !config.Name.IsNull() { + {{- if $dataSourceQuery}} + {{- range .Attributes}} + {{- if .DataSourceQuery}} + if config.Id.IsNull() && !config.{{toGoName .TfName}}.IsNull() { res, err := d.client.Get(config.getPath()) if err != nil { resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Failed to retrieve objects, got error: %s", err)) return } - if value := res.Get("response"); len(value.Array()) > 0 { + if value := res; len(value.Array()) > 0 { value.ForEach(func(k, v gjson.Result) bool { - if config.Name.ValueString() == v.Get("name").String() { + if config.{{toGoName .TfName}}.ValueString() == v.Get("{{if .ResponseDataPath}}{{.ResponseDataPath}}{{else}}{{if .DataPath}}{{.DataPath}}.{{end}}{{.ModelName}}{{end}}").String() { config.Id = types.StringValue(v.Get("id").String()) - tflog.Debug(ctx, fmt.Sprintf("%s: Found object with name '%v', id: %v", config.Id.String(), config.Name.ValueString(), config.Id.String())) + tflog.Debug(ctx, fmt.Sprintf("%s: Found object with {{.ModelName}} '%v', id: %v", config.Id.String(), config.{{toGoName .TfName}}.ValueString(), config.Id.String())) return false } return true @@ -198,11 +204,13 @@ func (d *{{camelCase .Name}}DataSource) Read(ctx context.Context, req datasource } if config.Id.IsNull() { - resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Failed to find object with name: %s", config.Name.ValueString())) + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Failed to find object with {{.ModelName}}: %s", config.{{toGoName .TfName}}.ValueString())) return } } {{- end}} + {{- end}} + {{- end}} params := "" {{- if .IdQueryParam}} diff --git a/internal/provider/data_source_catalystcenter_device.go b/internal/provider/data_source_catalystcenter_device.go index 6a061d6d..01c5ec95 100644 --- a/internal/provider/data_source_catalystcenter_device.go +++ b/internal/provider/data_source_catalystcenter_device.go @@ -24,10 +24,14 @@ import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-framework-validators/datasourcevalidator" "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" cc "github.com/netascode/go-catalystcenter" + "github.com/tidwall/gjson" ) //template:end imports @@ -60,10 +64,12 @@ func (d *DeviceDataSource) Schema(ctx context.Context, req datasource.SchemaRequ Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ MarkdownDescription: "The id of the object", - Required: true, + Optional: true, + Computed: true, }, "serial_number": schema.StringAttribute{ MarkdownDescription: "Device serial number", + Optional: true, Computed: true, }, "stack": schema.BoolAttribute{ @@ -81,6 +87,14 @@ func (d *DeviceDataSource) Schema(ctx context.Context, req datasource.SchemaRequ }, } } +func (d *DeviceDataSource) ConfigValidators(ctx context.Context) []datasource.ConfigValidator { + return []datasource.ConfigValidator{ + datasourcevalidator.ExactlyOneOf( + path.MatchRoot("id"), + path.MatchRoot("serial_number"), + ), + } +} func (d *DeviceDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, _ *datasource.ConfigureResponse) { if req.ProviderData == nil { @@ -104,6 +118,28 @@ func (d *DeviceDataSource) Read(ctx context.Context, req datasource.ReadRequest, } tflog.Debug(ctx, fmt.Sprintf("%s: Beginning Read", config.Id.String())) + if config.Id.IsNull() && !config.SerialNumber.IsNull() { + res, err := d.client.Get(config.getPath()) + if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Failed to retrieve objects, got error: %s", err)) + return + } + if value := res; len(value.Array()) > 0 { + value.ForEach(func(k, v gjson.Result) bool { + if config.SerialNumber.ValueString() == v.Get("deviceInfo.serialNumber").String() { + config.Id = types.StringValue(v.Get("id").String()) + tflog.Debug(ctx, fmt.Sprintf("%s: Found object with serialNumber '%v', id: %v", config.Id.String(), config.SerialNumber.ValueString(), config.Id.String())) + return false + } + return true + }) + } + + if config.Id.IsNull() { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Failed to find object with serialNumber: %s", config.SerialNumber.ValueString())) + return + } + } params := "" params += "/" + config.Id.ValueString()