-
Notifications
You must be signed in to change notification settings - Fork 1.1k
/
testdrive.go
293 lines (259 loc) · 11.2 KB
/
testdrive.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
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
// Copyright 2017 HootSuite Media Inc.
//
// Licensed under the Apache License, Version 2.0 (the License);
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an AS IS BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Modified hereafter by contributors to runatlantis/atlantis.
//
// Package testdrive is used by the testdrive command as a quick-start of
// Atlantis.
package testdrive
import (
"context"
"fmt"
"os"
"os/exec"
"os/signal"
"regexp"
"runtime"
"strings"
"sync"
"syscall"
"time"
"github.com/briandowns/spinner"
"github.com/google/go-github/v66/github"
"github.com/mitchellh/colorstring"
"github.com/pkg/errors"
)
var terraformExampleRepoOwner = "runatlantis"
var terraformExampleRepo = "atlantis-example"
var bootstrapDescription = `Welcome to Atlantis testdrive!
This mode sets up Atlantis on a test repo so you can try it out. We will
- fork an example terraform project to your username
- install terraform (if not already in your PATH)
- install ngrok so we can expose Atlantis to GitHub
- start Atlantis
[bold]Press Ctrl-c at any time to exit
`
var pullRequestBody = strings.Replace(`
In this pull request we will learn how to use Atlantis.
1. In a couple of seconds you should see the output of Atlantis automatically running $terraform plan$.
1. You can manually run $plan$ by typing a comment:
$$$
atlantis plan
$$$
Usually you'll let Atlantis automatically run plan for you though.
1. To see all the comment commands available, type:
$$$
atlantis help
$$$
1. To see the help for a specific command, for example $atlantis plan$, type:
$$$
atlantis plan --help
$$$
1. Atlantis holds a "Lock" on this directory to prevent other pull requests modifying
the Terraform state until this pull request is merged. To view the lock, go to the Atlantis UI: [http://localhost:4141](http://localhost:4141).
If you wanted, you could manually delete the plan and lock from the UI if you weren't ready to apply. Instead, we will apply it!
1. To $terraform apply$ this change (which does nothing because it is creating a $null_resource$), type:
$$$
atlantis apply
$$$
**NOTE:** Because this example isn't using [remote state storage](https://developer.hashicorp.com/terraform/language/state/remote) the state will be lost once the pull request is merged. To use Atlantis properly, you **must** be using remote state.
1. Finally, merge the pull request to unlock this directory.
Thank you for trying out Atlantis! Next, try using Atlantis on your own repositories: [www.runatlantis.io/guide/getting-started.html](https://www.runatlantis.io/guide/getting-started.html).`, "$", "`", -1)
// Start begins the testdrive process.
// nolint: errcheck
func Start() error {
s := spinner.New(spinner.CharSets[14], 100*time.Millisecond)
colorstring.Println(bootstrapDescription)
colorstring.Print("\n[bold]github.com username: ")
fmt.Scanln(&githubUsername)
if githubUsername == "" {
return fmt.Errorf("please enter a valid github username")
}
colorstring.Println(`
To continue, we need you to create a GitHub personal access token
with [green]"repo" [reset]scope so we can fork an example terraform project.
Follow these instructions to create a token (we don't store any tokens):
[green]https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token#creating-a-fine-grained-personal-access-token[reset]
- use "atlantis" for the token description
- add "repo" scope
- copy the access token
`)
// Read github token, check for error later.
colorstring.Print("[bold]GitHub access token (will be hidden): ")
githubToken, _ = readPassword()
tp := github.BasicAuthTransport{
Username: strings.TrimSpace(githubUsername),
Password: strings.TrimSpace(githubToken),
}
githubClient := &Client{client: github.NewClient(tp.Client()), ctx: context.Background()}
// Fork terraform example repo.
colorstring.Println("\n=> forking repo ")
s.Start()
if err := githubClient.CreateFork(terraformExampleRepoOwner, terraformExampleRepo); err != nil {
return errors.Wrapf(err, "forking repo %s/%s", terraformExampleRepoOwner, terraformExampleRepo)
}
if !githubClient.CheckForkSuccess(terraformExampleRepoOwner, terraformExampleRepo) {
return fmt.Errorf("didn't find forked repo %s/%s. fork unsuccessful", terraformExampleRepoOwner, terraformExampleRepo)
}
s.Stop()
colorstring.Println("[green]=> fork completed![reset]")
// Detect terraform and install it if not installed.
terraformPath, err := exec.LookPath("terraform")
if err != nil {
colorstring.Println("[yellow]=> terraform not found in $PATH.[reset]")
colorstring.Println("=> downloading terraform ")
s.Start()
terraformDownloadURL := fmt.Sprintf("%s/terraform/%s/terraform_%s_%s_%s.zip", hashicorpReleasesURL, terraformVersion, terraformVersion, runtime.GOOS, runtime.GOARCH)
if err = downloadAndUnzip(terraformDownloadURL, "/tmp/terraform.zip", "/tmp"); err != nil {
return errors.Wrapf(err, "downloading and unzipping terraform")
}
colorstring.Println("[green]=> downloaded terraform successfully![reset]")
s.Stop()
err = executeCmd("mv", "/tmp/terraform", "/usr/local/bin/")
if err != nil {
return errors.Wrapf(err, "moving terraform binary into /usr/local/bin")
}
colorstring.Println("[green]=> installed terraform successfully at /usr/local/bin[reset]")
} else {
colorstring.Printf("[green]=> terraform found in $PATH at %s\n[reset]", terraformPath)
}
// Detect ngrok and install it if not installed
ngrokPath, ngrokErr := exec.LookPath("ngrok")
if ngrokErr != nil {
colorstring.Println("[yellow]=> ngrok not found in $PATH.[reset]")
colorstring.Println("=> downloading ngrok")
s.Start()
ngrokURL := fmt.Sprintf("%s/ngrok-stable-%s-%s.zip", ngrokDownloadURL, runtime.GOOS, runtime.GOARCH)
if err = downloadAndUnzip(ngrokURL, "/tmp/ngrok.zip", "/tmp"); err != nil {
return errors.Wrapf(err, "downloading and unzipping ngrok")
}
s.Stop()
colorstring.Println("[green]=> downloaded ngrok successfully![reset]")
ngrokPath = "/tmp/ngrok"
} else {
colorstring.Printf("[green]=> ngrok found in $PATH at %s\n[reset]", ngrokPath)
}
// Create ngrok tunnel.
colorstring.Println("=> creating secure tunnel")
s.Start()
// We use a config file so we can set ngrok's API port (web_addr). We use
// the API to get the public URL and if there's already ngrok running, it
// will just choose a random API port and we won't be able to get the right
// url.
ngrokConfig := fmt.Sprintf(`
version: 1
web_addr: %s
tunnels:
atlantis:
addr: %d
bind_tls: true
proto: http
`, ngrokAPIURL, atlantisPort)
ngrokConfigFile, err := os.CreateTemp("", "atlantis-testdrive-ngrok-config")
if err != nil {
return errors.Wrap(err, "creating ngrok config file")
}
err = os.WriteFile(ngrokConfigFile.Name(), []byte(ngrokConfig), 0600)
if err != nil {
return errors.Wrap(err, "writing ngrok config file")
}
// Used to ensure proper termination of all background commands.
var wg sync.WaitGroup
defer wg.Wait()
tunnelReadyLog := regexp.MustCompile("client session established")
tunnelTimeout := 20 * time.Second
cancelNgrok, ngrokErrors, err := execAndWaitForStderr(&wg, tunnelReadyLog, tunnelTimeout,
ngrokPath, "start", "atlantis", "--config", ngrokConfigFile.Name(), "--log", "stderr", "--log-format", "term")
// Check if we got a fast error. Move on if we haven't (the command is still running).
if err != nil {
s.Stop()
return errors.Wrap(err, "creating ngrok tunnel")
}
// When this function returns, ngrok tunnel should be stopped.
defer cancelNgrok()
// The tunnel is up!
s.Stop()
colorstring.Println("[green]=> started tunnel![reset]")
// There's a 1s delay between tunnel starting and API being up.
time.Sleep(1 * time.Second)
tunnelURL, err := getTunnelAddr()
if err != nil {
return errors.Wrapf(err, "getting tunnel url")
}
// Start atlantis server.
colorstring.Println("=> starting atlantis server")
s.Start()
tmpDir, err := os.MkdirTemp("", "")
if err != nil {
return errors.Wrap(err, "creating a temporary data directory for Atlantis")
}
defer os.RemoveAll(tmpDir)
serverReadyLog := regexp.MustCompile("Atlantis started - listening on port 4141")
serverReadyTimeout := 5 * time.Second
cancelAtlantis, atlantisErrors, err := execAndWaitForStderr(&wg, serverReadyLog, serverReadyTimeout,
os.Args[0], "server", "--gh-user", githubUsername, "--gh-token", githubToken, "--data-dir", tmpDir, "--atlantis-url", tunnelURL, "--repo-allowlist", fmt.Sprintf("github.com/%s/%s", githubUsername, terraformExampleRepo))
// Check if we got a fast error. Move on if we haven't (the command is still running).
if err != nil {
return errors.Wrap(err, "creating atlantis server")
}
// When this function returns atlantis server should be stopped.
defer cancelAtlantis()
colorstring.Printf("[green]=> atlantis server is now securely exposed at [bold][underline]%s\n[reset]", tunnelURL)
fmt.Println("")
// Create atlantis webhook.
colorstring.Println("=> creating atlantis webhook")
s.Start()
err = githubClient.CreateWebhook(githubUsername, terraformExampleRepo, fmt.Sprintf("%s/events", tunnelURL))
if err != nil {
return errors.Wrapf(err, "creating atlantis webhook")
}
s.Stop()
colorstring.Println("[green]=> atlantis webhook created![reset]")
// Create a new pr in the example repo.
colorstring.Println("=> creating a new pull request")
s.Start()
pullRequestURL, err := githubClient.CreatePullRequest(githubUsername, terraformExampleRepo, "example", "main")
if err != nil {
return errors.Wrapf(err, "creating new pull request for repo %s/%s", githubUsername, terraformExampleRepo)
}
s.Stop()
colorstring.Println("[green]=> pull request created![reset]")
// Open new pull request in the browser.
colorstring.Println("=> opening pull request")
s.Start()
time.Sleep(2 * time.Second)
err = executeCmd("open", pullRequestURL)
if err != nil {
colorstring.Printf("[red]=> opening pull request failed. please go to: %s on the browser\n[reset]", pullRequestURL)
}
s.Stop()
// Wait for ngrok and atlantis server process to finish.
colorstring.Println("[_green_][light_green]atlantis is running [reset]")
s.Start()
colorstring.Println("[green] [press Ctrl-c to exit][reset]")
// Wait for SIGINT or SIGTERM signals meaning the user has Ctrl-C'd the
// testdrive process and want's to stop.
signalChan := make(chan os.Signal, 1)
signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM)
// Keep checking for errors from ngrok or atlantis server. Exit normally on shutdown signal.
select {
case <-signalChan:
colorstring.Println("\n[red]shutdown signal received, exiting....[reset]")
colorstring.Println("\n[green]Thank you for using atlantis :) \n[reset]For more information about how to use atlantis in production go to: https://www.runatlantis.io")
return nil
case err := <-ngrokErrors:
return errors.Wrap(err, "ngrok tunnel")
case err := <-atlantisErrors:
return errors.Wrap(err, "atlantis server")
}
}