Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add opsgenie destination resource #176

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions client/destinations.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,13 @@ type ServiceNowAttributes struct {
Auth Auth `json:"auth"`
}

type OpsgenieAttributes struct {
Name string `json:"name"`
DestinationType string `json:"destination_type"`
URL string `json:"url"`
Auth Auth `json:"auth"`
}

type Auth struct {
Username string `json:"username"`
// Password is only set for requests. Will be empty for API responses
Expand Down
2 changes: 2 additions & 0 deletions docs/data-sources/stream.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,5 @@ Use this data source to retrieve information about an existing stream for use in
- `id` (String) The ID of this resource.
- `stream_name` (String)
- `stream_query` (String) Stream query


37 changes: 37 additions & 0 deletions docs/resources/opsgenie_destination.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "lightstep_opsgenie_destination Resource - terraform-provider-lightstep"
subcategory: ""
description: |-

---

# lightstep_opsgenie_destination (Resource)





<!-- schema generated by tfplugindocs -->
## Schema

### Required

- `auth` (Block List, Min: 1, Max: 1) Basic auth used to authenticate with the Opsgenie instance (see [below for nested schema](#nestedblock--auth))
- `destination_name` (String) Name of the Opsgenie destination
- `project_name` (String)
- `url` (String) Opsgenie instance URL

### Read-Only

- `id` (String) The ID of this resource.

<a id="nestedblock--auth"></a>
### Nested Schema for `auth`

Required:

- `password` (String, Sensitive)
- `username` (String)


2 changes: 2 additions & 0 deletions docs/resources/user_role_binding.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,5 @@ resource "lightstep_user_role_binding" "proj_viewer" {
### Read-Only

- `id` (String) The ID of this resource.


1 change: 1 addition & 0 deletions lightstep/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ func Provider() *schema.Provider {
"lightstep_pagerduty_destination": resourcePagerdutyDestination(),
"lightstep_slack_destination": resourceSlackDestination(),
"lightstep_servicenow_destination": resourceServiceNowDestination(),
"lightstep_opsgenie_destination": resourceOpsgenieDestination(),
"lightstep_alerting_rule": resourceAlertingRule(),
"lightstep_dashboard": resourceUnifiedDashboard(UnifiedChartSchema),
"lightstep_alert": resourceUnifiedCondition(UnifiedConditionSchema),
Expand Down
2 changes: 1 addition & 1 deletion lightstep/provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ func init() {

testProject = os.Getenv("LIGHTSTEP_PROJECT")
if testProject == "" {
testProject = "terraform-provider-tests"
testProject = "terraform-provider-test"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thank you! I didn't realize this was already set automatically, I had just made an env variable

}
}

Expand Down
128 changes: 128 additions & 0 deletions lightstep/resource_opsgenie_destination.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package lightstep

import (
"context"
"fmt"
"strings"

"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"

"github.com/lightstep/terraform-provider-lightstep/client"
)

func resourceOpsgenieDestination() *schema.Resource {
return &schema.Resource{
CreateContext: resourceOpsgenieDestinationCreate,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do you want to add a note for future readers about why there's no Update entry here?

ReadContext: resourceDestinationRead,
DeleteContext: resourceDestinationDelete,
Importer: &schema.ResourceImporter{
StateContext: resourceOpsgenieDestinationImport,
},
Schema: map[string]*schema.Schema{
"project_name": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"destination_name": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
Description: "Name of the Opsgenie destination",
},
"url": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
Description: "Opsgenie instance URL",
ValidateFunc: validation.IsURLWithScheme([]string{"https"}),
},
"auth": {
Type: schema.TypeList,
MinItems: 1,
MaxItems: 1,
Required: true,
ForceNew: true,
Description: "Basic auth used to authenticate with the Opsgenie instance",
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could you add descriptions for these fields too? it's on my mind since I just went through and did it for a bunch of things

"username": {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought the username wasn't used with OpsGenie destination?

Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"password": {
Type: schema.TypeString,
Sensitive: true,
Required: true,
ForceNew: true,
},
},
},
},
},
}
}

func resourceOpsgenieDestinationCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
c := m.(*client.Client)
attrs := client.OpsgenieAttributes{
Name: d.Get("destination_name").(string),
DestinationType: "opsgenie",
URL: d.Get("url").(string),
}
auth := d.Get("auth").([]interface{})[0].(map[string]interface{})
attrs.Auth = client.Auth{
Username: auth["username"].(string),
Password: auth["password"].(string),
}
dest := client.Destination{
Type: "destination",
Attributes: attrs,
}

destination, err := c.CreateDestination(ctx, d.Get("project_name").(string), dest)
if err != nil {
return diag.FromErr(fmt.Errorf("failed to create Opsgenie destination %v: %v", attrs.Name, err))
}

d.SetId(destination.ID)
return resourceDestinationRead(ctx, d, m)
}

func resourceOpsgenieDestinationImport(ctx context.Context, d *schema.ResourceData, m interface{}) ([]*schema.ResourceData, error) {
Copy link
Contributor

@ltyson ltyson Aug 28, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ooc, how do we test this sort of thing? are the acceptance tests sufficient or do you have to actually try it using the terraform import command on a live system?

c := m.(*client.Client)

ids := strings.Split(d.Id(), ".")
if len(ids) != 2 {
return []*schema.ResourceData{}, fmt.Errorf("error importing lightstep_opsgenie_destination. Expecting an ID formed as '<lightstep_project>.<lightstep_destination_ID>'")
}

project, id := ids[0], ids[1]
dest, err := c.GetDestination(ctx, project, id)
if err != nil {
return []*schema.ResourceData{}, fmt.Errorf("failed to get Opsgenie destination: %v", err)
}

d.SetId(dest.ID)
if err := d.Set("project_name", project); err != nil {
return []*schema.ResourceData{}, fmt.Errorf("unable to set project_name resource field: %v", err)
}

attributes := dest.Attributes.(map[string]interface{})
if err := d.Set("destination_name", attributes["name"]); err != nil {
return []*schema.ResourceData{}, fmt.Errorf("unable to set destination_name resource field: %v", err)
}

if err := d.Set("url", attributes["url"]); err != nil {
return []*schema.ResourceData{}, fmt.Errorf("unable to set url resource field: %v", err)
}

if err := d.Set("auth", []interface{}{attributes["auth"]}); err != nil {
return []*schema.ResourceData{}, fmt.Errorf("unable to set auth resource field: %v", err)
}

return []*schema.ResourceData{d}, nil
}
150 changes: 150 additions & 0 deletions lightstep/resource_opsgenie_destination_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
package lightstep

import (
"context"
"fmt"
"regexp"
"testing"

"github.com/lightstep/terraform-provider-lightstep/client"

"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
)

func TestAccOpsgenieDestination(t *testing.T) {
var destination client.Destination

missingUrlConfig := `
resource "lightstep_opsgenie_destination" "missing_url" {
project_name = ` + fmt.Sprintf("\"%s\"", testProject) + `
destination_name = "my-destination"
auth {
username = ""
password = "pass123"
}
}
`
missingAuthConfig := `
resource "lightstep_opsgenie_destination" "missing_auth" {
project_name = ` + fmt.Sprintf("\"%s\"", testProject) + `
destination_name = "my-destination"
url = "https://example.com"
}
`

destinationConfig := `
resource "lightstep_opsgenie_destination" "opsgenie" {
project_name = ` + fmt.Sprintf("\"%s\"", testProject) + `
destination_name = "my-destination"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: just to make this more realistic, maybe add some spaces in the name?

url = "https://example.com"
auth {
username = ""
password = "pass123"
}
}
`
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccOpsgenieDestinationDestroy,
Steps: []resource.TestStep{
{
Config: missingUrlConfig,
Check: resource.ComposeTestCheckFunc(
testAccCheckOpsgenieDestinationExists("lightstep_opsgenie_destination.missing_url", &destination),
),
ExpectError: regexp.MustCompile("The argument \"url\" is required, but no definition was found."),
},
{
Config: missingAuthConfig,
Check: resource.ComposeTestCheckFunc(
testAccCheckOpsgenieDestinationExists("lightstep_opsgenie_destination.missing_auth", &destination),
),
ExpectError: regexp.MustCompile("Insufficient auth blocks"),
},
{
Config: destinationConfig,
Check: resource.ComposeTestCheckFunc(
testAccCheckWebhookDestinationExists("lightstep_opsgenie_destination.opsgenie", &destination),
resource.TestCheckResourceAttr("lightstep_opsgenie_destination.opsgenie", "destination_name", "my-destination"),
resource.TestCheckResourceAttr("lightstep_opsgenie_destination.opsgenie", "url", "https://example.com"),
resource.TestCheckResourceAttr("lightstep_opsgenie_destination.opsgenie", "auth.0.username", ""),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what happens if they do specify a username? is that an option we even want to expose if it will never be needed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I waffled on this. On one hand opsgenie is accepting a basic auth token so it makes sense to support username if they ever start using it. On the other hand we could hide this from the user in terraform just like we do in the UI. (It wouldn't be hidden in the public API though.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you have a preference?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd vote to hide it in terraform, assuming that doesn't create more work for you, in the interest of making terraform as minimally confusing as possible.

resource.TestCheckResourceAttr("lightstep_opsgenie_destination.opsgenie", "auth.0.password", "pass123"),
),
},
},
})

}

