Skip to content

Commit

Permalink
feat: environment management support (#501)
Browse files Browse the repository at this point in the history
Adds the ability to manage a team's Environments ✨ by introducing a pair
of data sources and a shiny new resource.

Co-authored-by: Brooke Sargent <brookesargent@honeycomb.io>
  • Loading branch information
jharley and brookesargent authored Jul 19, 2024
1 parent 3ecd18f commit 1018f58
Show file tree
Hide file tree
Showing 27 changed files with 1,360 additions and 117 deletions.
7 changes: 4 additions & 3 deletions client/v2/api_keys_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,14 +85,14 @@ func TestClient_APIKeys_Pagination(t *testing.T) {
testKeys := make([]*APIKey, numKeys)
for i := 0; i < numKeys; i++ {
k, err := c.APIKeys.Create(ctx, &APIKey{
Name: helper.ToPtr(fmt.Sprintf("testkey-%d", i)),
Name: helper.ToPtr(fmt.Sprintf("test.%d", i)),
KeyType: "ingest",
Environment: &Environment{
ID: testEnvironmentID,
},
})
testKeys[i] = k
require.NoError(t, err)
testKeys[i] = k
}
t.Cleanup(func() {
for _, k := range testKeys {
Expand All @@ -116,7 +116,8 @@ func TestClient_APIKeys_Pagination(t *testing.T) {
require.NoError(t, err)
keys = append(keys, items...)
}
assert.Len(t, keys, numKeys)
// we can't guarantee that there are exactly numKeys keys, but there should be at least that many
assert.GreaterOrEqual(t, len(keys), numKeys, "should have at least %d keys", numKeys)
})

t.Run("works with custom page size", func(t *testing.T) {
Expand Down
4 changes: 3 additions & 1 deletion client/v2/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ type Client struct {
http *retryablehttp.Client

// API handlers here
APIKeys APIKeys
APIKeys APIKeys
Environments Environments
}

func NewClient() (*Client, error) {
Expand Down Expand Up @@ -124,6 +125,7 @@ func NewClientWithConfig(config *Config) (*Client, error) {

// bind API handlers here
client.APIKeys = &apiKeys{client: client, authinfo: authinfo}
client.Environments = &environments{client: client, authinfo: authinfo}

return client, nil
}
Expand Down
143 changes: 143 additions & 0 deletions client/v2/environments.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
package v2

import (
"context"
"fmt"
"net/http"

"github.com/hashicorp/jsonapi"

hnyclient "github.com/honeycombio/terraform-provider-honeycombio/client"
)

// Compile-time proof of interface implementation.
var _ Environments = (*environments)(nil)

type Environments interface {
Create(ctx context.Context, env *Environment) (*Environment, error)
Get(ctx context.Context, id string) (*Environment, error)
Update(ctx context.Context, env *Environment) (*Environment, error)
Delete(ctx context.Context, id string) error
List(ctx context.Context, opts ...ListOption) (*Pager[Environment], error)
}

type environments struct {
client *Client
authinfo *AuthMetadata
}

const (
environmentsPath = "/2/teams/%s/environments"
environmentsByIDPath = "/2/teams/%s/environments/%s"
)

const (
EnvironmentColorBlue = "blue"
EnvironmentColorGreen = "green"
EnvironmentColorGold = "gold"
EnvironmentColorRed = "red"
EnvironmentColorPurple = "purple"
EnvironmentColorLightBlue = "lightBlue"
EnvironmentColorLightGreen = "lightGreen"
EnvironmentColorLightGold = "lightGold"
EnvironmentColorLightRed = "lightRed"
EnvironmentColorLightPurple = "lightPurple"
)

func EnvironmentColorTypes() []string {
return []string{
EnvironmentColorBlue,
EnvironmentColorGreen,
EnvironmentColorGold,
EnvironmentColorRed,
EnvironmentColorPurple,
EnvironmentColorLightBlue,
EnvironmentColorLightGreen,
EnvironmentColorLightGold,
EnvironmentColorLightRed,
EnvironmentColorLightPurple,
}
}

func (e *environments) Create(ctx context.Context, env *Environment) (*Environment, error) {
r, err := e.client.Do(ctx,
http.MethodPost,
fmt.Sprintf(environmentsPath, e.authinfo.Team.Slug),
env,
)
if err != nil {
return nil, err
}
if r.StatusCode != http.StatusCreated {
return nil, hnyclient.ErrorFromResponse(r)
}

envrion := new(Environment)
if err := jsonapi.UnmarshalPayload(r.Body, envrion); err != nil {
return nil, err
}
return envrion, nil
}

func (e *environments) Get(ctx context.Context, id string) (*Environment, error) {
r, err := e.client.Do(ctx,
http.MethodGet,
fmt.Sprintf(environmentsByIDPath, e.authinfo.Team.Slug, id),
nil,
)
if err != nil {
return nil, err
}
if r.StatusCode != http.StatusOK {
return nil, hnyclient.ErrorFromResponse(r)
}

envrion := new(Environment)
if err := jsonapi.UnmarshalPayload(r.Body, envrion); err != nil {
return nil, err
}
return envrion, nil
}

func (e *environments) Update(ctx context.Context, env *Environment) (*Environment, error) {
r, err := e.client.Do(ctx,
http.MethodPatch,
fmt.Sprintf(environmentsByIDPath, e.authinfo.Team.Slug, env.ID),
env,
)
if err != nil {
return nil, err
}
if r.StatusCode != http.StatusOK {
return nil, hnyclient.ErrorFromResponse(r)
}

envrion := new(Environment)
if err := jsonapi.UnmarshalPayload(r.Body, envrion); err != nil {
return nil, err
}
return envrion, nil
}

func (e *environments) Delete(ctx context.Context, id string) error {
r, err := e.client.Do(ctx,
http.MethodDelete,
fmt.Sprintf(environmentsByIDPath, e.authinfo.Team.Slug, id),
nil,
)
if err != nil {
return err
}
if r.StatusCode != http.StatusNoContent {
return hnyclient.ErrorFromResponse(r)
}
return nil
}

func (e *environments) List(ctx context.Context, os ...ListOption) (*Pager[Environment], error) {
return NewPager[Environment](
e.client,
fmt.Sprintf(environmentsPath, e.authinfo.Team.Slug),
os...,
)
}
141 changes: 141 additions & 0 deletions client/v2/environments_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package v2

import (
"context"
"math"
"net/http"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

hnyclient "github.com/honeycombio/terraform-provider-honeycombio/client"
"github.com/honeycombio/terraform-provider-honeycombio/internal/helper"
"github.com/honeycombio/terraform-provider-honeycombio/internal/helper/test"
)

func TestClient_Environments(t *testing.T) {
ctx := context.Background()
c := newTestClient(t)

t.Run("happy path", func(t *testing.T) {
// create a new Environment
newEnv := &Environment{
Name: test.RandomStringWithPrefix("test.", 20),
Description: helper.ToPtr(test.RandomString(50)),
Color: helper.ToPtr(EnvironmentColorBlue),
}
e, err := c.Environments.Create(ctx, newEnv)
require.NoError(t, err)
assert.NotEmpty(t, e.ID)
assert.Equal(t, newEnv.Name, e.Name)
assert.NotEmpty(t, e.Slug)
assert.Equal(t, newEnv.Description, e.Description)
assert.Equal(t, newEnv.Color, e.Color)
if assert.NotNil(t, e.Settings) {
assert.True(t, *e.Settings.DeleteProtected)
}

// read the Environment back and compare
env, err := c.Environments.Get(ctx, e.ID)
require.NoError(t, err)
assert.Equal(t, e.ID, env.ID)
assert.Equal(t, e.Name, env.Name)
assert.Equal(t, e.Slug, env.Slug)
assert.Equal(t, e.Description, env.Description)
assert.Equal(t, e.Color, env.Color)
if assert.NotNil(t, env.Settings) {
assert.True(t, *env.Settings.DeleteProtected)
}

// update the Environment's description and color
newDescription := helper.ToPtr(test.RandomString(50))
env.Description = newDescription
env.Color = helper.ToPtr(EnvironmentColorGreen)
env, err = c.Environments.Update(ctx, env)
require.NoError(t, err)
assert.Equal(t, e.ID, env.ID)
assert.Equal(t, newDescription, env.Description)
assert.Equal(t, EnvironmentColorGreen, *env.Color)

// try to delete the environment with delete protection enabled
var de hnyclient.DetailedError
err = c.Environments.Delete(ctx, env.ID)
require.ErrorAs(t, err, &de)
assert.Equal(t, http.StatusConflict, de.Status)

// disable deletion protection and delete the Environment
_, err = c.Environments.Update(ctx, &Environment{
ID: env.ID,
Settings: &EnvironmentSettings{
DeleteProtected: helper.ToPtr(false),
},
})
require.NoError(t, err)
err = c.Environments.Delete(ctx, env.ID)
require.NoError(t, err)

// verify the Environment was deleted
_, err = c.Environments.Get(ctx, env.ID)
require.ErrorAs(t, err, &de)
assert.True(t, de.IsNotFound())
})
}

func TestClient_Environments_Pagination(t *testing.T) {
ctx := context.Background()
c := newTestClient(t)

// create a bunch of environments
numEnvs := int(math.Floor(1.5 * float64(defaultPageSize)))
testEnvs := make([]*Environment, numEnvs)
for i := 0; i < numEnvs; i++ {
e, err := c.Environments.Create(ctx, &Environment{
Name: test.RandomStringWithPrefix("test.", 20),
})
require.NoError(t, err)
testEnvs[i] = e
}
t.Cleanup(func() {
for _, e := range testEnvs {
c.Environments.Update(ctx, &Environment{
ID: e.ID,
Settings: &EnvironmentSettings{
DeleteProtected: helper.ToPtr(false),
},
})
c.Environments.Delete(ctx, e.ID)
}
})

t.Run("happy path", func(t *testing.T) {
envs := make([]*Environment, 0)
pager, err := c.Environments.List(ctx)
require.NoError(t, err)

items, err := pager.Next(ctx)
require.NoError(t, err)
assert.Len(t, items, defaultPageSize, "incorrect number of items")
assert.True(t, pager.HasNext(), "should have more pages")
envs = append(envs, items...)

for pager.HasNext() {
items, err = pager.Next(ctx)
require.NoError(t, err)
envs = append(envs, items...)
}
// we can't guarantee that there are exactly numEnvs environments
assert.GreaterOrEqual(t, len(envs), numEnvs, "should have at least %d environments", numEnvs)
})

t.Run("works with custom page size", func(t *testing.T) {
pageSize := 5
pager, err := c.Environments.List(ctx, PageSize(pageSize))
require.NoError(t, err)

items, err := pager.Next(ctx)
require.NoError(t, err)
assert.Len(t, items, pageSize, "incorrect number of items")
assert.True(t, pager.HasNext(), "should have more pages")
})
}
13 changes: 10 additions & 3 deletions client/v2/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,16 @@ import (
)

type Environment struct {
ID string `jsonapi:"primary,environments"`
Name string `jsonapi:"attr,name"`
Slug string `jsonapi:"attr,slug"`
ID string `jsonapi:"primary,environments"`
Name string `jsonapi:"attr,name"`
Slug string `jsonapi:"attr,slug"`
Description *string `jsonapi:"attr,description,omitempty"`
Color *string `jsonapi:"attr,color,omitempty"`
Settings *EnvironmentSettings `jsonapi:"attr,settings,omitempty"`
}

type EnvironmentSettings struct {
DeleteProtected *bool `json:"delete_protected" jsonapi:"attr,delete_protected,omitempty"`
}

type Team struct {
Expand Down
33 changes: 33 additions & 0 deletions docs/data-sources/environment.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Data Source: honeycombio_environment

The `honeycombio_environment` data source retrieves the details of a single Environment.
If you want to retrieve multiple Environments, use the `honeycombio_environments` data source instead.

-> **NOTE** This data source requires the provider be configured with a Management Key with `environments:read` in the configured scopes.


## Example Usage

```hcl
# Retrieve the details of an Environment
data "honeycombio_environment" "prod" {
id = "hcaen_01j1d7t02zf7wgw7q89z3t60vf"
}
```

## Argument Reference

The following arguments are supported:

* `id` - (Required) The ID of the Environment

## Attribute Reference

In addition to all arguments above, the following attributes are exported:

* `name` - the Environment's name.
* `slug` - the Environment's slug.
* `description` - the Environment's description.
* `color` - the Environment's color.
* `delete_protected` - the current state of the Environment's deletion protection status.

Loading

0 comments on commit 1018f58

Please sign in to comment.