plugin/step/install/linux/apt/apt.go (188 lines of code) (raw):
package apt
/*
Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved
*/
import (
"bytes"
"context"
"fmt"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"regexp"
"sort"
"strings"
"time"
"github.com/facebookincubator/go2chef"
"github.com/facebookincubator/go2chef/util/temp"
"github.com/mitchellh/mapstructure"
)
// TypeNames for each variant of this step plugin
const (
TypeName = "go2chef.step.install.linux.apt"
GetTypeName = "go2chef.step.install.linux.apt_get"
)
var (
// DefaultPackageName is the default package name to use for Chef installation
DefaultPackageName = "chef"
)
// Step implements Chef installation via Debian/Ubuntu Apt/apt-get
type Step struct {
StepName string `mapstructure:"name"`
APTBinary string `mapstructure:"apt_binary"`
DPKGBinary string `mapstructure:"dpkg_binary"`
PackageName string `mapstructure:"package_name"`
Version string `mapstructure:"version"`
DpkgCheckTimeoutSeconds int `mapstructure:"dpkg_check_timeout_seconds"`
InstallTimeoutSeconds int `mapstructure:"install_timeout_seconds"`
logger go2chef.Logger
source go2chef.Source
downloadPath string
packageRegex *regexp.Regexp
packageVersionRegex *regexp.Regexp
}
func (s *Step) String() string {
return "<" + TypeName + ":" + s.StepName + ">"
}
// SetName sets the name of this step instance
func (s *Step) SetName(str string) {
s.StepName = str
}
// Name gets the name of this step instance
func (s *Step) Name() string {
return s.StepName
}
// Type returns the type of this step instance
func (s *Step) Type() string {
return TypeName
}
// Download fetches resources required for this step's execution
func (s *Step) Download() error {
if s.source == nil {
return nil
}
tmpdir, err := temp.Dir("", "go2chef-install")
if err != nil {
return err
}
if err := s.source.DownloadToPath(tmpdir); err != nil {
return err
}
s.downloadPath = tmpdir
return nil
}
// Execute performs the installation
func (s *Step) Execute() error {
installPackage := s.PackageName
if s.source != nil {
deb, err := s.findDEB()
if err != nil {
s.logger.Errorf("failed to discover DEB package")
return err
}
installPackage = filepath.Join(s.downloadPath, deb)
}
installed := false
if s.Version != "" {
if err := s.checkInstalled(); err != nil {
switch err.(type) {
case *exec.ExitError:
s.logger.Infof("dpkg-query exited with code %d", err.(*exec.ExitError).ExitCode())
installed = false
case *go2chef.ErrChefAlreadyInstalled:
s.logger.Infof("%s", err)
installed = true
}
}
}
if !installed {
return s.installChef(installPackage)
}
s.logger.Infof("%s specified is already installed, not reinstalling", installPackage)
return nil
}
// LoaderForBinary provides an instantiation function for this step plugin specific to the passed binary
func LoaderForBinary(binary string) go2chef.StepLoader {
return func(config map[string]interface{}) (go2chef.Step, error) {
step := &Step{
StepName: "",
APTBinary: "/usr/bin/" + binary,
DPKGBinary: "/usr/bin/dpkg-query",
PackageName: DefaultPackageName,
DpkgCheckTimeoutSeconds: 60,
InstallTimeoutSeconds: 300,
logger: go2chef.GetGlobalLogger(),
source: nil,
downloadPath: "",
}
if err := mapstructure.Decode(config, step); err != nil {
return nil, err
}
source, err := go2chef.GetSourceFromStepConfig(config)
if err != nil {
return nil, err
}
step.source = source
reStr := fmt.Sprintf("^%s.*.deb$", step.PackageName)
regex, err := regexp.Compile(reStr)
if err != nil {
step.logger.Errorf("failed to compile package matching regex %s", reStr)
return nil, err
}
step.packageRegex = regex
vreStr := fmt.Sprintf("^%s\t%s.*", step.PackageName, step.Version)
vRegex, err := regexp.Compile(vreStr)
if err != nil {
step.logger.Errorf("failed to compile package version match regex %s", vreStr)
}
step.packageVersionRegex = vRegex
return step, nil
}
}
var _ go2chef.Step = &Step{}
func init() {
go2chef.RegisterStep(TypeName, LoaderForBinary("apt"))
go2chef.RegisterStep(GetTypeName, LoaderForBinary("apt-get"))
}
func (s *Step) findDEB() (string, error) {
dirEntries, err := ioutil.ReadDir(s.downloadPath)
if err != nil {
return "", err
}
var matches []string
for _, entry := range dirEntries {
if s.packageRegex.MatchString(entry.Name()) {
matches = append(matches, entry.Name())
}
}
if len(matches) < 1 {
return "", os.ErrNotExist
}
sort.Strings(matches)
if s.Version != "" {
for _, m := range matches {
if strings.Contains(m, s.Version) {
return m, nil
}
}
}
return matches[0], nil
}
func (s *Step) checkInstalled() error {
chkCtx, chkCtxCancel := context.WithTimeout(context.Background(), time.Duration(s.DpkgCheckTimeoutSeconds)*time.Second)
defer chkCtxCancel()
// run `dpkg -W -f "${binary:Package}\t${Version}" chef` to get current package
buf := &bytes.Buffer{}
cmd := exec.CommandContext(chkCtx, s.DPKGBinary, "-W", "-f", "${binary:Package}\t${Version}", s.PackageName)
cmd.Stdin = nil
cmd.Stderr = os.Stderr
cmd.Stdout = buf
if err := cmd.Run(); err != nil {
return err
}
inst := strings.TrimSpace(buf.String())
if s.packageVersionRegex.MatchString(inst) {
return &go2chef.ErrChefAlreadyInstalled{
Installed: inst,
Requested: s.packageVersionRegex.String(),
}
}
return nil
}
func (s *Step) installChef(installPackage string) error {
instCtx, instCtxCancel := context.WithTimeout(context.Background(), time.Duration(s.InstallTimeoutSeconds)*time.Second)
defer instCtxCancel()
cmd := exec.CommandContext(instCtx, s.APTBinary, "-y", "install", installPackage)
cmd.Stdin = nil
cmd.Stderr = os.Stderr
cmd.Stdout = os.Stdout
return cmd.Run()
}