opentofu/terraform/terraform.go
2014-07-02 16:16:38 -07:00

475 lines
10 KiB
Go

package terraform
import (
"errors"
"fmt"
"log"
"sync"
"sync/atomic"
"github.com/hashicorp/terraform/config"
"github.com/hashicorp/terraform/depgraph"
)
// Terraform is the primary structure that is used to interact with
// Terraform from code, and can perform operations such as returning
// all resources, a resource tree, a specific resource, etc.
type Terraform struct {
hooks []Hook
providers map[string]ResourceProviderFactory
stopHook *stopHook
}
// This is a function type used to implement a walker for the resource
// tree internally on the Terraform structure.
type genericWalkFunc func(*Resource) (map[string]string, error)
// genericWalkStop is a special return value that can be returned from a
// genericWalkFunc that causes the walk to cease immediately.
var genericWalkStop error
// Config is the configuration that must be given to instantiate
// a Terraform structure.
type Config struct {
Hooks []Hook
Providers map[string]ResourceProviderFactory
}
func init() {
genericWalkStop = errors.New("genericWalkStop")
}
// New creates a new Terraform structure, initializes resource providers
// for the given configuration, etc.
//
// Semantic checks of the entire configuration structure are done at this
// time, as well as richer checks such as verifying that the resource providers
// can be properly initialized, can be configured, etc.
func New(c *Config) (*Terraform, error) {
sh := new(stopHook)
sh.Lock()
sh.reset()
sh.Unlock()
// Copy all the hooks and add our stop hook. We don't append directly
// to the Config so that we're not modifying that in-place.
hooks := make([]Hook, len(c.Hooks)+1)
copy(hooks, c.Hooks)
hooks[len(c.Hooks)] = sh
return &Terraform{
hooks: hooks,
stopHook: sh,
providers: c.Providers,
}, nil
}
func (t *Terraform) Apply(p *Plan) (*State, error) {
// Increase the count on the stop hook so we know when to stop
serial := t.stopHook.ref()
defer t.stopHook.unref(serial)
// Make sure we're working with a plan that doesn't have null pointers
// everywhere, and is instead just empty otherwise.
p.init()
g, err := Graph(&GraphOpts{
Config: p.Config,
Diff: p.Diff,
Providers: t.providers,
State: p.State,
})
if err != nil {
return nil, err
}
return t.apply(g, p)
}
// Stop stops all running tasks (applies, plans, refreshes).
//
// This will block until all running tasks are stopped. While Stop is
// blocked, any new calls to Apply, Plan, Refresh, etc. will also block. New
// calls, however, will start once this Stop has returned.
func (t *Terraform) Stop() {
log.Printf("[INFO] Terraform stopping tasks")
t.stopHook.Lock()
defer t.stopHook.Unlock()
// Setup the stoppedCh
stoppedCh := make(chan struct{}, t.stopHook.count)
t.stopHook.stoppedCh = stoppedCh
// Close the channel to signal that we're done
close(t.stopHook.ch)
// Expect the number of count stops...
log.Printf("[DEBUG] Waiting for %d tasks to stop", t.stopHook.count)
for i := 0; i < t.stopHook.count; i++ {
<-stoppedCh
}
log.Printf("[DEBUG] Stopped!")
// Success, everything stopped, reset everything
t.stopHook.reset()
}
func (t *Terraform) Plan(opts *PlanOpts) (*Plan, error) {
// Increase the count on the stop hook so we know when to stop
serial := t.stopHook.ref()
defer t.stopHook.unref(serial)
g, err := Graph(&GraphOpts{
Config: opts.Config,
Providers: t.providers,
State: opts.State,
})
if err != nil {
return nil, err
}
return t.plan(g, opts)
}
// Refresh goes through all the resources in the state and refreshes them
// to their latest status.
func (t *Terraform) Refresh(c *config.Config, s *State) (*State, error) {
// Increase the count on the stop hook so we know when to stop
serial := t.stopHook.ref()
defer t.stopHook.unref(serial)
g, err := Graph(&GraphOpts{
Config: c,
Providers: t.providers,
State: s,
})
if err != nil {
return s, err
}
return t.refresh(g)
}
func (t *Terraform) apply(
g *depgraph.Graph,
p *Plan) (*State, error) {
s := new(State)
err := g.Walk(t.applyWalkFn(s, p))
return s, err
}
func (t *Terraform) plan(g *depgraph.Graph, opts *PlanOpts) (*Plan, error) {
p := &Plan{
Config: opts.Config,
Vars: opts.Vars,
State: opts.State,
}
err := g.Walk(t.planWalkFn(p, opts))
return p, err
}
func (t *Terraform) refresh(g *depgraph.Graph) (*State, error) {
s := new(State)
err := g.Walk(t.refreshWalkFn(s))
return s, err
}
func (t *Terraform) refreshWalkFn(result *State) depgraph.WalkFunc {
var l sync.Mutex
// Initialize the result so we don't have to nil check everywhere
result.init()
cb := func(r *Resource) (map[string]string, error) {
for _, h := range t.hooks {
// TODO: return value
h.PreRefresh(r.Id, r.State)
}
rs, err := r.Provider.Refresh(r.State)
if err != nil {
return nil, err
}
if rs == nil {
rs = new(ResourceState)
}
// Fix the type to be the type we have
rs.Type = r.State.Type
l.Lock()
result.Resources[r.Id] = rs
l.Unlock()
for _, h := range t.hooks {
// TODO: return value
h.PostRefresh(r.Id, rs)
}
return nil, nil
}
return t.genericWalkFn(nil, cb)
}
func (t *Terraform) applyWalkFn(
result *State,
p *Plan) depgraph.WalkFunc {
var l sync.Mutex
// Initialize the result
result.init()
cb := func(r *Resource) (map[string]string, error) {
diff := r.Diff
if diff.Empty() {
return r.Vars(), nil
}
if !diff.Destroy {
var err error
diff, err = r.Provider.Diff(r.State, r.Config)
if err != nil {
return nil, err
}
}
// TODO(mitchellh): we need to verify the diff doesn't change
// anything and that the diff has no computed values (pre-computed)
for _, h := range t.hooks {
a, err := h.PreApply(r.Id, r.State, diff)
if err != nil {
return nil, err
}
switch a {
case HookActionHalt:
return nil, genericWalkStop
}
}
// With the completed diff, apply!
log.Printf("[DEBUG] %s: Executing Apply", r.Id)
rs, err := r.Provider.Apply(r.State, diff)
if err != nil {
return nil, err
}
// Make sure the result is instantiated
if rs == nil {
rs = new(ResourceState)
}
// Force the resource state type to be our type
rs.Type = r.State.Type
// If no state was returned, then no variables were updated so
// just return.
if rs == nil {
return nil, nil
}
var errs []error
for ak, av := range rs.Attributes {
// If the value is the unknown variable value, then it is an error.
// In this case we record the error and remove it from the state
if av == config.UnknownVariableValue {
errs = append(errs, fmt.Errorf(
"Attribute with unknown value: %s", ak))
delete(rs.Attributes, ak)
}
}
// Update the resulting diff
l.Lock()
result.Resources[r.Id] = rs
l.Unlock()
// Update the state for the resource itself
r.State = rs
for _, h := range t.hooks {
a, err := h.PostApply(r.Id, r.State)
if err != nil {
return nil, err
}
switch a {
case HookActionHalt:
return nil, genericWalkStop
}
}
// Determine the new state and update variables
err = nil
if len(errs) > 0 {
err = &MultiError{Errors: errs}
}
return r.Vars(), err
}
return t.genericWalkFn(p.Vars, cb)
}
func (t *Terraform) planWalkFn(result *Plan, opts *PlanOpts) depgraph.WalkFunc {
var l sync.Mutex
// Initialize the result
result.init()
cb := func(r *Resource) (map[string]string, error) {
var diff *ResourceDiff
for _, h := range t.hooks {
// TODO: return value
h.PreDiff(r.Id, r.State)
}
if opts.Destroy {
if r.State.ID != "" {
log.Printf("[DEBUG] %s: Making for destroy", r.Id)
diff = &ResourceDiff{Destroy: true}
} else {
log.Printf("[DEBUG] %s: Not marking for destroy, no ID", r.Id)
}
} else if r.Config == nil {
log.Printf("[DEBUG] %s: Orphan, marking for destroy", r.Id)
// This is an orphan (no config), so we mark it to be destroyed
diff = &ResourceDiff{Destroy: true}
} else {
log.Printf("[DEBUG] %s: Executing diff", r.Id)
// Get a diff from the newest state
var err error
diff, err = r.Provider.Diff(r.State, r.Config)
if err != nil {
return nil, err
}
}
l.Lock()
if !diff.Empty() {
result.Diff.Resources[r.Id] = diff
}
l.Unlock()
for _, h := range t.hooks {
// TODO: return value
h.PostDiff(r.Id, diff)
}
// Determine the new state and update variables
if !diff.Empty() {
r.State = r.State.MergeDiff(diff)
}
return r.Vars(), nil
}
return t.genericWalkFn(opts.Vars, cb)
}
func (t *Terraform) genericWalkFn(
invars map[string]string,
cb genericWalkFunc) depgraph.WalkFunc {
var l sync.RWMutex
// Initialize the variables for application
vars := make(map[string]string)
for k, v := range invars {
vars[fmt.Sprintf("var.%s", k)] = v
}
// This will keep track of whether we're stopped or not
var stop uint32 = 0
return func(n *depgraph.Noun) error {
// If it is the root node, ignore
if n.Name == GraphRootNode {
return nil
}
// If we're stopped, return right away
if atomic.LoadUint32(&stop) != 0 {
return nil
}
switch m := n.Meta.(type) {
case *GraphNodeResource:
case *GraphNodeResourceProvider:
var rc *ResourceConfig
if m.Config != nil {
if err := m.Config.RawConfig.Interpolate(vars); err != nil {
panic(err)
}
rc = NewResourceConfig(m.Config.RawConfig)
}
for k, p := range m.Providers {
log.Printf("[INFO] Configuring provider: %s", k)
err := p.Configure(rc)
if err != nil {
return err
}
}
return nil
}
rn := n.Meta.(*GraphNodeResource)
l.RLock()
if len(vars) > 0 && rn.Config != nil {
if err := rn.Config.RawConfig.Interpolate(vars); err != nil {
panic(fmt.Sprintf("Interpolate error: %s", err))
}
// Force the config to be set later
rn.Resource.Config = nil
}
l.RUnlock()
// Make sure that at least some resource configuration is set
if !rn.Orphan {
if rn.Resource.Config == nil {
if rn.Config == nil {
rn.Resource.Config = new(ResourceConfig)
} else {
rn.Resource.Config = NewResourceConfig(rn.Config.RawConfig)
}
}
} else {
rn.Resource.Config = nil
}
// Call the callack
log.Printf("[INFO] Walking: %s", rn.Resource.Id)
newVars, err := cb(rn.Resource)
if err != nil {
if err == genericWalkStop {
atomic.StoreUint32(&stop, 1)
return nil
}
return err
}
if len(newVars) > 0 {
// Acquire a lock since this function is called in parallel
l.Lock()
defer l.Unlock()
// Update variables
for k, v := range newVars {
vars[k] = v
}
}
return nil
}
}