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:
Todd Knight authored and GitHub committed 2020-05-29 09:13:31 -07:00
1 parent a9af48caee
commit 5bb8c82819
9 files changed
+1870 -66

No files matched your search

+31 -2
View File
@@ -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
)
+1364
View File
File diff suppressed because it is too large. Load diff
@@ -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
}
+44 -4
View File
@@ -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
}
}
+24
View File
@@ -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)
}
}
+157
View File
@@ -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
}
+249
View File
@@ -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")
}
-37
View File
@@ -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
}
+1 -1
View File
@@ -2,7 +2,7 @@ package main
import (
"github.com/hashicorp/terraform-plugin-sdk/plugin"
"github.com/hashicorp/terraform-provider-scaffolding/internal/provider"
"github.com/hashicorp/terraform-provider-watchtower/internal/provider"
)
func main() {