Skip to content

Commit

Permalink
feat: improve installation by storing lib metadata locally
Browse files Browse the repository at this point in the history
  • Loading branch information
mefellows committed Jun 6, 2022
1 parent cb3a802 commit 45b9311
Show file tree
Hide file tree
Showing 8 changed files with 264 additions and 31 deletions.
42 changes: 42 additions & 0 deletions command/check.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package command

import (
"log"
"os"

"github.com/pact-foundation/pact-go/v2/installer"

"github.com/spf13/cobra"
)

var checkCmd = &cobra.Command{
Use: "check",
Short: "Check required libraries",
Long: "Check the correct version of required libraries",
Run: func(cmd *cobra.Command, args []string) {
setLogLevel(verbose, logLevel)

// Run the installer
i, err := installer.NewInstaller()
if err != nil {
log.Println("[ERROR] Your Pact library installation is out of date and we were unable to download a newer one for you:", err)
os.Exit(1)
}

if libDir != "" {
log.Println("[INFO] set lib dir target to", libDir)
i.SetLibDir(libDir)
}

if err = i.CheckPackageInstall(); err != nil {
log.Println("[DEBUG] error from CheckPackageInstall:", err)
log.Println("[ERROR] Your Pact library installation is out of date. Run `pact-go install` to correct")
os.Exit(1)
}
},
}

