Skip to content

Commit

Permalink
Fixing errors and adding doc template
Browse files Browse the repository at this point in the history
  • Loading branch information
vlad-preutu authored and Reinhard-Pilz-Dynatrace committed Jul 6, 2023
1 parent 04b516f commit 3daa932
Show file tree
Hide file tree
Showing 4 changed files with 131 additions and 24 deletions.
38 changes: 38 additions & 0 deletions docs/resources/custom_device.md
Original file line number Diff line number Diff line change
@@ -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 generated by tfplugindocs -->
## 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.

69 changes: 53 additions & 16 deletions dynatrace/api/v2/customdevice/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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}
}
Expand All @@ -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"
}
Expand All @@ -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
Expand Down
40 changes: 32 additions & 8 deletions dynatrace/api/v2/customdevice/settings/custom_device.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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,
Expand Down
8 changes: 8 additions & 0 deletions terraform.tfstate
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"version": 4,
"terraform_version": "1.2.9",
"serial": 1,
"lineage": "2e9ffda4-8331-7a07-571a-1e66b1588f69",
"outputs": {},
"resources": []
}

0 comments on commit 3daa932

Please sign in to comment.