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

Create Watchtower Project Resources #1

Merged
merged 14 commits into from
May 29, 2020
Merged
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
33 changes: 31 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
@@ -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,364 changes: 1,364 additions & 0 deletions go.sum

Large diffs are not rendered by default.

22 changes: 0 additions & 22 deletions internal/provider/data_source_scaffolding.go

This file was deleted.

48 changes: 44 additions & 4 deletions internal/provider/provider.go
Original file line number Diff line number Diff line change
@@ -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(),
Copy link
Contributor

Choose a reason for hiding this comment

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

Since the provider is called watchtower I vote to have the key here be just project so it doesn't duplicate the primary namespace of the provider.

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'm happy to make the change. I thought that since different providers can be included in a single tf config the resources needed to be named in a way that won't collide with resources from other providers. For example, all github related resources have a "github_" prefix and the google related resources have "google_" prefix.

Copy link
Contributor

Choose a reason for hiding this comment

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

On second thought, I agree with your assessment. This makes sense as-is, let's leave it.

},
}

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)
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we want to expose the SetLimiter values in the provider schema?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Maybe at first? I think in the long run we may want to set a reasonable initial default and then allow the controller and go client to handle backoffs and retries. Thoughts?


return &metaData{client: client, ctx: p.StopContext()}, nil
}
}
24 changes: 24 additions & 0 deletions internal/provider/provider_test.go
Original file line number Diff line number Diff line change
@@ -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 changes: 157 additions & 0 deletions internal/provider/resource_project.go
Original file line number Diff line number Diff line change
@@ -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.
Copy link
Contributor

Choose a reason for hiding this comment

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

This comment surfaced some interesting thinking about architecture here: do we want resources within an instance of this provider to be able to provision resources within another org not declared outside the provider instance?

If we do allow resources to be created within orgs that are not globally defined to the instance of the provider, we'll have to bake in org logic into every resource within the provider. There was a similar issue with Vault's provider and Namespace support.

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 made this TODO and changed the provider's schema key from "organization" to "default_organization" after a comment from @jefferai where he seemed to hint at the need to do multi org config in a single tf config. The go client does facilitate setting a default org for the whole client and allowing a per call override. I think that would just mean that the resource logic would be checking to see for an org defined at the resource level and if so, using that for the calls through the go client.

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
}
Loading