astro/tvm/versionrepo.go (127 lines of code) (raw):

/* * Copyright (c) 2018 Uber Technologies, 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. */ // Package tvm stands for Terraform version manager. It will // automatically download and manage multiple Terraform binaries. package tvm import ( "errors" "fmt" "io/ioutil" "os" "path" "path/filepath" "regexp" "runtime" "sync" "github.com/uber/astro/astro/utils" homedir "github.com/mitchellh/go-homedir" ) // terraformBinaryFile is the name of the Terraform binary. const terraformBinaryFile = "terraform" // terraformZipFileDownloadURL is the path to download Terraform zip // files from the Hashicorp website. var terraformZipFileDownloadURL = "https://releases.hashicorp.com/terraform/%s/terraform_%s_%s_%s.zip" // versionDirectoryFormat is a regexp that matches Terraform semver, // e.g. "1.2.30" var versionDirectoryFormat = regexp.MustCompile(`\d+\.\d+\.\d+`) // VersionRepo is a directory on the filesystem that keeps // Terraform binaries. type VersionRepo struct { repoPath string arch string platform string // locks is a map of mutexes. There is one mutex created on demand for // every Terraform version requested from tvm. The mutex prevents tvm from // downloading the same version of Terraform multiple times. If multiple // threads request the same version of Terraform, only one of them will // trigger the download and the rest will block until the download is // complete. locks *sync.Map } // NewVersionRepo creates a new VersionRepo. The arch will // be appended to the provided path for all downloaded binaries. func NewVersionRepo(repoPath string, arch string, platform string) (*VersionRepo, error) { if repoPath == "" { home, err := homedir.Dir() if err != nil { return nil, err } repoPath = filepath.Join(home, ".tvm") } // Create directory if it doesn't exist if err := os.Mkdir(repoPath, 0755); err != nil && !os.IsExist(err) { return nil, err } return &VersionRepo{ locks: &sync.Map{}, repoPath: repoPath, arch: arch, platform: platform, }, nil } // NewVersionRepoForCurrentSystem returns a new VersionRepo instance // with platform and architecture information retrieve from the current // system. func NewVersionRepoForCurrentSystem(repoPath string) (*VersionRepo, error) { return NewVersionRepo(repoPath, runtime.GOARCH, runtime.GOOS) } // dir returns the directory in the repository that contains the // specified version. func (r *VersionRepo) dir(version string) string { return filepath.Join(r.repoPath, r.platform, r.arch, version) } // download gets the Terraform binary from the Terraform website. It // returns the path to the downloaded file or an error if there was a // problem. func (r *VersionRepo) download(version string) (string, error) { url := fmt.Sprintf(terraformZipFileDownloadURL, version, version, r.platform, r.arch) // Temporary directory for downloading Terraform and extracting the zip file tmpDir, err := ioutil.TempDir("", "terraform") if err != nil { return "", err } defer os.RemoveAll(tmpDir) zipFilePath := path.Join(tmpDir, "terraform.zip") // Download Terraform zip file if err := downloadFile(url, zipFilePath); err != nil { return "", err } // Extract contents of zip file if err := unzip(zipFilePath, tmpDir); err != nil { return "", err } terraformBinaryPath := path.Join(tmpDir, "terraform") // Check the binary is there if !utils.FileExists(terraformBinaryPath) { return "", errors.New("Terraform binary missing from zip file") } targetDir := r.dir(version) // Make repo dir if err := os.MkdirAll(targetDir, os.ModePerm); err != nil { return "", err } // Move binary to repo path if err := os.Rename(terraformBinaryPath, path.Join(targetDir, "terraform")); err != nil { return "", err } return r.terraformPath(version), nil } // exists returns whether or not the binary for the specified version // exists. func (r *VersionRepo) exists(version string) bool { return utils.FileExists(r.terraformPath(version)) } // getLock returns a mutex for the specified Terraform version which is used to // prevent multiple threads from downloading the same version of Terraform at // the same time. func (r *VersionRepo) getLock(version string) *sync.Mutex { v, _ := r.locks.LoadOrStore(version, &sync.Mutex{}) return v.(*sync.Mutex) } // Get takes a version and returns the path to the Terraform binary for // that version. If the binary doesn't exist, it will be downloaded from // the Terraform website automatically. func (r *VersionRepo) Get(version string) (string, error) { lock := r.getLock(version) // Lock() here will block and wait if another thread is currently // downloading Terraform. lock.Lock() defer lock.Unlock() path := r.terraformPath(version) if !utils.FileExists(path) { return r.download(version) } return path, nil } // Link symlinks the version binary into the targetPath. It will // download the binary if the version does not exist in the repository. func (r *VersionRepo) Link(version string, targetPath string, overwrite bool) error { terraformPath, err := r.Get(version) if err != nil { return err } if overwrite { _, err := os.Lstat(targetPath) if !os.IsNotExist(err) { os.Remove(targetPath) } } return os.Symlink(terraformPath, targetPath) } // List returns all locally downloaded Terraform versions and their paths. func (r *VersionRepo) List() (map[string]string, error) { dirs := map[string]string{} repoBaseDir := r.dir("") f, err := os.Open(repoBaseDir) defer f.Close() if err != nil { return nil, err } files, err := f.Readdir(-1) if err != nil { return nil, err } for _, file := range files { terraformVersion := file.Name() if file.IsDir() && versionDirectoryFormat.MatchString(terraformVersion) { dirs[terraformVersion] = r.terraformPath(terraformVersion) } } return dirs, nil } // terraformPath returns the path to the Terraform binary file with the // specified version. func (r *VersionRepo) terraformPath(version string) string { return filepath.Join(r.dir(version), terraformBinaryFile) }