plugin/step/install/darwin/pkg/pkg.go (171 lines of code) (raw):

package pkg /* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved */ import ( "context" "fmt" "io/ioutil" "os" "os/exec" "path/filepath" "regexp" "time" "github.com/facebookincubator/go2chef/util/temp" "github.com/facebookincubator/go2chef/util" "github.com/mitchellh/mapstructure" "github.com/facebookincubator/go2chef" ) // TypeName is the name of this step plugin const TypeName = "go2chef.step.install.darwin.pkg" // Step implements Chef installation via macOS type Step struct { // StepName defines the name of the step StepName string `mapstructure:"name"` // PKGMatch specifies a regex to match the filenames in this step's // source directory against to find the PKG file PKGMatch string `mapstructure:"pkg_match"` // DMGMatch specifies a regex to match the filenames in this step's // source directory against to find the DMG file DMGMatch string `mapstructure:"dmg_match"` // InstallerTimeoutSeconds defines the timeout for installation InstallerTimeoutSeconds int `mapstructure:"installer_timeout_seconds"` // IsDMG enables installation of a pkg inside a DMG IsDMG bool `mapstructure:"is_dmg"` logger go2chef.Logger source go2chef.Source downloadPath string } 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 { // If this is a DMG, go down the rabbit hole. Mount it and // then set downloadPath to its mount point. if s.IsDMG { s.logger.WriteEvent(go2chef.NewEvent("INSTALL_PKG_DMG_MOUNT", s.Name(), "mounting DMG")) dmg, err := s.findDMG() if err != nil { return err } if err := s.mountDMG(filepath.Join(s.downloadPath, dmg)); err != nil { return err } // unmount and emit events defer func() { if err := s.unmountDMG(); err != nil { s.logger.WriteEvent(go2chef.NewEvent("INSTALL_PKG_DMG_UNMOUNT_FAILED", s.Name(), "unmounting DMG failed!")) } s.logger.WriteEvent(go2chef.NewEvent("INSTALL_PKG_DMG_UNMOUNT", s.Name(), "unmounting DMG")) }() } pkg, err := s.findPKG() if err != nil { return err } instCtx, cancel := context.WithTimeout(context.Background(), time.Duration(s.InstallerTimeoutSeconds)*time.Second) defer cancel() cmd := exec.CommandContext(instCtx, "installer", "-verbose", "-pkg", filepath.Join(s.downloadPath, pkg), "-target", "/") cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { // preserve exit error xerr := err if exit, ok := xerr.(*exec.ExitError); ok { s.logger.Errorf("pkg installer exited with code %d", exit.ExitCode()) } return xerr } return nil } // Loader provides an instantiation function for this step plugin func Loader(config map[string]interface{}) (go2chef.Step, error) { step := &Step{ StepName: "", PKGMatch: "chef.*", DMGMatch: "chef.*", InstallerTimeoutSeconds: 300, IsDMG: false, 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 return step, nil } func init() { if go2chef.AutoRegisterPlugins { go2chef.RegisterStep(TypeName, Loader) } } func (s *Step) findPKG() (string, error) { re, err := regexp.Compile(s.PKGMatch) if err != nil { return "", err } return util.MatchPath(s.downloadPath, re) } func (s *Step) findDMG() (string, error) { re, err := regexp.Compile(s.DMGMatch) if err != nil { return "", err } return util.MatchPath(s.downloadPath, re) } func (s *Step) mountDMG(dmg string) error { ctx := context.Background() tmpdir, err := temp.Dir("", "") if err != nil { return err } cmd := exec.CommandContext(ctx, "hdiutil", "mount", dmg, "-mountroot", tmpdir, ) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { return err } dir, err := getDMGVolume(tmpdir) if err != nil { s.downloadPath = tmpdir defer func() { _ = s.unmountDMG() }() return err } s.downloadPath = dir return nil } func (s *Step) unmountDMG() error { ctx := context.Background() cmd := exec.CommandContext(ctx, "hdiutil", "detach", s.downloadPath) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { return err } return nil } func getDMGVolume(dir string) (string, error) { ents, err := ioutil.ReadDir(dir) if err != nil { return "", err } if len(ents) < 1 { return "", fmt.Errorf("DMG volume mount contains no volume folder") } return filepath.Join(dir, ents[0].Name()), nil }