From 3daa932d8f43807526ab4d4f27aef4c630cb71c1 Mon Sep 17 00:00:00 2001 From: Vlad Preutu Date: Thu, 6 Jul 2023 12:55:57 +0200 Subject: [PATCH] Fixing errors and adding doc template --- docs/resources/custom_device.md | 38 ++++++++++ dynatrace/api/v2/customdevice/service.go | 69 ++++++++++++++----- .../v2/customdevice/settings/custom_device.go | 40 ++++++++--- terraform.tfstate | 8 +++ 4 files changed, 131 insertions(+), 24 deletions(-) create mode 100644 docs/resources/custom_device.md create mode 100644 terraform.tfstate diff --git a/docs/resources/custom_device.md b/docs/resources/custom_device.md new file mode 100644 index 000000000..9cd461899 --- /dev/null +++ b/docs/resources/custom_device.md @@ -0,0 +1,38 @@ +--- +layout: "" +page_title: dynatrace_custom_device Resource - terraform-provider-dynatrace" +description: |- + The resource `dynatrace_custom_device` covers configuration for custom devices +--- + +# dynatrace_custom_device (Resource) + +## Dynatrace Documentation + +- Monitored entities API - https://www.dynatrace.com/support/help/dynatrace-api/environment-api/entity-v2 + + +## Resource Example Usage + +```terraform +resource "dynatrace_custom_device" "#name#" { + custom_device_id = "customDeviceId" + display_name = "customDevicename" +} +``` + +## Schema + +### Required + +- `display_name` (String) Tag name + +### Optional + +- `custom_device_id` (String) This Id can either be provided in the resource or generated by Terraform when the resource is created. If you use the ID of an existing device, the respective parameters will be updated. + +### Read-Only + +- `id` (String) The ID of this resource - same value as the `custom_device_id`. +- `entity_id` (String) The Dynatrace EntityId of this resource. + diff --git a/dynatrace/api/v2/customdevice/service.go b/dynatrace/api/v2/customdevice/service.go index 9b16b6337..2dd62239d 100644 --- a/dynatrace/api/v2/customdevice/service.go +++ b/dynatrace/api/v2/customdevice/service.go @@ -19,6 +19,8 @@ package customdevice import ( "fmt" + "sync" + "time" "github.com/dynatrace-oss/terraform-provider-dynatrace/dynatrace/api" customdevice "github.com/dynatrace-oss/terraform-provider-dynatrace/dynatrace/api/v2/customdevice/settings" @@ -27,6 +29,8 @@ import ( "github.com/google/uuid" ) +var mutex = &sync.Mutex{} + func Service(credentials *settings.Credentials) settings.CRUDService[*customdevice.CustomDevice] { return &service{credentials} } @@ -37,26 +41,53 @@ type service struct { func (me *service) Get(id string, v *customdevice.CustomDevice) error { var err error - client := rest.DefaultClient(me.credentials.URL, me.credentials.Token) entitySelector := `detectedName("` + id + `"),type("CUSTOM_DEVICE")` - req := client.Get(fmt.Sprintf("/api/v2/entities?from=now-3y&&entitySelector=%s", entitySelector)).Expect(200) - var enitityList customdevice.CustomDeviceList - if err = req.Finish(enitityList); err != nil { - return err + var CustomDeviceGetResponse customdevice.CustomDeviceGetResponse + + // The result from the GET API enpoint is not very stable, so attepting to get the custom device once is not enough. + // 20 is an arbitraty number (it takes 40s before the method gives up) that should be long enough for the endpoint to return a value. + for i := 0; i < 20; i++ { + req := client.Get(fmt.Sprintf("/api/v2/entities?from=now-3y&&entitySelector=%s", entitySelector)).Expect(200) + err = req.Finish(&CustomDeviceGetResponse) + if len(CustomDeviceGetResponse.Entities) != 0 { + break + } + time.Sleep(2 * time.Second) } - if len(enitityList.Entities) == 0 { + if len(CustomDeviceGetResponse.Entities) == 0 { + // We only throu this error if the Finish method failed for the last attempt because sometimes random calls fail. + // This way if all calls fail, the last will fail as well, and we only get a false positive if the last call happens to be the only one to fail. + if err != nil { + return err + } return rest.Error{Code: 404, Message: `Custom device with ID:` + id + " not found!"} } - v.DisplayName = enitityList.Entities[0].DisplayName - v.EntityId = enitityList.Entities[0].EntityId + v.DisplayName = CustomDeviceGetResponse.Entities[0].DisplayName + v.EntityId = CustomDeviceGetResponse.Entities[0].EntityId v.CustomDeviceID = id return nil } +func (me *service) CheckGet(id string, v *customdevice.CustomDevice) error { + var err error + client := rest.DefaultClient(me.credentials.URL, me.credentials.Token) + entitySelector := `detectedName("` + id + `"),type("CUSTOM_DEVICE")` + req := client.Get(fmt.Sprintf("/api/v2/entities?from=now-3y&&entitySelector=%s", entitySelector)).Expect(200) + var CustomDeviceGetResponse customdevice.CustomDeviceGetResponse + if err = req.Finish(&CustomDeviceGetResponse); err != nil { + return err + } + if len(CustomDeviceGetResponse.Entities) == 0 { + return nil + } + v.EntityId = CustomDeviceGetResponse.Entities[0].EntityId + return nil +} + func (me *service) SchemaID() string { return "v2:environment:custom-device" } @@ -70,28 +101,34 @@ func (me *service) Validate(v *customdevice.CustomDevice) error { } func (me *service) Create(v *customdevice.CustomDevice) (*api.Stub, error) { + mutex.Lock() + defer mutex.Unlock() var err error if v.CustomDeviceID == "" { v.CustomDeviceID = uuid.NewString() } - resultDevice := customdevice.CustomDevice{} client := rest.DefaultClient(me.credentials.URL, me.credentials.Token) - if err = client.Post("/api/v2/entities/custom", v, 201, 204).Finish(&resultDevice); err != nil { + if err = client.Post("/api/v2/entities/custom", v, 201, 204).Finish(); err != nil { return nil, err } - resultDevice.CustomDeviceID = v.CustomDeviceID - resultDevice.DisplayName = v.DisplayName - return &api.Stub{ID: resultDevice.CustomDeviceID, Name: *resultDevice.DisplayName, Value: resultDevice}, nil + // Check the custom device was indeed created before finishing up + for i := 0; i < 50; i++ { + me.CheckGet(v.CustomDeviceID, v) + time.Sleep(2 * time.Second) + if v.EntityId != "" { + break + } + } + return &api.Stub{ID: v.CustomDeviceID, Name: *v.DisplayName}, nil } func (me *service) Update(id string, v *customdevice.CustomDevice) error { var err error v.CustomDeviceID = id - v.EntityId = nil - resultDevice := customdevice.CustomDevice{} + v.EntityId = "" client := rest.DefaultClient(me.credentials.URL, me.credentials.Token) - if err = client.Post("/api/v2/entities/custom", v, 201, 204).Finish(&resultDevice); err != nil { + if err = client.Post("/api/v2/entities/custom", v, 204).Finish(); err != nil { return err } return nil diff --git a/dynatrace/api/v2/customdevice/settings/custom_device.go b/dynatrace/api/v2/customdevice/settings/custom_device.go index 26e8d5420..933fc6cb5 100644 --- a/dynatrace/api/v2/customdevice/settings/custom_device.go +++ b/dynatrace/api/v2/customdevice/settings/custom_device.go @@ -6,16 +6,13 @@ import ( ) type CustomDevice struct { - EntityId *string `json:"entityId,omitempty"` // The ID of the custom device. - // Type *string `json:"type,omitempty"` // The type of the custom device. - DisplayName *string `json:"displayName,omitempty"` // The name of the custom device, displayed in the UI. - // Tags Tags `json:"tags,omitempty"` // A set of tags assigned to the custom device. - // Properties map[string]any `json:"properties"` - CustomDeviceID string `json:"customDeviceId,omitempty"` + EntityId string `json:"entityId,omitempty"` // The ID of the custom device. + DisplayName *string `json:"displayName,omitempty"` // The name of the custom device, displayed in the UI. + CustomDeviceID string `json:"customDeviceId,omitempty"` // A unique name that can be provided or generated by the provider } -type CustomDeviceList struct { - Entities []*CustomDevice `json:"entities"` // An unordered list of custom devices +type CustomDeviceGetResponse struct { + Entities []*CustomDevice `json:"entities,omitempty"` // An unordered list of custom devices } func (me *CustomDevice) Schema() map[string]*schema.Schema { @@ -35,10 +32,37 @@ func (me *CustomDevice) Schema() map[string]*schema.Schema { Description: "The unique name of the custom device.", Optional: true, Computed: true, + ForceNew: true, }, } } +func (me *CustomDeviceGetResponse) Schema() map[string]*schema.Schema { + return map[string]*schema.Schema{ + "entities": { + Type: schema.TypeString, + Description: "The list of entities returned by the GET call.", + Optional: true, + Computed: true, + }, + } +} + +func (me *CustomDeviceGetResponse) MarshalHCL(properties hcl.Properties) error { + if err := properties.EncodeAll(map[string]any{ + "entities": me.Entities, + }); err != nil { + return err + } + return nil +} + +func (me *CustomDeviceGetResponse) UnmarshalHCL(decoder hcl.Decoder) error { + return decoder.DecodeAll(map[string]any{ + "entities": &me.Entities, + }) +} + func (me *CustomDevice) MarshalHCL(properties hcl.Properties) error { if err := properties.EncodeAll(map[string]any{ "entity_id": me.EntityId, diff --git a/terraform.tfstate b/terraform.tfstate new file mode 100644 index 000000000..6cbcdc171 --- /dev/null +++ b/terraform.tfstate @@ -0,0 +1,8 @@ +{ + "version": 4, + "terraform_version": "1.2.9", + "serial": 1, + "lineage": "2e9ffda4-8331-7a07-571a-1e66b1588f69", + "outputs": {}, + "resources": [] +}