// 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" "net/http" "os" "os/exec" "path/filepath" "regexp" "strings" "sync" "syscall" "time" "github.com/pkg/errors" "golang.org/x/term" ) const hashicorpReleasesURL = "https://releases.hashicorp.com" const terraformVersion = "1.11.4" // renovate: datasource=github-releases depName=hashicorp/terraform versioning=hashicorp 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 := term.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 } // This function is used to sanitize the file path to avoid a "zip slip" attack // source: https://github.com/securego/gosec/issues/324#issuecomment-935927967 func sanitizeArchivePath(d, t string) (v string, err error) { v = filepath.Join(d, t) if strings.HasPrefix(v, filepath.Clean(d)) { return v, nil } return "", fmt.Errorf("%s: %s", "content filepath is tainted", t) } func unzip(archive, target string) error { reader, err := zip.OpenReader(archive) if err != nil { return err } for _, file := range reader.File { path, err := sanitizeArchivePath(target, file.Name) if err != nil { return err } 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 for { _, err := io.CopyN(targetFile, fileReader, 1024) if err != nil { if err == io.EOF { break } 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 := io.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("http://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, revive } // 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 }