func TestAccOpsgenieDestinationImport(t *testing.T) {
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
Steps: []resource.TestStep{
{
Config: `
resource "lightstep_opsgenie_destination" "opsgenie" {
project_name = ` + fmt.Sprintf("\"%s\"", testProject) + `
destination_name = "do-not-delete-opsgenie"
url = "https://example.com"
auth {
username = ""
password = "pass123"
}
}
`,
},
{
ResourceName: "lightstep_opsgenie_destination.opsgenie",
ImportState: true,
ImportStateVerify: true,
ImportStateVerifyIgnore: []string{"auth.0.password"},
ImportStateIdPrefix: fmt.Sprintf("%s.", testProject),
},
},
})
}

func testAccCheckOpsgenieDestinationExists(resourceName string, destination *client.Destination) resource.TestCheckFunc {
return func(s *terraform.State) error {
// get destination from TF state
tfDestination, ok := s.RootModule().Resources[resourceName]
if !ok {
return fmt.Errorf("not found: %s", resourceName)
}

if tfDestination.Primary.ID == "" {
return fmt.Errorf("id is not set")
}

// get destination from LS
client := testAccProvider.Meta().(*client.Client)
d, err := client.GetDestination(context.Background(), testProject, tfDestination.Primary.ID)
if err != nil {
return err
}

destination = d
return nil
}
}

func testAccOpsgenieDestinationDestroy(s *terraform.State) error {
conn := testAccProvider.Meta().(*client.Client)
for _, resource := range s.RootModule().Resources {
if resource.Type != "lightstep_opsgenie_destination" {
continue
}

s, err := conn.GetDestination(context.Background(), testProject, resource.Primary.ID)
if err == nil {
if s.ID == resource.Primary.ID {
return fmt.Errorf("destination with ID (%v) still exists.", resource.Primary.ID)
}
}

}
return nil
}