mirror of
https://git.vectorsigma.ru/public/atlantis.git
synced 2026-08-03 17:58:43 +00:00
Automerging merges pull requests automatically if all plans have been successfully applied. * Save status of PR's to BoltDB so after each apply, we can check if there are pending plans. * Add new feature where we delete successful plans *unless* all plans have succeeded *if* automerge is enabled. This was requested by users because when automerge is enabled, they want to enforce that a pull request's changes have been fully applied. They asked that plans not be allowed to be applied "piecemeal" and instead, all plans must be generated successfully prior to allowing any plans to be applied.
224 lines
6.3 KiB
Go
224 lines
6.3 KiB
Go
// 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
|
|
|
|
import (
|
|
"archive/zip"
|
|
"bufio"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"regexp"
|
|
"sync"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/pkg/errors"
|
|
"golang.org/x/crypto/ssh/terminal"
|
|
)
|
|
|
|
const hashicorpReleasesURL = "https://releases.hashicorp.com"
|
|
const terraformVersion = "0.10.8"
|
|
const ngrokDownloadURL = "https://bin.equinox.io/c/4VmDzA7iaHb"
|
|
const ngrokAPIURL = "localhost:41414" // We hope this isn't used.
|
|
const atlantisPort = 4141
|
|
|
|
func readPassword() (string, error) {
|
|
password, err := terminal.ReadPassword(int(syscall.Stdin)) // nolint: unconvert
|
|
return string(password), err
|
|
}
|
|
|
|
func downloadFile(url string, path string) error {
|
|
output, err := os.Create(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer output.Close() // nolint: errcheck
|
|
|
|
response, err := http.Get(url) // nolint: gosec
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer response.Body.Close() // nolint: errcheck
|
|
|
|
_, err = io.Copy(output, response.Body)
|
|
return err
|
|
}
|
|
|
|
func unzip(archive, target string) error {
|
|
reader, err := zip.OpenReader(archive)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for _, file := range reader.File {
|
|
path := filepath.Join(target, file.Name) // nolint: gosec
|
|
if file.FileInfo().IsDir() {
|
|
if err := os.MkdirAll(path, file.Mode()); err != nil {
|
|
return err
|
|
}
|
|
continue
|
|
}
|
|
|
|
fileReader, err := file.Open()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer fileReader.Close() // nolint: errcheck
|
|
|
|
targetFile, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, file.Mode())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer targetFile.Close() // nolint: errcheck
|
|
|
|
if _, err := io.Copy(targetFile, fileReader); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func getTunnelAddr() (string, error) {
|
|
tunAPI := fmt.Sprintf("http://%s/api/tunnels", ngrokAPIURL)
|
|
response, err := http.Get(tunAPI) // nolint: gosec
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer response.Body.Close() // nolint: errcheck
|
|
|
|
type tunnels struct {
|
|
Tunnels []struct {
|
|
PublicURL string `json:"public_url"`
|
|
Proto string `json:"proto"`
|
|
Config struct {
|
|
Addr string `json:"addr"`
|
|
} `json:"config"`
|
|
} `json:"tunnels"`
|
|
}
|
|
|
|
var t tunnels
|
|
|
|
body, err := ioutil.ReadAll(response.Body)
|
|
if err != nil {
|
|
return "", errors.Wrap(err, "reading ngrok api")
|
|
}
|
|
if err = json.Unmarshal(body, &t); err != nil {
|
|
return "", errors.Wrapf(err, "parsing ngrok api: %s", string(body))
|
|
}
|
|
|
|
// Find the tunnel we just created.
|
|
expAtlantisURL := fmt.Sprintf("localhost:%d", atlantisPort)
|
|
for _, tun := range t.Tunnels {
|
|
if tun.Proto == "https" && tun.Config.Addr == expAtlantisURL {
|
|
return tun.PublicURL, nil
|
|
}
|
|
}
|
|
|
|
return "", fmt.Errorf("did not find ngrok tunnel with proto 'https' and config.addr '%s' in list of tunnels at %s\n%s", expAtlantisURL, tunAPI, string(body))
|
|
}
|
|
|
|
func downloadAndUnzip(url string, path string, target string) error {
|
|
if err := downloadFile(url, path); err != nil {
|
|
return err
|
|
}
|
|
return unzip(path, target)
|
|
}
|
|
|
|
// executeCmd executes a command, waits for it to finish and returns any errors.
|
|
func executeCmd(cmd string, args ...string) error {
|
|
command := exec.Command(cmd, args...) // #nosec
|
|
bytes, err := command.CombinedOutput()
|
|
if err != nil {
|
|
return fmt.Errorf("%s: %s", err, bytes)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// execAndWaitForStderr executes a command with name and args. It waits until
|
|
// timeout for the stderr output of the command to match stderrMatch. If the
|
|
// timeout comes first, then it cancels the command and returns the error as
|
|
// error (not on the channel). Otherwise the function returns and the command
|
|
// continues to run in the background. Any errors after this point are passed
|
|
// onto the error channel and the command is stopped. We increment the wg
|
|
// so that callers can wait until command is killed before exiting.
|
|
// The cancelFunc can be used to stop the command but callers should still wait
|
|
// for the wg to be Done to ensure the command completes its cancellation
|
|
// process.
|
|
func execAndWaitForStderr(wg *sync.WaitGroup, stderrMatch *regexp.Regexp, timeout time.Duration, name string, args ...string) (context.CancelFunc, <-chan error, error) {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
errChan := make(chan error, 1)
|
|
|
|
// Set up the command and stderr pipe.
|
|
command := exec.CommandContext(ctx, name, args...) // #nosec
|
|
stderr, err := command.StderrPipe()
|
|
if err != nil {
|
|
return cancel, errChan, errors.Wrap(err, "creating stderr pipe")
|
|
}
|
|
|
|
// Start the command in the background. This will only return error if the
|
|
// command is not executable.
|
|
err = command.Start()
|
|
if err != nil {
|
|
return cancel, errChan, fmt.Errorf("starting command: %v", err)
|
|
}
|
|
|
|
// Wait until we see the desired output or time out.
|
|
foundLine := make(chan bool, 1)
|
|
scanner := bufio.NewScanner(stderr)
|
|
var log string
|
|
|
|
// This goroutine watches the process stderr and sends true along the
|
|
// foundLine channel if a line matches.
|
|
go func() {
|
|
for scanner.Scan() {
|
|
text := scanner.Text()
|
|
log += text + "\n"
|
|
if stderrMatch.MatchString(text) {
|
|
foundLine <- true
|
|
break
|
|
}
|
|
}
|
|
}()
|
|
|
|
// Block on either finding a matching line or timeout.
|
|
select {
|
|
case <-foundLine:
|
|
// If we find the line, continue.
|
|
case <-time.After(timeout):
|
|
// If it's a timeout we cancel the command ourselves.
|
|
cancel()
|
|
// We still need to wait for the command to finish.
|
|
command.Wait() // nolint: errcheck
|
|
return cancel, errChan, fmt.Errorf("timeout, logs:\n%s\n", log) // nolint: staticcheck, golint
|
|
}
|
|
|
|
// Increment the wait group so callers can wait for the command to finish.
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
err := command.Wait()
|
|
errChan <- err
|
|
}()
|
|
|
|
return cancel, errChan, nil
|
|
}
|