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 }