forked from gruntwork-io/terragrunt
-
Notifications
You must be signed in to change notification settings - Fork 0
/
remote_state_s3.go
198 lines (162 loc) · 7.42 KB
/
remote_state_s3.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
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
package remote
import (
"github.com/mitchellh/mapstructure"
"github.com/gruntwork-io/terragrunt/errors"
"github.com/gruntwork-io/terragrunt/options"
"github.com/aws/aws-sdk-go/service/s3"
"github.com/aws/aws-sdk-go/aws"
"fmt"
"github.com/gruntwork-io/terragrunt/shell"
"github.com/gruntwork-io/terragrunt/aws_helper"
"time"
)
// A representation of the configuration options available for S3 remote state
type RemoteStateConfigS3 struct {
Encrypt string
Bucket string
Key string
Region string
Profile string
}
const MAX_RETRIES_WAITING_FOR_S3_BUCKET = 12
const SLEEP_BETWEEN_RETRIES_WAITING_FOR_S3_BUCKET = 5 * time.Second
// Initialize the remote state S3 bucket specified in the given config. This function will validate the config
// parameters, create the S3 bucket if it doesn't already exist, and check that versioning is enabled.
func InitializeRemoteStateS3(config map[string]string, terragruntOptions *options.TerragruntOptions) error {
s3Config, err := parseS3Config(config)
if err != nil {
return err
}
if err := validateS3Config(s3Config, terragruntOptions); err != nil {
return err
}
s3Client, err := CreateS3Client(s3Config.Region, s3Config.Profile)
if err != nil {
return err
}
if err := createS3BucketIfNecessary(s3Client, s3Config, terragruntOptions); err != nil {
return err
}
if err := checkIfVersioningEnabled(s3Client, s3Config, terragruntOptions); err != nil {
return err
}
return nil
}
// Parse the given map into an S3 config
func parseS3Config(config map[string]string) (*RemoteStateConfigS3, error) {
var s3Config RemoteStateConfigS3
if err := mapstructure.Decode(config, &s3Config); err != nil {
return nil, errors.WithStackTrace(err)
}
return &s3Config, nil
}
// Validate all the parameters of the given S3 remote state configuration
func validateS3Config(config *RemoteStateConfigS3, terragruntOptions *options.TerragruntOptions) error {
if config.Region == "" {
return errors.WithStackTrace(MissingRequiredS3RemoteStateConfig("region"))
}
if config.Bucket == "" {
return errors.WithStackTrace(MissingRequiredS3RemoteStateConfig("bucket"))
}
if config.Key == "" {
return errors.WithStackTrace(MissingRequiredS3RemoteStateConfig("key"))
}
if config.Encrypt != "true" {
terragruntOptions.Logger.Printf("WARNING: encryption is not enabled on the S3 remote state bucket %s. Terraform state files may contain secrets, so we STRONGLY recommend enabling encryption!", config.Bucket)
}
return nil
}
// If the bucket specified in the given config doesn't already exist, prompt the user to create it, and if the user
// confirms, create the bucket and enable versioning for it.
func createS3BucketIfNecessary(s3Client *s3.S3, config *RemoteStateConfigS3, terragruntOptions *options.TerragruntOptions) error {
if !DoesS3BucketExist(s3Client, config) {
prompt := fmt.Sprintf("Remote state S3 bucket %s does not exist or you don't have permissions to access it. Would you like Terragrunt to create it?", config.Bucket)
shouldCreateBucket, err := shell.PromptUserForYesNo(prompt, terragruntOptions)
if err != nil {
return err
}
if shouldCreateBucket {
return CreateS3BucketWithVersioning(s3Client, config, terragruntOptions)
}
}
return nil
}
// Check if versioning is enabled for the S3 bucket specified in the given config and warn the user if it is not
func checkIfVersioningEnabled(s3Client *s3.S3, config *RemoteStateConfigS3, terragruntOptions *options.TerragruntOptions) error {
out, err := s3Client.GetBucketVersioning(&s3.GetBucketVersioningInput{Bucket: aws.String(config.Bucket)})
if err != nil {
return errors.WithStackTrace(err)
}
// NOTE: There must be a bug in the AWS SDK since out == nil when versioning is not enabled. In the future,
// check the AWS SDK for updates to see if we can remove "out == nil ||".
if out == nil || out.Status == nil || *out.Status != s3.BucketVersioningStatusEnabled {
terragruntOptions.Logger.Printf("WARNING: Versioning is not enabled for the remote state S3 bucket %s. We recommend enabling versioning so that you can roll back to previous versions of your Terraform state in case of error.", config.Bucket)
}
return nil
}
// Create the given S3 bucket and enable versioning for it
func CreateS3BucketWithVersioning(s3Client *s3.S3, config *RemoteStateConfigS3, terragruntOptions *options.TerragruntOptions) error {
if err := CreateS3Bucket(s3Client, config, terragruntOptions); err != nil {
return err
}
if err := WaitUntilS3BucketExists(s3Client, config, terragruntOptions); err != nil {
return err
}
if err := EnableVersioningForS3Bucket(s3Client, config, terragruntOptions); err != nil {
return err
}
return nil
}
// AWS is eventually consistent, so after creating an S3 bucket, this method can be used to wait until the information
// about that S3 bucket has propagated everywhere
func WaitUntilS3BucketExists(s3Client *s3.S3, config *RemoteStateConfigS3, terragruntOptions *options.TerragruntOptions) error {
for retries := 0; retries < MAX_RETRIES_WAITING_FOR_S3_BUCKET; retries++ {
if DoesS3BucketExist(s3Client, config) {
terragruntOptions.Logger.Printf("S3 bucket %s created.", config.Bucket)
return nil
} else if retries < MAX_RETRIES_WAITING_FOR_S3_BUCKET - 1 {
terragruntOptions.Logger.Printf("S3 bucket %s has not been created yet. Sleeping for %s and will check again.", config.Bucket, SLEEP_BETWEEN_RETRIES_WAITING_FOR_S3_BUCKET)
time.Sleep(SLEEP_BETWEEN_RETRIES_WAITING_FOR_S3_BUCKET)
}
}
return errors.WithStackTrace(MaxRetriesWaitingForS3BucketExceeded(config.Bucket))
}
// Create the S3 bucket specified in the given config
func CreateS3Bucket(s3Client *s3.S3, config *RemoteStateConfigS3, terragruntOptions *options.TerragruntOptions) error {
terragruntOptions.Logger.Printf("Creating S3 bucket %s", config.Bucket)
_, err := s3Client.CreateBucket(&s3.CreateBucketInput{Bucket: aws.String(config.Bucket)})
return errors.WithStackTrace(err)
}
// Enable versioning for the S3 bucket specified in the given config
func EnableVersioningForS3Bucket(s3Client *s3.S3, config *RemoteStateConfigS3, terragruntOptions *options.TerragruntOptions) error {
terragruntOptions.Logger.Printf("Enabling versioning on S3 bucket %s", config.Bucket)
input := s3.PutBucketVersioningInput{
Bucket: aws.String(config.Bucket),
VersioningConfiguration: &s3.VersioningConfiguration{Status: aws.String(s3.BucketVersioningStatusEnabled)},
}
_, err := s3Client.PutBucketVersioning(&input)
return errors.WithStackTrace(err)
}
// Returns true if the S3 bucket specified in the given config exists and the current user has the ability to access
// it.
func DoesS3BucketExist(s3Client *s3.S3, config *RemoteStateConfigS3) bool {
_, err := s3Client.HeadBucket(&s3.HeadBucketInput{Bucket: aws.String(config.Bucket)})
return err == nil
}
// Create an authenticated client for DynamoDB
func CreateS3Client(awsRegion, awsProfile string) (*s3.S3, error) {
session, err := aws_helper.CreateAwsSession(awsRegion, awsProfile)
if err != nil {
return nil, err
}
return s3.New(session), nil
}
// Custom error types
type MissingRequiredS3RemoteStateConfig string
func (configName MissingRequiredS3RemoteStateConfig) Error() string {
return fmt.Sprintf("Missing required S3 remote state configuration %s", string(configName))
}
type MaxRetriesWaitingForS3BucketExceeded string
func (err MaxRetriesWaitingForS3BucketExceeded) Error() string {
return fmt.Sprintf("Exceeded max retries (%d) waiting for bucket S3 bucket %s", MAX_RETRIES_WAITING_FOR_S3_BUCKET, string(err))
}