forked from hashicorp/otto
/
foundation.go
161 lines (144 loc) · 4.94 KB
/
foundation.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
package terraform
import (
"fmt"
"path/filepath"
"github.com/hashicorp/otto/directory"
"github.com/hashicorp/otto/foundation"
)
// Foundation is a helper for various operations a foundation must
// perform with Terraform.
type Foundation struct {
// Dir is the directory where Terraform is run. If this isn't set, it'll
// default to "#{ctx.Dir}/deploy".
Dir string
}
// Infra manages a foundation using Terraform.
//
// This will verify the infrastruction is created and use that information
// to execute Terraform with the options given.
//
// This function implements foundation.Foundation.Infra.
func (f *Foundation) Infra(ctx *foundation.Context) error {
switch ctx.Action {
case "":
if err := f.execute(ctx, "get", "."); err != nil {
return err
}
return f.execute(ctx, "apply")
case "destroy":
if err := f.execute(ctx, "get", "."); err != nil {
return err
}
return f.execute(ctx, "destroy", "-force")
default:
return fmt.Errorf("unknown action: %s", ctx.Action)
}
}
func (f *Foundation) execute(ctx *foundation.Context, args ...string) error {
project, err := Project(&ctx.Shared)
if err != nil {
return err
}
appInfra := ctx.Appfile.ActiveInfrastructure()
// Foundations themselves are represented as infrastructure in the
// backend. Let's look that up. If it doesn't exist, we have to create
// it in order to get our UUID for storing state.
lookup := directory.Lookup{Infra: appInfra.Type, Foundation: ctx.Tuple.Type}
foundationInfra, err := ctx.Directory.GetInfra(&directory.Infra{Lookup: lookup})
if err != nil {
return fmt.Errorf(
"Error looking up existing infrastructure data: %s\n\n"+
"These errors are usually transient and can be fixed by retrying\n"+
"the command. Additional causes of errors are networking or disk\n"+
"issues that can be resolved external to Otto.",
err)
}
if foundationInfra == nil {
// If we don't have an infra, create one
foundationInfra = &directory.Infra{Lookup: lookup}
foundationInfra.State = directory.InfraStatePartial
// Put the infrastructure so we can get the UUID to use for our state
if err := ctx.Directory.PutInfra(foundationInfra); err != nil {
return fmt.Errorf(
"Error preparing infrastructure: %s\n\n"+
"These errors are usually transient and can be fixed by retrying\n"+
"the command. Additional causes of errors are networking or disk\n"+
"issues that can be resolved external to Otto.",
err)
}
}
// Get the infrastructure state. The infrastructure must be
// created for us to deploy to it.
infra, err := ctx.Directory.GetInfra(&directory.Infra{
Lookup: directory.Lookup{Infra: appInfra.Name}})
if err != nil {
return err
}
if infra == nil || infra.State != directory.InfraStateReady {
return fmt.Errorf(
"Infrastructure for this application hasn't been built yet.\n" +
"Building a foundation requires a target infrastruction to\n" +
"be built. Please run `otta infra` to build the underlying\n" +
"infrastructure.")
}
// Construct the variables for Terraform from our queried infra
vars := make(map[string]string)
for k, v := range infra.Outputs {
vars[k] = v
}
for k, v := range ctx.InfraCreds {
vars[k] = v
}
// Get the directory
tfDir := f.Dir
if tfDir == "" {
tfDir = filepath.Join(ctx.Dir, "deploy")
}
// Run Terraform!
tf := &Terraform{
Path: project.Path(),
Dir: tfDir,
Ui: ctx.Ui,
Variables: vars,
Directory: ctx.Directory,
StateId: foundationInfra.ID,
}
err = tf.Execute(args...)
if err != nil {
return fmt.Errorf(
"Error running Terraform: %s\n\n"+
"Terraform usually has helpful error messages. Please read the error\n"+
"messages above and resolve them. Sometimes simply re-running the\n"+
"command again will work.",
err)
}
ctx.Ui.Header("Terraform execution complete. Saving results...")
if err == nil {
if ctx.Action == "destroy" {
// If we just destroyed successfully, the infra is now empty.
foundationInfra.State = directory.InfraStateInvalid
foundationInfra.Outputs = map[string]string{}
} else {
// If an apply was successful, populate the state and outputs.
foundationInfra.State = directory.InfraStateReady
foundationInfra.Outputs, err = tf.Outputs()
if err != nil {
err = fmt.Errorf("Error reading Terraform outputs: %s", err)
foundationInfra.State = directory.InfraStatePartial
}
}
}
// Save the infrastructure information
if err := ctx.Directory.PutInfra(foundationInfra); err != nil {
return fmt.Errorf(
"Error storing infrastructure data: %s\n\n"+
"This means that Otto won't be able to know that your infrastructure\n"+
"was successfully created. Otto tries a few times to save the\n"+
"infrastructure. At this point in time, Otto doesn't support gracefully\n"+
"recovering from this error. Your infrastructure is now orphaned from\n"+
"Otto's management. Please reference the community for help.\n\n"+
"A future version of Otto will resolve this.",
err)
}
return err
}