diff --git a/pkg/config-api-provider/provider/data-sources/sensor.go b/pkg/config-api-provider/provider/data-sources/sensor.go new file mode 100644 index 00000000..abe27413 --- /dev/null +++ b/pkg/config-api-provider/provider/data-sources/sensor.go @@ -0,0 +1,163 @@ +package datasources + +import ( + "context" + + config_api_client "github.com/aruba-uxi/configuration-api-terraform-provider/pkg/config-api-client" + "github.com/aruba-uxi/configuration-api-terraform-provider/pkg/terraform-provider-configuration/provider/util" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +var ( + _ datasource.DataSource = &sensorDataSource{} + _ datasource.DataSourceWithConfigure = &sensorDataSource{} +) + +func NewSensorDataSource() datasource.DataSource { + return &sensorDataSource{} +} + +type sensorDataSource struct { + client *config_api_client.APIClient +} + +type sensorDataSourceModel struct { + Id types.String `tfsdk:"id"` + Serial types.String `tfsdk:"serial"` + Name types.String `tfsdk:"name"` + ModelNumber types.String `tfsdk:"model_number"` + WifiMacAddress types.String `tfsdk:"wifi_mac_address"` + EthernetMacAddress types.String `tfsdk:"ethernet_mac_address"` + AddressNote types.String `tfsdk:"address_note"` + Longitude types.Float32 `tfsdk:"longitude"` + Latitude types.Float32 `tfsdk:"latitude"` + Notes types.String `tfsdk:"notes"` + PcapMode types.String `tfsdk:"pcap_mode"` + Filter struct { + SensorID types.String `tfsdk:"sensor_id"` + } `tfsdk:"filter"` +} + +func (d *sensorDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_sensor" +} + +func (d *sensorDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + }, + "serial": schema.StringAttribute{ + Computed: true, + }, + "name": schema.StringAttribute{ + Computed: true, + }, + "model_number": schema.StringAttribute{ + Computed: true, + }, + "wifi_mac_address": schema.StringAttribute{ + Computed: true, + }, + "ethernet_mac_address": schema.StringAttribute{ + Computed: true, + }, + "address_note": schema.StringAttribute{ + Computed: true, + }, + "longitude": schema.Float32Attribute{ + Computed: true, + }, + "latitude": schema.Float32Attribute{ + Computed: true, + }, + "notes": schema.StringAttribute{ + Computed: true, + }, + "pcap_mode": schema.StringAttribute{ + Computed: true, + }, + "filter": schema.SingleNestedAttribute{ + Required: true, + Attributes: map[string]schema.Attribute{ + "sensor_id": schema.StringAttribute{ + Required: true, + }, + }, + }, + }, + } +} + +func (d *sensorDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var state sensorDataSourceModel + + // Read configuration from request + diags := req.Config.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + request := d.client.ConfigurationAPI. + GetUxiV1alpha1SensorsGet(ctx). + Id(state.Filter.SensorID.ValueString()) + + sensorResponse, response, err := util.RetryFor429(request.Execute) + errorPresent, errorDetail := util.RaiseForStatus(response, err) + + errorSummary := util.GenerateErrorSummary("read", "uxi_sensor") + + if errorPresent { + resp.Diagnostics.AddError(errorSummary, errorDetail) + return + } + + if len(sensorResponse.Items) != 1 { + resp.Diagnostics.AddError(errorSummary, "Could not find specified data source") + return + } + + sensor := sensorResponse.Items[0] + + state.Id = types.StringValue(sensor.Id) + state.Name = types.StringValue(sensor.Name) + state.ModelNumber = types.StringPointerValue(sensor.ModelNumber.Get()) + state.WifiMacAddress = types.StringPointerValue(sensor.WifiMacAddress.Get()) + state.EthernetMacAddress = types.StringPointerValue(sensor.EthernetMacAddress.Get()) + state.AddressNote = types.StringPointerValue(sensor.AddressNote.Get()) + state.Longitude = types.Float32PointerValue(sensor.Longitude.Get()) + state.Latitude = types.Float32PointerValue(sensor.Latitude.Get()) + state.Notes = types.StringPointerValue(sensor.Notes.Get()) + state.PcapMode = types.StringPointerValue(sensor.PcapMode.Get()) + + // Set state + diags = resp.State.Set(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +func (d *sensorDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + // Add a nil check when handling ProviderData because Terraform + // sets that data after it calls the ConfigureProvider RPC. + if req.ProviderData == nil { + return + } + + client, ok := req.ProviderData.(*config_api_client.APIClient) + + if !ok { + resp.Diagnostics.AddError( + "Unexpected Data Source Configure Type", + "Data Source type: Sensor. Please report this issue to the provider developers.", + ) + return + } + + d.client = client +} diff --git a/pkg/config-api-provider/provider/provider.go b/pkg/config-api-provider/provider/provider.go index 164cb376..a5f20b96 100644 --- a/pkg/config-api-provider/provider/provider.go +++ b/pkg/config-api-provider/provider/provider.go @@ -156,6 +156,7 @@ func (p *uxiConfigurationProvider) Configure(ctx context.Context, req provider.C func (p *uxiConfigurationProvider) DataSources(_ context.Context) []func() datasource.DataSource { return []func() datasource.DataSource{ datasources.NewGroupDataSource, + datasources.NewSensorDataSource, datasources.NewWiredNetworkDataSource, datasources.NewWirelessNetworkDataSource, datasources.NewSensorGroupAssignmentDataSource, diff --git a/pkg/config-api-provider/test/data-sources/sensor_test.go b/pkg/config-api-provider/test/data-sources/sensor_test.go new file mode 100644 index 00000000..43042262 --- /dev/null +++ b/pkg/config-api-provider/test/data-sources/sensor_test.go @@ -0,0 +1,141 @@ +package data_source_test + +import ( + "regexp" + "testing" + + "github.com/aruba-uxi/configuration-api-terraform-provider/pkg/terraform-provider-configuration/test/provider" + "github.com/aruba-uxi/configuration-api-terraform-provider/pkg/terraform-provider-configuration/test/util" + "github.com/h2non/gock" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" + "github.com/nbio/st" +) + +func TestSensorDataSource(t *testing.T) { + defer gock.Off() + mockOAuth := util.MockOAuth() + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: provider.TestAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Test Read + { + PreConfig: func() { + util.MockGetSensor( + "uid", + util.GeneratePaginatedResponse([]map[string]interface{}{util.GenerateSensorResponseModel("uid", "")}), + 3, + ) + }, + Config: provider.ProviderConfig + ` + data "uxi_sensor" "my_sensor" { + filter = { + sensor_id = "uid" + } + } + `, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("data.uxi_sensor.my_sensor", "id", "uid"), + ), + }, + }, + }) + + mockOAuth.Mock.Disable() +} + +func TestSensorDataSource429Handling(t *testing.T) { + defer gock.Off() + mockOAuth := util.MockOAuth() + var mock429 *gock.Response + + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: provider.TestAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + + // Test Read + { + PreConfig: func() { + mock429 = gock.New("https://test.api.capenetworks.com"). + Get("/uxi/v1alpha1/sensors"). + Reply(429). + SetHeaders(util.RateLimitingHeaders) + util.MockGetSensor( + "uid", + util.GeneratePaginatedResponse([]map[string]interface{}{util.GenerateSensorResponseModel("uid", "")}), + 3, + ) + }, + Config: provider.ProviderConfig + ` + data "uxi_sensor" "my_sensor" { + filter = { + sensor_id = "uid" + } + } + `, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("data.uxi_sensor.my_sensor", "id", "uid"), + func(s *terraform.State) error { + st.Assert(t, mock429.Mock.Request().Counter, 0) + return nil + }, + ), + }, + }, + }) + + mockOAuth.Mock.Disable() +} + +func TestSensorDataSourceHttpErrorHandling(t *testing.T) { + defer gock.Off() + mockOAuth := util.MockOAuth() + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: provider.TestAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // 5xx error + { + PreConfig: func() { + gock.New("https://test.api.capenetworks.com"). + Get("/uxi/v1alpha1/sensors"). + Reply(500). + JSON(map[string]interface{}{ + "httpStatusCode": 500, + "errorCode": "HPE_GL_ERROR_INTERNAL_SERVER_ERROR", + "message": "Current request cannot be processed due to unknown issue", + "debugId": "12312-123123-123123-1231212", + }) + }, + Config: provider.ProviderConfig + ` + data "uxi_sensor" "my_sensor" { + filter = { + sensor_id = "uid" + } + } + `, + ExpectError: regexp.MustCompile(`(?s)Current request cannot be processed due to unknown issue\s*DebugID: 12312-123123-123123-1231212`), + }, + // Not found error + { + PreConfig: func() { + util.MockGetSensor( + "uid", + util.GeneratePaginatedResponse([]map[string]interface{}{}), + 1, + ) + }, + Config: provider.ProviderConfig + ` + data "uxi_sensor" "my_sensor" { + filter = { + sensor_id = "uid" + } + } + `, + ExpectError: regexp.MustCompile(`Could not find specified data source`), + }, + }, + }) + + mockOAuth.Mock.Disable() +}