opentofu/terraform/terraform.go
2014-06-20 12:51:42 -07:00

326 lines
7.0 KiB
Go

package terraform
import (
"fmt"
"sync"
"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 {
config *config.Config
graph *depgraph.Graph
mapping map[*config.Resource]*terraformProvider
variables map[string]string
}
// terraformProvider contains internal state information about a resource
// provider for Terraform.
type terraformProvider struct {
Provider ResourceProvider
Config *config.ProviderConfig
sync.Once
}
// 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)
// Config is the configuration that must be given to instantiate
// a Terraform structure.
type Config struct {
Config *config.Config
Providers map[string]ResourceProviderFactory
Variables map[string]string
computedPlaceholder string
}
// 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) {
var errs []error
// Calculate the computed key placeholder
c.computedPlaceholder = "tf_computed_placeholder"
// Validate that all required variables have values
if err := smcVariables(c); err != nil {
errs = append(errs, err...)
}
// Match all the resources with a provider and initialize the providers
mapping, err := smcProviders(c)
if err != nil {
errs = append(errs, err...)
}
// Validate all the configurations, once.
tps := make(map[*terraformProvider]struct{})
for _, tp := range mapping {
if _, ok := tps[tp]; !ok {
tps[tp] = struct{}{}
}
}
for tp, _ := range tps {
var rc *ResourceConfig
if tp.Config != nil {
rc = NewResourceConfig(tp.Config.RawConfig)
}
_, tpErrs := tp.Provider.Validate(rc)
if len(tpErrs) > 0 {
errs = append(errs, tpErrs...)
}
}
// Build the resource graph
graph := c.Config.Graph()
if err := graph.Validate(); err != nil {
errs = append(errs, fmt.Errorf(
"Resource graph has an error: %s", err))
}
// If we accumulated any errors, then return them all
if len(errs) > 0 {
return nil, &MultiError{Errors: errs}
}
return &Terraform{
config: c.Config,
graph: graph,
mapping: mapping,
variables: c.Variables,
}, nil
}
func (t *Terraform) Apply(p *Plan) (*State, error) {
result := new(State)
err := t.graph.Walk(t.applyWalkFn(p, result))
if err != nil {
return nil, err
}
return result, nil
}
func (t *Terraform) Plan(s *State) (*Plan, error) {
result := new(Plan)
err := t.graph.Walk(t.planWalkFn(s, result))
if err != nil {
return nil, err
}
return result, nil
}
func (t *Terraform) Refresh(*State) (*State, error) {
return nil, nil
}
func (t *Terraform) applyWalkFn(
p *Plan,
result *State) depgraph.WalkFunc {
var l sync.Mutex
// Initialize the result
result.init()
cb := func(r *Resource) (map[string]string, error) {
rs, err := r.Provider.Apply(r.State, r.Diff)
if err != nil {
return nil, err
}
// If no state was returned, then no variables were updated so
// just return.
if rs == nil {
return nil, nil
}
// Update the resulting diff
l.Lock()
result.Resources[r.Id] = rs
l.Unlock()
// Determine the new state and update variables
vars := make(map[string]string)
for ak, av := range rs.Attributes {
vars[fmt.Sprintf("%s.%s", r.Id, ak)] = av
}
return vars, nil
}
return t.genericWalkFn(p.State, p.Diff, cb)
}
func (t *Terraform) planWalkFn(
state *State, result *Plan) depgraph.WalkFunc {
var l sync.Mutex
// Initialize the result diff so we can write to it
result.init()
// Write our configuration out
result.Config = t.config
result.Vars = t.variables
cb := func(r *Resource) (map[string]string, error) {
// Refresh the state so we're working with the latest resource info
newState, err := r.Provider.Refresh(r.State)
if err != nil {
return nil, err
}
// Get a diff from the newest state
diff, err := r.Provider.Diff(newState, r.Config)
if err != nil {
return nil, err
}
l.Lock()
if !diff.Empty() {
result.Diff.Resources[r.Id] = diff
}
result.State.Resources[r.Id] = newState
l.Unlock()
// Determine the new state and update variables
vars := make(map[string]string)
rs := newState
if !diff.Empty() {
rs = r.State.MergeDiff(diff)
}
if rs != nil {
for ak, av := range rs.Attributes {
vars[fmt.Sprintf("%s.%s", r.Id, ak)] = av
}
}
return vars, nil
}
return t.genericWalkFn(state, nil, cb)
}
func (t *Terraform) genericWalkFn(
state *State,
diff *Diff,
cb genericWalkFunc) depgraph.WalkFunc {
var l sync.Mutex
// Initialize the variables for application
vars := make(map[string]string)
for k, v := range t.variables {
vars[k] = v
}
return func(n *depgraph.Noun) error {
// If it is the root node, ignore
if n.Meta == nil {
return nil
}
switch n.Meta.(type) {
case *config.ProviderConfig:
// Ignore, we don't treat this any differently since we always
// initialize the provider on first use and use a lock to make
// sure we only do this once.
return nil
case *config.Resource:
// Continue
}
r := n.Meta.(*config.Resource)
p := t.mapping[r]
if p == nil {
panic(fmt.Sprintf("No provider for resource: %s", r.Id()))
}
// Initialize the provider if we haven't already
if err := p.init(vars); err != nil {
return err
}
// Get the resource state
var rs *ResourceState
if state != nil {
rs = state.Resources[r.Id()]
}
// Get the resource diff
var rd *ResourceDiff
if diff != nil {
rd = diff.Resources[r.Id()]
}
if len(vars) > 0 {
if err := r.RawConfig.Interpolate(vars); err != nil {
panic(fmt.Sprintf("Interpolate error: %s", err))
}
}
// If we have no state, then create an empty state with the
// type fulfilled at the least.
if rs == nil {
rs = new(ResourceState)
}
rs.Type = r.Type
// Call the callack
newVars, err := cb(&Resource{
Id: r.Id(),
Config: NewResourceConfig(r.RawConfig),
Diff: rd,
Provider: p.Provider,
State: rs,
})
if err != 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
}
}
func (t *terraformProvider) init(vars map[string]string) (err error) {
t.Once.Do(func() {
var rc *ResourceConfig
if t.Config != nil {
if err := t.Config.RawConfig.Interpolate(vars); err != nil {
panic(err)
}
rc = &ResourceConfig{
ComputedKeys: t.Config.RawConfig.UnknownKeys(),
Raw: t.Config.RawConfig.Config(),
}
}
err = t.Provider.Configure(rc)
})
return
}