Create Watchtower Project Resources (#1)
* Add watchtower provider using the auto generated go sdk. * Add read and create for project resources. * Add provider internal validate test. * Update the project module to be terraform-provider-watchtower * Organize the ResourceData to Project and vis versa logic into their own helper methods. * Adding tests for going ResourceData to Project and vis versa. * provider_update * Single acceptance test written and correctly failing. * Implement update and delete. Always run Acceptance Testing * Move project field names into constants. * Allow tests to use the apiError response for checking error code. * Correcting the naming of key value consts. * Rely on "make testacc" for acceptance testing instead of always running it. * Updating dependencies to use the new TestController creation.
This commit is contained in:
1 parent
a9af48caee
commit
5bb8c82819
9 files changed
+1870
-66
No files matched your search
@@ -1,5 +1,34 @@
|
||||
module github.com/hashicorp/terraform-provider-scaffolding
|
||||
module github.com/hashicorp/terraform-provider-watchtower
|
||||
|
||||
go 1.12
|
||||
|
||||
require github.com/hashicorp/terraform-plugin-sdk v1.4.1
|
||||
require (
|
||||
cloud.google.com/go/storage v1.8.0 // indirect
|
||||
github.com/agext/levenshtein v1.2.3 // indirect
|
||||
github.com/aws/aws-sdk-go v1.31.5 // indirect
|
||||
github.com/google/go-cmp v0.4.1 // indirect
|
||||
github.com/hashicorp/go-getter v1.4.1 // indirect
|
||||
github.com/hashicorp/go-hclog v0.14.0 // indirect
|
||||
github.com/hashicorp/go-plugin v1.3.0 // indirect
|
||||
github.com/hashicorp/hcl/v2 v2.5.1 // indirect
|
||||
github.com/hashicorp/terraform-plugin-sdk v1.13.0
|
||||
github.com/hashicorp/terraform-svchost v0.0.0-20191119180714-d2e4933b9136 // indirect
|
||||
github.com/hashicorp/vault-plugin-secrets-ad v0.6.5 // indirect
|
||||
github.com/hashicorp/watchtower v0.0.0-20200526204621-44152ae63ead
|
||||
github.com/hashicorp/yamux v0.0.0-20190923154419-df201c70410d // indirect
|
||||
github.com/mitchellh/go-testing-interface v1.14.1 // indirect
|
||||
github.com/mitchellh/mapstructure v1.3.1 // indirect
|
||||
github.com/oklog/run v1.1.0 // indirect
|
||||
github.com/sergi/go-diff v1.1.0 // indirect
|
||||
github.com/stretchr/testify v1.5.1
|
||||
github.com/ulikunitz/xz v0.5.7 // indirect
|
||||
github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect
|
||||
github.com/zclconf/go-cty v1.4.1 // indirect
|
||||
golang.org/x/mod v0.3.0 // indirect
|
||||
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2 // indirect
|
||||
golang.org/x/sys v0.0.0-20200523222454-059865788121 // indirect
|
||||
golang.org/x/tools v0.0.0-20200526205600-253fce384c97 // indirect
|
||||
google.golang.org/api v0.25.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20200526151428-9bb895338b15 // indirect
|
||||
honnef.co/go/tools v0.0.1-2020.1.4 // indirect
|
||||
)
|
||||
@@ -1,22 +0,0 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
|
||||
)
|
||||
|
||||
func dataSourceScaffolding() *schema.Resource {
|
||||
return &schema.Resource{
|
||||
Read: dataSourceScaffoldingRead,
|
||||
|
||||
Schema: map[string]*schema.Schema{
|
||||
"sample_attribute": {
|
||||
Type: schema.TypeString,
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func dataSourceScaffoldingRead(d *schema.ResourceData, meta interface{}) error {
|
||||
return nil
|
||||
}
|
||||
@@ -1,17 +1,57 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
|
||||
"github.com/hashicorp/terraform-plugin-sdk/terraform"
|
||||
"github.com/hashicorp/watchtower/api"
|
||||
)
|
||||
|
||||
func New() terraform.ResourceProvider {
|
||||
return &schema.Provider{
|
||||
DataSourcesMap: map[string]*schema.Resource{
|
||||
"scaffolding_data_source": dataSourceScaffolding(),
|
||||
p := &schema.Provider{
|
||||
Schema: map[string]*schema.Schema{
|
||||
"default_organization": {
|
||||
Type: schema.TypeString,
|
||||
Optional: true,
|
||||
DefaultFunc: schema.EnvDefaultFunc("WATCHTOWER_DEFAULT_ORG", ""),
|
||||
Description: "The Watchtower organization scope to operate all actions in if not provided in the individual resources.",
|
||||
},
|
||||
"base_url": {
|
||||
Type: schema.TypeString,
|
||||
Required: true,
|
||||
Description: "The base url of the Watchtower API. For example 'http://127.0.0.1/'",
|
||||
},
|
||||
},
|
||||
ResourcesMap: map[string]*schema.Resource{
|
||||
"scaffolding_resource": resourceScaffolding(),
|
||||
"watchtower_project": resourceProject(),
|
||||
},
|
||||
}
|
||||
|
||||
p.ConfigureFunc = providerConfigure(p)
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
type metaData struct {
|
||||
client *api.Client
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
func providerConfigure(p *schema.Provider) schema.ConfigureFunc {
|
||||
return func(d *schema.ResourceData) (interface{}, error) {
|
||||
client, err := api.NewClient(nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := client.SetAddr(d.Get("base_url").(string)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
client.SetOrg(d.Get("default_organization").(string))
|
||||
|
||||
// TODO: Pass these in through the config, add token, etc...
|
||||
client.SetLimiter(5, 5)
|
||||
|
||||
return &metaData{client: client, ctx: p.StopContext()}, nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
|
||||
"github.com/hashicorp/terraform-plugin-sdk/terraform"
|
||||
)
|
||||
|
||||
var testProvider *schema.Provider
|
||||
var testProviders map[string]terraform.ResourceProvider
|
||||
|
||||
func init() {
|
||||
testProvider = New().(*schema.Provider)
|
||||
testProviders = map[string]terraform.ResourceProvider{
|
||||
"watchtower": testProvider,
|
||||
}
|
||||
}
|
||||
|
||||
func TestProvider(t *testing.T) {
|
||||
if err := New().(*schema.Provider).InternalValidate(); err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
|
||||
"github.com/hashicorp/watchtower/api/scopes"
|
||||
)
|
||||
|
||||
const (
|
||||
projectDescriptionKey = "description"
|
||||
projectNameKey = "name"
|
||||
)
|
||||
|
||||
func resourceProject() *schema.Resource {
|
||||
return &schema.Resource{
|
||||
Create: resourceProjectCreate,
|
||||
Read: resourceProjectRead,
|
||||
Update: resourceProjectUpdate,
|
||||
Delete: resourceProjectDelete,
|
||||
|
||||
// TODO: Add the ability to define a parent org instead of using one defined in the provider.
|
||||
Schema: map[string]*schema.Schema{
|
||||
projectNameKey: {
|
||||
Type: schema.TypeString,
|
||||
Optional: true,
|
||||
},
|
||||
projectDescriptionKey: {
|
||||
Type: schema.TypeString,
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// convertProjectToResourceData populates the provided ResourceData with the appropriate values from the provided Project.
|
||||
// The project passed into thie function should be one read from the watchtower API with all fields populated.
|
||||
func convertProjectToResourceData(p *scopes.Project, d *schema.ResourceData) error {
|
||||
if p.Name != nil {
|
||||
if err := d.Set(projectNameKey, p.Name); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if p.Description != nil {
|
||||
if err := d.Set(projectDescriptionKey, p.Description); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
d.SetId(p.Id)
|
||||
return nil
|
||||
}
|
||||
|
||||
// convertResourceDataToProject returns a localy built Project using the values provided in the ResourceData.
|
||||
func convertResourceDataToProject(d *schema.ResourceData) *scopes.Project {
|
||||
p := &scopes.Project{}
|
||||
if descVal, ok := d.GetOk(projectDescriptionKey); ok {
|
||||
desc := descVal.(string)
|
||||
p.Description = &desc
|
||||
}
|
||||
if nameVal, ok := d.GetOk(projectNameKey); ok {
|
||||
name := nameVal.(string)
|
||||
p.Name = &name
|
||||
}
|
||||
if d.Id() != "" {
|
||||
p.Id = d.Id()
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
func resourceProjectCreate(d *schema.ResourceData, meta interface{}) error {
|
||||
md := meta.(*metaData)
|
||||
client := md.client
|
||||
ctx := md.ctx
|
||||
|
||||
// The org id is declared in the client, so no need to specify that here.
|
||||
o := &scopes.Organization{
|
||||
Client: client,
|
||||
}
|
||||
p := convertResourceDataToProject(d)
|
||||
p, _, err := o.CreateProject(ctx, p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.SetId(p.Id)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func resourceProjectRead(d *schema.ResourceData, meta interface{}) error {
|
||||
md := meta.(*metaData)
|
||||
client := md.client
|
||||
ctx := md.ctx
|
||||
|
||||
o := &scopes.Organization{
|
||||
Client: client,
|
||||
}
|
||||
p := &scopes.Project{Id: d.Id()}
|
||||
p, _, err := o.ReadProject(ctx, p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return convertProjectToResourceData(p, d)
|
||||
}
|
||||
|
||||
func resourceProjectUpdate(d *schema.ResourceData, meta interface{}) error {
|
||||
md := meta.(*metaData)
|
||||
client := md.client
|
||||
ctx := md.ctx
|
||||
|
||||
o := &scopes.Organization{
|
||||
Client: client,
|
||||
}
|
||||
p := &scopes.Project{
|
||||
Id: d.Id(),
|
||||
}
|
||||
|
||||
if d.HasChange(projectDescriptionKey) {
|
||||
desc := d.Get(projectDescriptionKey).(string)
|
||||
if desc == "" {
|
||||
p.SetDefault(projectDescriptionKey)
|
||||
} else {
|
||||
p.Description = &desc
|
||||
}
|
||||
}
|
||||
|
||||
if d.HasChange(projectNameKey) {
|
||||
name := d.Get(projectNameKey).(string)
|
||||
if name == "" {
|
||||
p.SetDefault(projectNameKey)
|
||||
} else {
|
||||
p.Name = &name
|
||||
}
|
||||
}
|
||||
|
||||
p, _, err := o.UpdateProject(ctx, p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return convertProjectToResourceData(p, d)
|
||||
}
|
||||
|
||||
func resourceProjectDelete(d *schema.ResourceData, meta interface{}) error {
|
||||
md := meta.(*metaData)
|
||||
client := md.client
|
||||
ctx := md.ctx
|
||||
|
||||
o := &scopes.Organization{
|
||||
Client: client,
|
||||
}
|
||||
p := convertResourceDataToProject(d)
|
||||
_, _, err := o.DeleteProject(ctx, p)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed deleting project: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-sdk/helper/resource"
|
||||
"github.com/hashicorp/terraform-plugin-sdk/terraform"
|
||||
"github.com/hashicorp/watchtower/api"
|
||||
"github.com/hashicorp/watchtower/api/scopes"
|
||||
"github.com/hashicorp/watchtower/testing/controller"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestAccProjectCreation(t *testing.T) {
|
||||
tc := controller.NewTestController(t, controller.WithDefaultOrgId("o_0000000000"))
|
||||
defer tc.Shutdown()
|
||||
url := tc.ApiAddrs()[0]
|
||||
|
||||
resource.Test(t, resource.TestCase{
|
||||
Providers: testProviders,
|
||||
CheckDestroy: testAccCheckProjectResourceDestroy(t),
|
||||
Steps: []resource.TestStep{
|
||||
{
|
||||
Config: testConfig(url, firstProjectBar, secondProject),
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
testAccCheckProjectResourceExists("watchtower_project.project1"),
|
||||
resource.TestCheckResourceAttr("watchtower_project.project1", projectDescriptionKey, "bar"),
|
||||
resource.TestCheckResourceAttr("watchtower_project.project2", projectDescriptionKey, "project2"),
|
||||
),
|
||||
},
|
||||
// Updates the first project to have description foo
|
||||
{
|
||||
Config: testConfig(url, firstProjectFoo, secondProject),
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
testAccCheckProjectResourceExists("watchtower_project.project1"),
|
||||
resource.TestCheckResourceAttr("watchtower_project.project1", projectDescriptionKey, "foo"),
|
||||
resource.TestCheckResourceAttr("watchtower_project.project2", projectDescriptionKey, "project2"),
|
||||
),
|
||||
},
|
||||
// Remove second project
|
||||
{
|
||||
Config: testConfig(url, firstProjectFoo),
|
||||
Check: resource.ComposeTestCheckFunc(
|
||||
testAccCheckProjectResourceExists("watchtower_project.project1"),
|
||||
resource.TestCheckResourceAttr("watchtower_project.project1", projectDescriptionKey, "foo"),
|
||||
),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func testAccCheckProjectResourceExists(name string) resource.TestCheckFunc {
|
||||
return func(s *terraform.State) error {
|
||||
rs, ok := s.RootModule().Resources[name]
|
||||
if !ok {
|
||||
return fmt.Errorf("Not found: %s", name)
|
||||
}
|
||||
id := rs.Primary.ID
|
||||
if id == "" {
|
||||
return fmt.Errorf("No ID is set")
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(id, "p_") {
|
||||
return fmt.Errorf("ID not formatted as expected")
|
||||
}
|
||||
md := testProvider.Meta().(*metaData)
|
||||
o := scopes.Organization{
|
||||
Client: md.client,
|
||||
}
|
||||
if _, _, err := o.ReadProject(md.ctx, &scopes.Project{Id: id}); err != nil {
|
||||
return fmt.Errorf("Got an error when reading project %q: %v", id, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func testAccCheckProjectResourceDestroy(t *testing.T) resource.TestCheckFunc {
|
||||
return func(s *terraform.State) error {
|
||||
// retrieve the connection established in Provider configuration
|
||||
md := testProvider.Meta().(*metaData)
|
||||
o := scopes.Organization{
|
||||
Client: md.client,
|
||||
}
|
||||
|
||||
for _, rs := range s.RootModule().Resources {
|
||||
id := rs.Primary.ID
|
||||
switch rs.Type {
|
||||
case "watchtower_project":
|
||||
if _, apiErr, _ := o.ReadProject(md.ctx, &scopes.Project{Id: id}); apiErr == nil || *apiErr.Status != http.StatusNotFound {
|
||||
return fmt.Errorf("Didn't get a 404 when reading destroyed project %q: %v", id, apiErr)
|
||||
}
|
||||
default:
|
||||
t.Logf("Got unknown resource type %q", rs.Type)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func TestResourceDataToProject(t *testing.T) {
|
||||
rp := resourceProject()
|
||||
testCases := []struct {
|
||||
name string
|
||||
rData map[string]interface{}
|
||||
expected *scopes.Project
|
||||
}{
|
||||
{
|
||||
name: "Fully populated",
|
||||
rData: map[string]interface{}{
|
||||
projectNameKey: "name",
|
||||
projectDescriptionKey: "desc",
|
||||
},
|
||||
expected: &scopes.Project{
|
||||
Name: api.String("name"),
|
||||
Description: api.String("desc"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Name populated",
|
||||
rData: map[string]interface{}{
|
||||
projectNameKey: "name",
|
||||
},
|
||||
expected: &scopes.Project{
|
||||
Name: api.String("name"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Description populated",
|
||||
rData: map[string]interface{}{
|
||||
projectDescriptionKey: "desc",
|
||||
},
|
||||
expected: &scopes.Project{
|
||||
Description: api.String("desc"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Not populated",
|
||||
rData: map[string]interface{}{},
|
||||
expected: &scopes.Project{},
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
rd := rp.TestResourceData()
|
||||
for k, v := range tc.rData {
|
||||
err := rd.Set(k, v)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
assert.Equal(t, tc.expected, convertResourceDataToProject(rd))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProjectToResourceData(t *testing.T) {
|
||||
rp := resourceProject()
|
||||
testCases := []struct {
|
||||
name string
|
||||
expected map[string]interface{}
|
||||
proj *scopes.Project
|
||||
}{
|
||||
{
|
||||
name: "Fully populated",
|
||||
proj: &scopes.Project{
|
||||
Id: "someid",
|
||||
Name: api.String("name"),
|
||||
Description: api.String("desc"),
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
projectNameKey: "name",
|
||||
projectDescriptionKey: "desc",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Name populated",
|
||||
proj: &scopes.Project{
|
||||
Id: "someid",
|
||||
Name: api.String("name"),
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
projectNameKey: "name",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Description populated",
|
||||
proj: &scopes.Project{
|
||||
Id: "someid",
|
||||
Description: api.String("desc"),
|
||||
},
|
||||
expected: map[string]interface{}{
|
||||
projectDescriptionKey: "desc",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Not populated",
|
||||
proj: &scopes.Project{
|
||||
Id: "someid",
|
||||
},
|
||||
expected: map[string]interface{}{},
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
expectedRd := rp.TestResourceData()
|
||||
expectedRd.SetId("someid")
|
||||
for k, v := range tc.expected {
|
||||
err := expectedRd.Set(k, v)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
actual := rp.TestResourceData()
|
||||
err := convertProjectToResourceData(tc.proj, actual)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expectedRd, actual)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
firstProjectFoo = `
|
||||
resource "watchtower_project" "project1" {
|
||||
description = "foo"
|
||||
}`
|
||||
|
||||
firstProjectBar = `
|
||||
resource "watchtower_project" "project1" {
|
||||
description = "bar"
|
||||
}`
|
||||
|
||||
secondProject = `
|
||||
resource "watchtower_project" "project2" {
|
||||
description = "project2"
|
||||
}
|
||||
`
|
||||
)
|
||||
|
||||
func testConfig(url string, res ...string) string {
|
||||
provider := fmt.Sprintf(`
|
||||
provider "watchtower" {
|
||||
base_url = "%s"
|
||||
default_organization = "o_0000000000"
|
||||
}`, url)
|
||||
c := []string{provider}
|
||||
c = append(c, res...)
|
||||
return strings.Join(c, "\n")
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
|
||||
)
|
||||
|
||||
func resourceScaffolding() *schema.Resource {
|
||||
return &schema.Resource{
|
||||
Create: resourceScaffoldingCreate,
|
||||
Read: resourceScaffoldingRead,
|
||||
Update: resourceScaffoldingUpdate,
|
||||
Delete: resourceScaffoldingDelete,
|
||||
|
||||
Schema: map[string]*schema.Schema{
|
||||
"sample_attribute": {
|
||||
Type: schema.TypeString,
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func resourceScaffoldingCreate(d *schema.ResourceData, meta interface{}) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func resourceScaffoldingRead(d *schema.ResourceData, meta interface{}) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func resourceScaffoldingUpdate(d *schema.ResourceData, meta interface{}) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func resourceScaffoldingDelete(d *schema.ResourceData, meta interface{}) error {
|
||||
return nil
|
||||
}
|
||||
Reference in new issue
Block a user