func init() {
checkCmd.Flags().StringVarP(&libDir, "libDir", "d", "", "Target directory of the library installation")
RootCmd.AddCommand(checkCmd)
}
14 changes: 7 additions & 7 deletions command/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,26 +13,26 @@ var libDir string
var force bool
var installCmd = &cobra.Command{
Use: "install",
Short: "Check required tools",
Long: "Checks versions of required Pact CLI tools for used by the library",
Short: "Install required libraries",
Long: "Install the correct version of required libraries",
Run: func(cmd *cobra.Command, args []string) {
setLogLevel(verbose, logLevel)

// Run the installer
i, err := installer.NewInstaller()

if err != nil {
log.Println("[ERROR] Your Pact library installation is out of date and we were unable to download a newer one for you:", err)
os.Exit(1)
}

if libDir != "" {
log.Println("[INFO] set lib dir target to", libDir)
i.SetLibDir(libDir)
}

i.Force(force)

if err != nil {
log.Println("[ERROR] Your Pact library installation is out of date and we were unable to download a newer one for you:", err)
os.Exit(1)
}

if err = i.CheckInstallation(); err != nil {
log.Println("[ERROR] Your Pact library installation is out of date and we were unable to download a newer one for you:", err)
os.Exit(1)
Expand Down
14 changes: 12 additions & 2 deletions docs/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,19 @@ You can also export `LOG_LEVEL=trace` before running a test to increase verbosit

## Library status check

Pact ships with a CLI that you can also use to check if the tools are up to date. Simply run `pact-go install`, exit status `0` is good, `1` or higher is bad.
Pact ships with a CLI that you can also use to check if the tools are up to date. Simply run `pact-go check` - an exit status of `0` is good, `1` or higher is bad. `pact-go install` will also do this, and also install any dependencies if missing.

You can also opt to have Pact automatically upgrade library version using the function `CheckVersion()`
You can also opt to have Pact automatically upgrade library version using the function `CheckVersion()`.

Pact go from 2.0.0-beta-11 onwards, also stores a configuration file in `~/.pact/pact-go.yml` that contains the version information for the current libraries it manages. You should not edit this file, however it has a structure as follows:

```yaml
libraries:
libpact_ffi:
libname: libpact_ffi
version: 0.3.2
hash: d6503769896eecbc027815d20aff19c3
```

#### Re-run a specific provider verification test

Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,6 @@ require (
google.golang.org/genproto v0.0.0-20220524023933-508584e28198 // indirect
google.golang.org/grpc v1.46.2
google.golang.org/protobuf v1.28.0
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -828,6 +828,8 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
Expand Down
186 changes: 167 additions & 19 deletions installer/installer.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,22 @@ package installer

import (
"fmt"
"io"
"io/ioutil"
"log"
"os"
"os/exec"
"os/user"
"path"
"path/filepath"
"runtime"
"strings"

getter "github.com/hashicorp/go-getter"
goversion "github.com/hashicorp/go-version"
"gopkg.in/yaml.v2"

"crypto/md5"

"github.com/spf13/afero"
)
Expand All @@ -21,6 +28,8 @@ import (
// packages if required
type Installer struct {
downloader downloader
hasher hasher
config configReadWriter
os string
arch string
fs afero.Fs
Expand All @@ -32,7 +41,7 @@ type installerConfig func(*Installer) error

// NewInstaller creates a new initialised Installer
func NewInstaller(opts ...installerConfig) (*Installer, error) {
i := &Installer{downloader: &defaultDownloader{}, fs: afero.NewOsFs()}
i := &Installer{downloader: &defaultDownloader{}, fs: afero.NewOsFs(), hasher: &defaultHasher{}, config: &configuration{}}

for _, opt := range opts {
opt(i)
Expand Down Expand Up @@ -77,7 +86,7 @@ func (i *Installer) CheckInstallation() error {
// Check if files exist
// --> Check if existing installed files
if !i.force {
if err := i.checkPackageInstall(); err == nil {
if err := i.CheckPackageInstall(); err == nil {
return nil
}
}
Expand All @@ -94,7 +103,7 @@ func (i *Installer) CheckInstallation() error {

// Double check files landed correctly (can't execute 'version' call here,
// because of dependency on the native libs we're trying to download!)
if err := i.checkPackageInstall(); err != nil {
if err := i.CheckPackageInstall(); err != nil {
return fmt.Errorf("unable to verify downloaded/installed dependencies: %s", err)
}

Expand All @@ -114,8 +123,8 @@ func (i *Installer) getLibDir() string {
return "/usr/local/lib"
}

// checkPackageInstall discovers any existing packages, and checks installation of a given binary using semver-compatible checks
func (i *Installer) checkPackageInstall() error {
// CheckPackageInstall discovers any existing packages, and checks installation of a given binary using semver-compatible checks
func (i *Installer) CheckPackageInstall() error {
for pkg, info := range packages {

dst, _ := i.getLibDstForPackage(pkg)
Expand All @@ -127,31 +136,46 @@ func (i *Installer) checkPackageInstall() error {
log.Println("[INFO] package", info.libName, "found")
}

lib, ok := LibRegistry[pkg]

lib, ok := i.config.readConfig().Libraries[pkg]
if ok {
log.Println("[INFO] checking version", lib.Version(), "for lib", info.libName, "within semver range", info.semverRange)
if err := checkVersion(info.libName, lib.Version(), info.semverRange); err != nil {
if err := checkVersion(info.libName, lib.Version, info.semverRange); err != nil {
return err
}
log.Println("[INFO] package", info.libName, "is correctly installed")
} else {
log.Println("[DEBUG] unable to determine current version of package", pkg, "this is probably because the package is currently being installed")
log.Println("[INFO] no package metadata information was found, run `pact-go install -f` to correct")
}
}

return nil
}
// This will only be populated during test when the ffi is loaded, but will actually test the FFI itself
// It is helpful because it will prevent issues where the FFI is manually updated without using the `pact-go install` command
if len(LibRegistry) == 0 {
log.Println("[DEBUG] skip checking ffi version() call because FFI not loaded. This is expected when running the 'pact-go' command.")
} else {
lib, ok := LibRegistry[pkg]

if ok {
log.Println("[INFO] checking version", lib.Version(), "for lib", info.libName, "within semver range", info.semverRange)
if err := checkVersion(info.libName, lib.Version(), info.semverRange); err != nil {
return err
}
} else {
log.Println("[DEBUG] unable to determine current version of package", pkg, "in LibRegistry", LibRegistry)
}

// getVersionForBinary gets the version of a given Ruby binary
func (i *Installer) getVersionForBinary(binary string) (version string, err error) {
log.Println("[DEBUG] running binary", binary)
// Correct the configuration to reduce drift
err := i.updateConfiguration(dst, pkg, info)
if err != nil {
return err
}
}
}

return "", nil
return nil
}

// TODO: checksums (they don't currently exist)
// Download all dependencies, and update the pact-go configuration file
func (i *Installer) downloadDependencies() error {
for pkg := range packages {
for pkg, pkgInfo := range packages {
src, err := i.getDownloadURLForPackage(pkg)

if err != nil {
Expand All @@ -169,6 +193,12 @@ func (i *Installer) downloadDependencies() error {
if err != nil {
return err
}

err = i.updateConfiguration(dst, pkg, pkgInfo)

if err != nil {
return err
}
}

return nil
Expand Down Expand Up @@ -215,6 +245,29 @@ func (i *Installer) getLibDstForPackage(pkg string) (string, error) {
return path.Join(i.getLibDir(), pkgInfo.libName) + "." + osToExtension[i.os], nil
}

// Write the metadata to reduce drift
func (i *Installer) updateConfiguration(dst string, pkg string, info packageInfo) error {
// Get hash of file
fmt.Println(i.hasher)
hash, err := i.hasher.hash(dst)
if err != nil {
return err
}

// Read metadata
c := i.config.readConfig()

// Update config
c.Libraries[pkg] = packageMetadata{
LibName: info.libName,
Version: info.version,
Hash: hash,
}

// Write metadata
return i.config.writeConfig(c)
}

var setOSXInstallName = func(file string, lib string) error {
cmd := exec.Command("install_name_tool", "-id", fmt.Sprintf("%s.dylib", lib), file)
stdoutStderr, err := cmd.CombinedOutput()
Expand Down Expand Up @@ -305,3 +358,98 @@ func (d *defaultDownloader) download(src string, dst string) error {

return getter.GetFile(dst, src)
}

type packageMetadata struct {
LibName string
Version string
Hash string
}

type pactConfig struct {
Libraries map[string]packageMetadata
}

type configReader interface {
readConfig() pactConfig
}
type configWriter interface {
writeConfig(pactConfig) error
}

type configReadWriter interface {
configReader
configWriter
}

type configuration struct{}

func getConfigPath() string {
user, err := user.Current()
if err != nil {
log.Fatalf(err.Error())
}

return path.Join(user.HomeDir, ".pact", "pact-go.yml")
}

func (configuration) readConfig() pactConfig {
pactConfigPath := getConfigPath()
c := pactConfig{
Libraries: map[string]packageMetadata{},
}

bytes, err := ioutil.ReadFile(pactConfigPath)
if err != nil {
log.Println("[DEBUG] error reading file", pactConfigPath, "error: ", err)
return c
}

err = yaml.Unmarshal(bytes, &c)
if err != nil {
log.Println("[DEBUG] error unmarshalling YAML", pactConfigPath, "error: ", err)
}
return c
}

func (configuration) writeConfig(c pactConfig) error {
log.Println("[DEBUG] writing config", c)
pactConfigPath := getConfigPath()

err := os.MkdirAll(filepath.Dir(pactConfigPath), 0644)
if err != nil {
log.Println("[DEBUG] error creating pact config directory")
return err
}

bytes, err := yaml.Marshal(c)
if err != nil {
log.Println("[DEBUG] error marshalling YAML", pactConfigPath, "error: ", err)
return err
}
log.Println("[DEBUG] writing yaml config to file", string(bytes))

return ioutil.WriteFile(pactConfigPath, bytes, 0644)
}

type hasher interface {
hash(src string) (string, error)
}

type defaultHasher struct{}

func (d *defaultHasher) hash(src string) (string, error) {
log.Println("[DEBUG] obtaining hash for file", src)

f, err := os.Open(src)
if err != nil {
return "", err
}
defer f.Close()

h := md5.New()
if _, err := io.Copy(h, f); err != nil {
return "", err
}

return fmt.Sprintf("%x", h.Sum(nil)), nil
}

0 comments on commit 45b9311

Please sign in to comment.