helper/schema: Provisioner support
This commit is contained in:
parent
96884ec42d
commit
b2891bc9ef
|
@ -0,0 +1,173 @@
|
||||||
|
package schema
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/hashicorp/go-multierror"
|
||||||
|
"github.com/hashicorp/terraform/config"
|
||||||
|
"github.com/hashicorp/terraform/terraform"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Provisioner represents a resource provisioner in Terraform and properly
|
||||||
|
// implements all of the ResourceProvisioner API.
|
||||||
|
//
|
||||||
|
// This higher level structure makes it much easier to implement a new or
|
||||||
|
// custom provisioner for Terraform.
|
||||||
|
//
|
||||||
|
// The function callbacks for this structure are all passed a context object.
|
||||||
|
// This context object has a number of pre-defined values that can be accessed
|
||||||
|
// via the global functions defined in context.go.
|
||||||
|
type Provisioner struct {
|
||||||
|
// ConnSchema is the schema for the connection settings for this
|
||||||
|
// provisioner.
|
||||||
|
//
|
||||||
|
// The keys of this map are the configuration keys, and the value is
|
||||||
|
// the schema describing the value of the configuration.
|
||||||
|
//
|
||||||
|
// NOTE: The value of connection keys can only be strings for now.
|
||||||
|
ConnSchema map[string]*Schema
|
||||||
|
|
||||||
|
// Schema is the schema for the usage of this provisioner.
|
||||||
|
//
|
||||||
|
// The keys of this map are the configuration keys, and the value is
|
||||||
|
// the schema describing the value of the configuration.
|
||||||
|
Schema map[string]*Schema
|
||||||
|
|
||||||
|
// ApplyFunc is the function for executing the provisioner. This is required.
|
||||||
|
// It is given a context. See the Provisioner struct docs for more
|
||||||
|
// information.
|
||||||
|
ApplyFunc func(ctx context.Context) error
|
||||||
|
|
||||||
|
stopCtx context.Context
|
||||||
|
stopCtxCancel context.CancelFunc
|
||||||
|
stopOnce sync.Once
|
||||||
|
}
|
||||||
|
|
||||||
|
// These constants are the keys that can be used to access data in
|
||||||
|
// the context parameters for Provisioners.
|
||||||
|
const (
|
||||||
|
connDataInvalid int = iota
|
||||||
|
|
||||||
|
// This returns a *ResourceData for the connection information.
|
||||||
|
// Guaranteed to never be nil.
|
||||||
|
ProvConnDataKey
|
||||||
|
|
||||||
|
// This returns a *ResourceData for the config information.
|
||||||
|
// Guaranteed to never be nil.
|
||||||
|
ProvConfigDataKey
|
||||||
|
|
||||||
|
// This returns a terraform.UIOutput. Guaranteed to never be nil.
|
||||||
|
ProvOutputKey
|
||||||
|
)
|
||||||
|
|
||||||
|
// InternalValidate should be called to validate the structure
|
||||||
|
// of the provisioner.
|
||||||
|
//
|
||||||
|
// This should be called in a unit test to verify before release that this
|
||||||
|
// structure is properly configured for use.
|
||||||
|
func (p *Provisioner) InternalValidate() error {
|
||||||
|
if p == nil {
|
||||||
|
return errors.New("provisioner is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
var validationErrors error
|
||||||
|
{
|
||||||
|
sm := schemaMap(p.ConnSchema)
|
||||||
|
if err := sm.InternalValidate(sm); err != nil {
|
||||||
|
validationErrors = multierror.Append(validationErrors, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
sm := schemaMap(p.Schema)
|
||||||
|
if err := sm.InternalValidate(sm); err != nil {
|
||||||
|
validationErrors = multierror.Append(validationErrors, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.ApplyFunc == nil {
|
||||||
|
validationErrors = multierror.Append(validationErrors, fmt.Errorf(
|
||||||
|
"ApplyFunc must not be nil"))
|
||||||
|
}
|
||||||
|
|
||||||
|
return validationErrors
|
||||||
|
}
|
||||||
|
|
||||||
|
// StopContext returns a context that checks whether a provisioner is stopped.
|
||||||
|
func (p *Provisioner) StopContext() context.Context {
|
||||||
|
p.stopOnce.Do(p.stopInit)
|
||||||
|
return p.stopCtx
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Provisioner) stopInit() {
|
||||||
|
p.stopCtx, p.stopCtxCancel = context.WithCancel(context.Background())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop implementation of terraform.ResourceProvisioner interface.
|
||||||
|
func (p *Provisioner) Stop() error {
|
||||||
|
p.stopOnce.Do(p.stopInit)
|
||||||
|
p.stopCtxCancel()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Provisioner) Validate(c *terraform.ResourceConfig) ([]string, []error) {
|
||||||
|
return schemaMap(p.Schema).Validate(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply implementation of terraform.ResourceProvisioner interface.
|
||||||
|
func (p *Provisioner) Apply(
|
||||||
|
o terraform.UIOutput,
|
||||||
|
s *terraform.InstanceState,
|
||||||
|
c *terraform.ResourceConfig) error {
|
||||||
|
var connData, configData *ResourceData
|
||||||
|
|
||||||
|
{
|
||||||
|
// We first need to turn the connection information into a
|
||||||
|
// terraform.ResourceConfig so that we can use that type to more
|
||||||
|
// easily build a ResourceData structure. We do this by simply treating
|
||||||
|
// the conn info as configuration input.
|
||||||
|
raw := make(map[string]interface{})
|
||||||
|
for k, v := range s.Ephemeral.ConnInfo {
|
||||||
|
raw[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err := config.NewRawConfig(raw)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
sm := schemaMap(p.ConnSchema)
|
||||||
|
diff, err := sm.Diff(nil, terraform.NewResourceConfig(c))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
connData, err = sm.Data(nil, diff)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// Build the configuration data. Doing this requires making a "diff"
|
||||||
|
// even though that's never used. We use that just to get the correct types.
|
||||||
|
configMap := schemaMap(p.Schema)
|
||||||
|
diff, err := configMap.Diff(nil, c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
configData, err = configMap.Data(nil, diff)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the context and call the function
|
||||||
|
ctx := p.StopContext()
|
||||||
|
ctx = context.WithValue(ctx, ProvConnDataKey, connData)
|
||||||
|
ctx = context.WithValue(ctx, ProvConfigDataKey, configData)
|
||||||
|
ctx = context.WithValue(ctx, ProvOutputKey, o)
|
||||||
|
return p.ApplyFunc(ctx)
|
||||||
|
}
|
|
@ -0,0 +1,241 @@
|
||||||
|
package schema
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/config"
|
||||||
|
"github.com/hashicorp/terraform/terraform"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestProvisioner_impl(t *testing.T) {
|
||||||
|
var _ terraform.ResourceProvisioner = new(Provisioner)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProvisionerValidate(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
Name string
|
||||||
|
P *Provisioner
|
||||||
|
Config map[string]interface{}
|
||||||
|
Err bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"Basic required field",
|
||||||
|
&Provisioner{
|
||||||
|
Schema: map[string]*Schema{
|
||||||
|
"foo": &Schema{
|
||||||
|
Required: true,
|
||||||
|
Type: TypeString,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"Basic required field set",
|
||||||
|
&Provisioner{
|
||||||
|
Schema: map[string]*Schema{
|
||||||
|
"foo": &Schema{
|
||||||
|
Required: true,
|
||||||
|
Type: TypeString,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
map[string]interface{}{
|
||||||
|
"foo": "bar",
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, tc := range cases {
|
||||||
|
t.Run(fmt.Sprintf("%d-%s", i, tc.Name), func(t *testing.T) {
|
||||||
|
c, err := config.NewRawConfig(tc.Config)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, es := tc.P.Validate(terraform.NewResourceConfig(c))
|
||||||
|
if len(es) > 0 != tc.Err {
|
||||||
|
t.Fatalf("%d: %#v", i, es)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProvisionerApply(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
Name string
|
||||||
|
P *Provisioner
|
||||||
|
Conn map[string]string
|
||||||
|
Config map[string]interface{}
|
||||||
|
Err bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"Basic config",
|
||||||
|
&Provisioner{
|
||||||
|
ConnSchema: map[string]*Schema{
|
||||||
|
"foo": &Schema{
|
||||||
|
Type: TypeString,
|
||||||
|
Optional: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
Schema: map[string]*Schema{
|
||||||
|
"foo": &Schema{
|
||||||
|
Type: TypeInt,
|
||||||
|
Optional: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
ApplyFunc: func(ctx context.Context) error {
|
||||||
|
cd := ctx.Value(ProvConnDataKey).(*ResourceData)
|
||||||
|
d := ctx.Value(ProvConfigDataKey).(*ResourceData)
|
||||||
|
if d.Get("foo").(int) != 42 {
|
||||||
|
return fmt.Errorf("bad config data")
|
||||||
|
}
|
||||||
|
if cd.Get("foo").(string) != "bar" {
|
||||||
|
return fmt.Errorf("bad conn data")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
map[string]string{
|
||||||
|
"foo": "bar",
|
||||||
|
},
|
||||||
|
map[string]interface{}{
|
||||||
|
"foo": 42,
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, tc := range cases {
|
||||||
|
t.Run(fmt.Sprintf("%d-%s", i, tc.Name), func(t *testing.T) {
|
||||||
|
c, err := config.NewRawConfig(tc.Config)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
state := &terraform.InstanceState{
|
||||||
|
Ephemeral: terraform.EphemeralState{
|
||||||
|
ConnInfo: tc.Conn,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err = tc.P.Apply(
|
||||||
|
nil, state, terraform.NewResourceConfig(c))
|
||||||
|
if err != nil != tc.Err {
|
||||||
|
t.Fatalf("%d: %s", i, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProvisionerStop(t *testing.T) {
|
||||||
|
var p Provisioner
|
||||||
|
|
||||||
|
// Verify stopch blocks
|
||||||
|
ch := p.StopContext().Done()
|
||||||
|
select {
|
||||||
|
case <-ch:
|
||||||
|
t.Fatal("should not be stopped")
|
||||||
|
case <-time.After(10 * time.Millisecond):
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop it
|
||||||
|
if err := p.Stop(); err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ch:
|
||||||
|
case <-time.After(10 * time.Millisecond):
|
||||||
|
t.Fatal("should be stopped")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProvisionerStop_apply(t *testing.T) {
|
||||||
|
p := &Provisioner{
|
||||||
|
ConnSchema: map[string]*Schema{
|
||||||
|
"foo": &Schema{
|
||||||
|
Type: TypeString,
|
||||||
|
Optional: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
Schema: map[string]*Schema{
|
||||||
|
"foo": &Schema{
|
||||||
|
Type: TypeInt,
|
||||||
|
Optional: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
ApplyFunc: func(ctx context.Context) error {
|
||||||
|
<-ctx.Done()
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
conn := map[string]string{
|
||||||
|
"foo": "bar",
|
||||||
|
}
|
||||||
|
|
||||||
|
conf := map[string]interface{}{
|
||||||
|
"foo": 42,
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err := config.NewRawConfig(conf)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
state := &terraform.InstanceState{
|
||||||
|
Ephemeral: terraform.EphemeralState{
|
||||||
|
ConnInfo: conn,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the apply in a goroutine
|
||||||
|
doneCh := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
p.Apply(nil, state, terraform.NewResourceConfig(c))
|
||||||
|
close(doneCh)
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Should block
|
||||||
|
select {
|
||||||
|
case <-doneCh:
|
||||||
|
t.Fatal("should not be done")
|
||||||
|
case <-time.After(10 * time.Millisecond):
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop!
|
||||||
|
p.Stop()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-doneCh:
|
||||||
|
case <-time.After(10 * time.Millisecond):
|
||||||
|
t.Fatal("should be done")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProvisionerStop_stopFirst(t *testing.T) {
|
||||||
|
var p Provisioner
|
||||||
|
|
||||||
|
// Stop it
|
||||||
|
if err := p.Stop(); err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-p.StopContext().Done():
|
||||||
|
case <-time.After(10 * time.Millisecond):
|
||||||
|
t.Fatal("should be stopped")
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue