github.com/buildpacks/pack@v0.33.3-0.20240516162812-884dd1837311/acceptance/invoke/pack.go (about)

     1  //go:build acceptance
     2  // +build acceptance
     3  
     4  package invoke
     5  
     6  import (
     7  	"bytes"
     8  	"fmt"
     9  	"os"
    10  	"os/exec"
    11  	"path/filepath"
    12  	"regexp"
    13  	"strings"
    14  	"sync"
    15  	"testing"
    16  
    17  	"github.com/Masterminds/semver"
    18  
    19  	acceptanceOS "github.com/buildpacks/pack/acceptance/os"
    20  	h "github.com/buildpacks/pack/testhelpers"
    21  )
    22  
    23  type PackInvoker struct {
    24  	testObject      *testing.T
    25  	assert          h.AssertionManager
    26  	path            string
    27  	home            string
    28  	dockerConfigDir string
    29  	fixtureManager  PackFixtureManager
    30  	verbose         bool
    31  }
    32  
    33  type packPathsProvider interface {
    34  	Path() string
    35  	FixturePaths() []string
    36  }
    37  
    38  func NewPackInvoker(
    39  	testObject *testing.T,
    40  	assert h.AssertionManager,
    41  	packAssets packPathsProvider,
    42  	dockerConfigDir string,
    43  ) *PackInvoker {
    44  
    45  	testObject.Helper()
    46  
    47  	home, err := os.MkdirTemp("", "buildpack.pack.home.")
    48  	if err != nil {
    49  		testObject.Fatalf("couldn't create home folder for pack: %s", err)
    50  	}
    51  
    52  	return &PackInvoker{
    53  		testObject:      testObject,
    54  		assert:          assert,
    55  		path:            packAssets.Path(),
    56  		home:            home,
    57  		dockerConfigDir: dockerConfigDir,
    58  		verbose:         true,
    59  		fixtureManager: PackFixtureManager{
    60  			testObject: testObject,
    61  			assert:     assert,
    62  			locations:  packAssets.FixturePaths(),
    63  		},
    64  	}
    65  }
    66  
    67  func (i *PackInvoker) Cleanup() {
    68  	if i == nil {
    69  		return
    70  	}
    71  	i.testObject.Helper()
    72  
    73  	err := os.RemoveAll(i.home)
    74  	i.assert.Nil(err)
    75  }
    76  
    77  func (i *PackInvoker) cmd(name string, args ...string) *exec.Cmd {
    78  	i.testObject.Helper()
    79  
    80  	cmdArgs := append([]string{name}, args...)
    81  	cmdArgs = append(cmdArgs, "--no-color")
    82  	if i.verbose {
    83  		cmdArgs = append(cmdArgs, "--verbose")
    84  	}
    85  
    86  	cmd := i.baseCmd(cmdArgs...)
    87  
    88  	cmd.Env = append(os.Environ(), "DOCKER_CONFIG="+i.dockerConfigDir)
    89  	if i.home != "" {
    90  		cmd.Env = append(cmd.Env, "PACK_HOME="+i.home)
    91  	}
    92  
    93  	return cmd
    94  }
    95  
    96  func (i *PackInvoker) baseCmd(parts ...string) *exec.Cmd {
    97  	return exec.Command(i.path, parts...)
    98  }
    99  
   100  func (i *PackInvoker) Run(name string, args ...string) (string, error) {
   101  	i.testObject.Helper()
   102  
   103  	output, err := i.cmd(name, args...).CombinedOutput()
   104  
   105  	return string(output), err
   106  }
   107  
   108  func (i *PackInvoker) SetVerbose(verbose bool) {
   109  	i.verbose = verbose
   110  }
   111  
   112  func (i *PackInvoker) RunSuccessfully(name string, args ...string) string {
   113  	i.testObject.Helper()
   114  
   115  	output, err := i.Run(name, args...)
   116  	i.assert.NilWithMessage(err, output)
   117  
   118  	return output
   119  }
   120  
   121  func (i *PackInvoker) JustRunSuccessfully(name string, args ...string) {
   122  	i.testObject.Helper()
   123  
   124  	_ = i.RunSuccessfully(name, args...)
   125  }
   126  
   127  func (i *PackInvoker) StartWithWriter(combinedOutput *bytes.Buffer, name string, args ...string) *InterruptCmd {
   128  	cmd := i.cmd(name, args...)
   129  	cmd.Stderr = combinedOutput
   130  	cmd.Stdout = combinedOutput
   131  
   132  	err := cmd.Start()
   133  	i.assert.Nil(err)
   134  
   135  	return &InterruptCmd{
   136  		testObject:     i.testObject,
   137  		assert:         i.assert,
   138  		cmd:            cmd,
   139  		combinedOutput: combinedOutput,
   140  	}
   141  }
   142  
   143  func (i *PackInvoker) Home() string {
   144  	return i.home
   145  }
   146  
   147  type InterruptCmd struct {
   148  	testObject     *testing.T
   149  	assert         h.AssertionManager
   150  	cmd            *exec.Cmd
   151  	combinedOutput *bytes.Buffer
   152  	outputMux      sync.Mutex
   153  }
   154  
   155  func (c *InterruptCmd) TerminateAtStep(pattern string) {
   156  	c.testObject.Helper()
   157  
   158  	for {
   159  		c.outputMux.Lock()
   160  		if strings.Contains(c.combinedOutput.String(), pattern) {
   161  			err := c.cmd.Process.Signal(acceptanceOS.InterruptSignal)
   162  			c.assert.Nil(err)
   163  			h.AssertNil(c.testObject, err)
   164  			return
   165  		}
   166  		c.outputMux.Unlock()
   167  	}
   168  }
   169  
   170  func (c *InterruptCmd) Wait() error {
   171  	return c.cmd.Wait()
   172  }
   173  
   174  func (i *PackInvoker) Version() string {
   175  	i.testObject.Helper()
   176  	return strings.TrimSpace(i.RunSuccessfully("version"))
   177  }
   178  
   179  func (i *PackInvoker) SanitizedVersion() string {
   180  	i.testObject.Helper()
   181  	// Sanitizing any git commit sha and build number from the version output
   182  	re := regexp.MustCompile(`\d+\.\d+\.\d+`)
   183  	return re.FindString(strings.TrimSpace(i.RunSuccessfully("version")))
   184  }
   185  
   186  func (i *PackInvoker) EnableExperimental() {
   187  	i.testObject.Helper()
   188  
   189  	i.JustRunSuccessfully("config", "experimental", "true")
   190  }
   191  
   192  // Supports returns whether or not the executor's pack binary supports a
   193  // given command string. The command string can take one of four forms:
   194  //   - "<command>" (e.g. "create-builder")
   195  //   - "<flag>" (e.g. "--verbose")
   196  //   - "<command> <flag>" (e.g. "build --network")
   197  //   - "<command>... <flag>" (e.g. "config trusted-builder --network")
   198  //
   199  // Any other form may return false.
   200  func (i *PackInvoker) Supports(command string) bool {
   201  	i.testObject.Helper()
   202  
   203  	parts := strings.Split(command, " ")
   204  
   205  	var cmdParts = []string{"help"}
   206  	var search string
   207  
   208  	if len(parts) > 1 {
   209  		last := len(parts) - 1
   210  		cmdParts = append(cmdParts, parts[:last]...)
   211  		search = parts[last]
   212  	} else {
   213  		cmdParts = append(cmdParts, command)
   214  		search = command
   215  	}
   216  
   217  	re := regexp.MustCompile(fmt.Sprint(`\b%s\b`, search))
   218  	output, err := i.baseCmd(cmdParts...).CombinedOutput()
   219  	i.assert.Nil(err)
   220  
   221  	// FIXME: this doesn't appear to be working as expected,
   222  	// as tests against "build --creation-time" and "build --cache" are returning unsupported
   223  	// even on the latest version of pack.
   224  	return re.MatchString(string(output)) && !strings.Contains(string(output), "Unknown help topic")
   225  }
   226  
   227  type Feature int
   228  
   229  const (
   230  	CreationTime = iota
   231  	Cache
   232  	BuildImageExtensions
   233  	RunImageExtensions
   234  	StackValidation
   235  	ForceRebase
   236  	BuildpackFlatten
   237  	MetaBuildpackFolder
   238  	PlatformRetries
   239  	FlattenBuilderCreationV2
   240  	FixesRunImageMetadata
   241  	ManifestCommands
   242  )
   243  
   244  var featureTests = map[Feature]func(i *PackInvoker) bool{
   245  	CreationTime: func(i *PackInvoker) bool {
   246  		return i.Supports("build --creation-time")
   247  	},
   248  	Cache: func(i *PackInvoker) bool {
   249  		return i.Supports("build --cache")
   250  	},
   251  	BuildImageExtensions: func(i *PackInvoker) bool {
   252  		return i.laterThan("v0.27.0")
   253  	},
   254  	RunImageExtensions: func(i *PackInvoker) bool {
   255  		return i.laterThan("v0.29.0")
   256  	},
   257  	StackValidation: func(i *PackInvoker) bool {
   258  		return !i.atLeast("v0.30.0")
   259  	},
   260  	ForceRebase: func(i *PackInvoker) bool {
   261  		return i.atLeast("v0.30.0")
   262  	},
   263  	BuildpackFlatten: func(i *PackInvoker) bool {
   264  		return i.atLeast("v0.30.0")
   265  	},
   266  	MetaBuildpackFolder: func(i *PackInvoker) bool {
   267  		return i.atLeast("v0.30.0")
   268  	},
   269  	PlatformRetries: func(i *PackInvoker) bool {
   270  		return i.atLeast("v0.32.1")
   271  	},
   272  	FlattenBuilderCreationV2: func(i *PackInvoker) bool {
   273  		return i.atLeast("v0.33.1")
   274  	},
   275  	FixesRunImageMetadata: func(i *PackInvoker) bool {
   276  		return i.atLeast("v0.34.0")
   277  	},
   278  	ManifestCommands: func(i *PackInvoker) bool {
   279  		return i.atLeast("v0.34.0")
   280  	},
   281  }
   282  
   283  func (i *PackInvoker) SupportsFeature(f Feature) bool {
   284  	return featureTests[f](i)
   285  }
   286  
   287  func (i *PackInvoker) semanticVersion() *semver.Version {
   288  	version := i.Version()
   289  	semanticVersion, err := semver.NewVersion(strings.TrimPrefix(strings.Split(version, " ")[0], "v"))
   290  	i.assert.Nil(err)
   291  
   292  	return semanticVersion
   293  }
   294  
   295  // laterThan returns true if pack version is older than the provided version
   296  func (i *PackInvoker) laterThan(version string) bool {
   297  	providedVersion := semver.MustParse(version)
   298  	ver := i.semanticVersion()
   299  	return ver.Compare(providedVersion) > 0 || ver.Equal(semver.MustParse("0.0.0"))
   300  }
   301  
   302  // atLeast returns true if pack version is the same or older than the provided version
   303  func (i *PackInvoker) atLeast(version string) bool {
   304  	minimalVersion := semver.MustParse(version)
   305  	ver := i.semanticVersion()
   306  	return ver.Equal(minimalVersion) || ver.GreaterThan(minimalVersion) || ver.Equal(semver.MustParse("0.0.0"))
   307  }
   308  
   309  func (i *PackInvoker) ConfigFileContents() string {
   310  	i.testObject.Helper()
   311  
   312  	contents, err := os.ReadFile(filepath.Join(i.home, "config.toml"))
   313  	i.assert.Nil(err)
   314  
   315  	return string(contents)
   316  }
   317  
   318  func (i *PackInvoker) FixtureManager() PackFixtureManager {
   319  	return i.fixtureManager
   320  }