diff --git a/.gitpod.yml b/.gitpod.yml deleted file mode 100644 index b716e56cd5..0000000000 --- a/.gitpod.yml +++ /dev/null @@ -1,18 +0,0 @@ - -tasks: - - name: Setup - before: chmod ugo+w /var/run/docker.sock - init: make build - command: chmod ugo+w /var/run/docker.sock - -github: - prebuilds: - master: true - branches: true - pullRequests: true - pullRequestsFromForks: true - addCheck: true - -vscode: - extensions: - - golang.go diff --git a/builder/config_reader.go b/builder/config_reader.go index 6ebd688d55..78d1eabda6 100644 --- a/builder/config_reader.go +++ b/builder/config_reader.go @@ -9,6 +9,7 @@ import ( "github.com/BurntSushi/toml" "github.com/pkg/errors" + "github.com/buildpacks/pack/buildpackage" "github.com/buildpacks/pack/internal/config" "github.com/buildpacks/pack/internal/style" "github.com/buildpacks/pack/pkg/dist" @@ -25,6 +26,13 @@ type Config struct { Lifecycle LifecycleConfig `toml:"lifecycle"` Run RunConfig `toml:"run"` Build BuildConfig `toml:"build"` + WithTargets []dist.Target `toml:"targets,omitempty"` +} + +type MultiArchConfig struct { + Config + flagTargets []dist.Target + relativeBaseDir string } // ModuleCollection is a list of ModuleConfigs @@ -115,6 +123,151 @@ func ReadConfig(path string) (config Config, warnings []string, err error) { return config, warnings, nil } +func (c *MultiArchConfig) Targets() []dist.Target { + if len(c.flagTargets) != 0 { + return c.flagTargets + } + + return c.WithTargets +} + +func ReadMultiArchConfig(path string, flagTargets []dist.Target) (config MultiArchConfig, warnings []string, err error) { + file, err := os.Open(filepath.Clean(path)) + if err != nil { + return MultiArchConfig{}, nil, errors.Wrap(err, "opening config file") + } + defer file.Close() + + config, err = parseMultiArchConfig(file) + if err != nil { + return MultiArchConfig{}, nil, errors.Wrapf(err, "parse contents of '%s'", path) + } + + if len(config.Order) == 0 { + warnings = append(warnings, fmt.Sprintf("empty %s definition", style.Symbol("order"))) + } + + config.mergeStackWithImages() + if len(flagTargets) != 0 { + config.flagTargets = flagTargets + } + return config, warnings, nil +} + +func (c *MultiArchConfig) BuilderConfigs(getIndexManifest buildpackage.GetIndexManifestFn) (configs []Config, err error) { + for _, target := range c.Targets() { + if err := target.Range(func(target dist.Target, distroName, distroVersion string) error { + cfg, err := c.processTarget(target, "", "", getIndexManifest) + configs = append(configs, cfg) + return err + }); err != nil { + return configs, err + } + } + return configs, nil +} + +func copyConfig(config Config) Config { + return Config{ + Description: config.Description, + Buildpacks: make(ModuleCollection, len(config.Buildpacks)), + Extensions: make(ModuleCollection, len(config.Extensions)), + Order: make(dist.Order, len(config.Order)), + OrderExtensions: make(dist.Order, len(config.OrderExtensions)), + Stack: StackConfig{ + RunImageMirrors: make([]string, len(config.Stack.RunImageMirrors)), + }, + Run: RunConfig{ + Images: make([]RunImageConfig, len(config.Run.Images)), + }, + } +} + +func (c *MultiArchConfig) processTarget(target dist.Target, distroName, distroVersion string, getIndexManifest buildpackage.GetIndexManifestFn) (config Config, err error) { + config = copyConfig(c.Config) + target = buildpackage.ProcessTarget(target, distroName, distroVersion) + for i, bp := range c.Config.Buildpacks { + if bp.URI != "" { + if config.Buildpacks[i].URI, err = buildpackage.GetRelativeURI(bp.URI, c.relativeBaseDir, &target, getIndexManifest); err != nil { + return config, err + } + } + } + + for i, ext := range c.Config.Extensions { + if ext.URI != "" { + if config.Extensions[i].URI, err = buildpackage.GetRelativeURI(ext.URI, c.relativeBaseDir, &target, getIndexManifest); err != nil { + return config, err + } + } + } + + if img := c.Config.Build.Image; img != "" { + if config.Build.Image, err = buildpackage.ParseURItoString(img, target, getIndexManifest); err != nil { + return config, err + } + } + + for i, runImg := range c.Config.Run.Images { + config.Run.Images[i].Image, err = buildpackage.ParseURItoString(runImg.Image, target, getIndexManifest) + if len(config.Run.Images[i].Mirrors) == 0 { + config.Run.Images[i].Mirrors = make([]string, len(runImg.Mirrors)) + } + + if err != nil { + for j, mirror := range runImg.Mirrors { + if config.Run.Images[i].Mirrors[j], err = buildpackage.ParseURItoString(mirror, target, getIndexManifest); err == nil { + break + } + } + + if err != nil { + return config, err + } + } + } + + if img := c.Config.Stack.BuildImage; img != "" { + if config.Stack.BuildImage, err = buildpackage.ParseURItoString(img, target, getIndexManifest); err != nil { + return config, err + } + } + + if img := c.Config.Stack.RunImage; img != "" { + config.Stack.RunImage, err = buildpackage.ParseURItoString(img, target, getIndexManifest) + } + + if err != nil { + for i, mirror := range c.Config.Stack.RunImageMirrors { + if config.Stack.RunImageMirrors[i], err = buildpackage.ParseURItoString(mirror, target, getIndexManifest); err == nil { + break + } + } + } + + config.Order = c.Order + config.OrderExtensions = c.OrderExtensions + config.WithTargets = []dist.Target{target} + return config, err +} + +func (c *MultiArchConfig) MultiArch() bool { + targets := c.Targets() + if len(targets) > 1 { + return true + } + + targetsLen := 0 + for _, target := range targets { + target.Range(func(_ dist.Target, _, _ string) error { + targetsLen++ + return nil + }) + } + + return targetsLen > 1 +} + // ValidateConfig validates the config func ValidateConfig(c Config) error { if c.Build.Image == "" && c.Stack.BuildImage == "" { @@ -182,27 +335,48 @@ func parseConfig(file *os.File) (Config, error) { return builderConfig, nil } +// parseMultiArchConfig reads a builder configuration from file +func parseMultiArchConfig(file *os.File) (MultiArchConfig, error) { + multiArchBuilderConfig := MultiArchConfig{} + tomlMetadata, err := toml.NewDecoder(file).Decode(&multiArchBuilderConfig) + if err != nil { + return MultiArchConfig{}, errors.Wrap(err, "decoding MultiArchBuilder Toml") + } + + undecodedKeys := tomlMetadata.Undecoded() + if len(undecodedKeys) > 0 { + unknownElementsMsg := config.FormatUndecodedKeys(undecodedKeys) + + return MultiArchConfig{}, errors.Errorf("%s in %s", + unknownElementsMsg, + style.Symbol(file.Name()), + ) + } + + return multiArchBuilderConfig, nil +} + func ParseBuildConfigEnv(env []BuildConfigEnv, path string) (envMap map[string]string, warnings []string, err error) { envMap = map[string]string{} var appendOrPrependWithoutDelim = 0 for _, v := range env { - if name := v.Name; name == "" || len(name) == 0 { + if name := v.Name; name == "" { return nil, nil, errors.Wrapf(errors.Errorf("env name should not be empty"), "parse contents of '%s'", path) } - if val := v.Value; val == "" || len(val) == 0 { + if val := v.Value; val == "" { warnings = append(warnings, fmt.Sprintf("empty value for key/name %s", style.Symbol(v.Name))) } suffixName, delimName, err := getBuildConfigEnvFileName(v) if err != nil { return envMap, warnings, err } - if val, e := envMap[suffixName]; e { + if val, ok := envMap[suffixName]; ok { warnings = append(warnings, fmt.Sprintf(errors.Errorf("overriding env with name: %s and suffix: %s from %s to %s", style.Symbol(v.Name), style.Symbol(string(v.Suffix)), style.Symbol(val), style.Symbol(v.Value)).Error(), "parse contents of '%s'", path)) } - if val, e := envMap[delimName]; e { + if val, ok := envMap[delimName]; ok { warnings = append(warnings, fmt.Sprintf(errors.Errorf("overriding env with name: %s and delim: %s from %s to %s", style.Symbol(v.Name), style.Symbol(v.Delim), style.Symbol(val), style.Symbol(v.Value)).Error(), "parse contents of '%s'", path)) } - if delim := v.Delim; (delim != "" || len(delim) != 0) && (delimName != "" || len(delimName) != 0) { + if delim := v.Delim; delim != "" && delimName != "" { envMap[delimName] = delim } envMap[suffixName] = v.Value @@ -234,7 +408,7 @@ func getBuildConfigEnvFileName(env BuildConfigEnv) (suffixName, delimName string } else { suffixName = env.Name + suffix } - if delim := env.Delim; delim != "" || len(delim) != 0 { + if delim := env.Delim; delim != "" { delimName = env.Name + ".delim" } return suffixName, delimName, err diff --git a/builder/config_reader_test.go b/builder/config_reader_test.go index 6c5512f897..02dbc4071b 100644 --- a/builder/config_reader_test.go +++ b/builder/config_reader_test.go @@ -5,11 +5,13 @@ import ( "path/filepath" "testing" + "github.com/BurntSushi/toml" "github.com/heroku/color" "github.com/sclevine/spec" "github.com/sclevine/spec/report" "github.com/buildpacks/pack/builder" + "github.com/buildpacks/pack/pkg/dist" h "github.com/buildpacks/pack/testhelpers" ) @@ -336,4 +338,171 @@ uri = "noop-buildpack.tgz" h.AssertMapContains[string, string](t, env, h.NewKeyValue[string, string]("key", "value")) }) }) + when("#ReadMultiArchConfig", func() { + var ( + tmpDir string + builderConfigPath string + err error + builderConfig builder.MultiArchConfig + ) + it.Before(func() { + tmpDir, err = os.MkdirTemp("", "config-test") + h.AssertNil(t, err) + builderConfigPath = filepath.Join(tmpDir, "builder.toml") + builderConfig = builder.MultiArchConfig{ + Config: builder.Config{ + Buildpacks: builder.ModuleCollection{ + { + ImageOrURI: dist.ImageOrURI{ + BuildpackURI: dist.BuildpackURI{ + URI: "busybox:1.36-musl", + }, + }, + }, + }, + WithTargets: []dist.Target{ + { + OS: "linux", + Arch: "amd64", + }, + { + OS: "linux", + Arch: "arm", + ArchVariant: "v6", + }, + }, + Order: dist.Order{ + dist.OrderEntry{ + Group: []dist.ModuleRef{ + { + ModuleInfo: dist.ModuleInfo{ + Name: "busybox", + Version: "1.36-musl", + }, + }, + }, + }, + }, + }, + } + }) + it.After(func() { + h.AssertNil(t, os.RemoveAll(tmpDir)) + }) + it("should return multi-arch config", func() { + file, err := os.OpenFile(builderConfigPath, os.O_TRUNC|os.O_WRONLY|os.O_CREATE, os.ModePerm) + h.AssertNil(t, err) + h.AssertNil(t, toml.NewEncoder(file).Encode(&builderConfig)) + + config, warnings, err := builder.ReadMultiArchConfig(builderConfigPath, nil) + h.AssertNil(t, err) + h.AssertEq(t, len(warnings), 0) + h.AssertEq(t, config.Config, builderConfig.Config) + }) + it("should return multi-arch config with flag targets", func() { + file, err := os.OpenFile(builderConfigPath, os.O_TRUNC|os.O_WRONLY|os.O_CREATE, os.ModePerm) + h.AssertNil(t, err) + h.AssertNil(t, toml.NewEncoder(file).Encode(&builderConfig)) + + config, warnings, err := builder.ReadMultiArchConfig(builderConfigPath, []dist.Target{ + { + OS: "some-os", + Arch: "some-arch", + }, + }) + h.AssertNil(t, err) + h.AssertEq(t, len(warnings), 0) + h.AssertEq(t, config.Config, builderConfig.Config) + h.AssertNotEq(t, config.Targets(), builderConfig.Config.WithTargets) + }) + it("should return an error", func() { + _, _, err := builder.ReadMultiArchConfig(builderConfigPath, nil) + h.AssertNotNil(t, err) + }) + it("should return a warning when order is not specified", func() { + file, err := os.OpenFile(builderConfigPath, os.O_TRUNC|os.O_WRONLY|os.O_CREATE, os.ModePerm) + h.AssertNil(t, err) + + builderConfig := builderConfig + builderConfig.Order = nil + + h.AssertNil(t, toml.NewEncoder(file).Encode(&builderConfig)) + + config, warnings, err := builder.ReadMultiArchConfig(builderConfigPath, nil) + h.AssertNil(t, err) + h.AssertEq(t, len(warnings), 1) + h.AssertEq(t, config.Config, builderConfig.Config) + }) + when("#BuilderConfigs", func() { + var ( + config builder.MultiArchConfig + warnings []string + ) + it.Before(func() { + builderConfig.Extensions = builder.ModuleCollection{ + { + ImageOrURI: dist.ImageOrURI{BuildpackURI: dist.BuildpackURI{URI: "./some-uri"}}, + }, + } + builderConfig.Build.Image = "some/build:image" + builderConfig.Run.Images = append(builderConfig.Run.Images, builder.RunImageConfig{ + Image: "$ome/image+", + Mirrors: []string{"some/run-image:mirror"}, + }) + builderConfig.Stack = builder.StackConfig{ + BuildImage: "some/stack:build-image", + RunImage: "$ome/run-image", + RunImageMirrors: []string{"some/stack:run-image1"}, + } + file, err := os.OpenFile(builderConfigPath, os.O_TRUNC|os.O_WRONLY|os.O_CREATE, os.ModePerm) + h.AssertNil(t, err) + h.AssertNil(t, toml.NewEncoder(file).Encode(&builderConfig)) + config, warnings, err = builder.ReadMultiArchConfig(builderConfigPath, nil) + h.AssertEq(t, len(warnings), 0) + h.AssertEq(t, err, nil) + }) + it.After(func() { + h.AssertNil(t, os.RemoveAll(tmpDir)) + }) + it("should return multiple configs", func() { + configs, err := config.BuilderConfigs(h.FakeIndexManifestBuilderFn(config.Targets())) + h.AssertNil(t, err) + h.AssertEq(t, len(configs), len(config.Targets())) + }) + }) + when("#MultiArch", func() { + it("should return true when multi-target config provided", func() { + h.AssertTrue(t, builderConfig.MultiArch()) + }) + it("should return true when multi-distro config provided", func() { + builderConfig := builderConfig + builderConfig.WithTargets = []dist.Target{ + { + Distributions: []dist.Distribution{ + { + Name: "distro1", + }, { + Name: "distro2", + }, + }, + }, + } + h.AssertTrue(t, builderConfig.MultiArch()) + }) + it("should return true when single distro multi-version config provided", func() { + builderConfig := builderConfig + builderConfig.WithTargets = []dist.Target{ + { + Distributions: []dist.Distribution{ + { + Name: "distro1", + Versions: []string{"version1", "version2"}, + }, + }, + }, + } + h.AssertTrue(t, builderConfig.MultiArch()) + }) + }) + }) } diff --git a/buildpackage/config_reader.go b/buildpackage/config_reader.go index d8b8d7d436..b207649d32 100644 --- a/buildpackage/config_reader.go +++ b/buildpackage/config_reader.go @@ -19,7 +19,7 @@ type Config struct { Buildpack dist.BuildpackURI `toml:"buildpack"` Extension dist.BuildpackURI `toml:"extension"` Dependencies []dist.ImageOrURI `toml:"dependencies"` - Platform dist.Platform `toml:"platform"` + Platform dist.Platform `toml:"platform,omitempty"` } func DefaultConfig() Config { @@ -117,6 +117,48 @@ func (r *ConfigReader) Read(path string) (Config, error) { return packageConfig, nil } +func (r *ConfigReader) ReadBuildpackDescriptor(path string) (dist.BuildpackDescriptor, error) { + buildpackConfig := dist.BuildpackDescriptor{} + + tomlMetadata, err := toml.DecodeFile(path, &buildpackConfig) + if err != nil { + return buildpackConfig, errors.Wrap(err, "decoding toml") + } + + undecodedKeys := tomlMetadata.Undecoded() + if len(undecodedKeys) > 0 { + unknownElementsMsg := config.FormatUndecodedKeys(undecodedKeys) + + return buildpackConfig, errors.Errorf("%s in %s", + unknownElementsMsg, + style.Symbol(path), + ) + } + + return buildpackConfig, nil +} + +func (r *ConfigReader) ReadExtensionDescriptor(path string) (dist.ExtensionDescriptor, error) { + extensionConfig := dist.ExtensionDescriptor{} + + tomlMetadata, err := toml.DecodeFile(path, &extensionConfig) + if err != nil { + return extensionConfig, errors.Wrap(err, "decoding toml") + } + + undecodedKeys := tomlMetadata.Undecoded() + if len(undecodedKeys) > 0 { + unknownElementsMsg := config.FormatUndecodedKeys(undecodedKeys) + + return extensionConfig, errors.Errorf("%s in %s", + unknownElementsMsg, + style.Symbol(path), + ) + } + + return extensionConfig, nil +} + func validateURI(uri, relativeBaseDir string) error { locatorType, err := buildpack.GetLocatorType(uri, relativeBaseDir, nil) if err != nil { diff --git a/buildpackage/multi_arch_config.go b/buildpackage/multi_arch_config.go new file mode 100644 index 0000000000..23da5b5c08 --- /dev/null +++ b/buildpackage/multi_arch_config.go @@ -0,0 +1,489 @@ +package buildpackage + +import ( + "errors" + "fmt" + "net/url" + "os" + "path/filepath" + "strings" + + "github.com/BurntSushi/toml" + "github.com/google/go-containerregistry/pkg/name" + v1 "github.com/google/go-containerregistry/pkg/v1" + + "github.com/buildpacks/imgutil" + + "github.com/buildpacks/pack/internal/paths" + "github.com/buildpacks/pack/internal/style" + "github.com/buildpacks/pack/pkg/buildpack" + "github.com/buildpacks/pack/pkg/dist" + "github.com/buildpacks/pack/pkg/logging" +) + +const ( + DigestDelim = "@" + TagDelim = ":" + RegistryDelim = "/" + PackageToml = "package.toml" + BuildpackToml = "buildpack.toml" +) + +type BuildpackType int +type GetIndexManifestFn func(ref name.Reference) (*v1.IndexManifest, error) + +const ( + Buildpack BuildpackType = iota + Composite + Extension +) + +type multiArchBuildpack struct { + relativeBaseDir string + flatten bool + flagTargets []dist.Target + config dist.BuildpackDescriptor + flattenChanged bool +} + +type multiArchExtension struct { + config dist.ExtensionDescriptor + flagTargets []dist.Target + relativeBaseDir string +} + +type IndexOptions struct { + BPConfigs *[]MultiArchBuildpackConfig + ExtConfigs *[]MultiArchExtensionConfig + PkgConfig *MultiArchPackage + Logger logging.Logger + RelativeBaseDir string + Targets []dist.Target + ImageIndex imgutil.ImageIndex +} + +type MultiArchPackage struct { + Config + relativeBaseDir string +} + +type MultiArchBuildpackConfig struct { + dist.BuildpackDescriptor + dist.Platform + bpType BuildpackType + relativeBaseDir string + Flatten bool + FlattenExclude []string + Labels map[string]string +} + +type MultiArchExtensionConfig struct { + dist.ExtensionDescriptor + dist.Platform + relativeBaseDir string +} + +func NewMultiArchBuildpack(config dist.BuildpackDescriptor, relativeBaseDir string, flatten, flattenChanged bool, flags []dist.Target) *multiArchBuildpack { + if relativeBaseDir == "" { + relativeBaseDir = "." + } + + return &multiArchBuildpack{ + relativeBaseDir: relativeBaseDir, + config: config, + flatten: flatten, + flagTargets: flags, + flattenChanged: flattenChanged, + } +} + +func NewMultiArchExtension(config dist.ExtensionDescriptor, relativeBaseDir string, flags []dist.Target) *multiArchExtension { + if relativeBaseDir == "" { + relativeBaseDir = "." + } + + return &multiArchExtension{ + config: config, + flagTargets: flags, + relativeBaseDir: relativeBaseDir, + } +} + +func NewMultiArchPackage(config Config, relativeBaseDir string) *MultiArchPackage { + if relativeBaseDir == "" { + relativeBaseDir = "." + } + + return &MultiArchPackage{ + relativeBaseDir: relativeBaseDir, + Config: config, + } +} + +func (m *multiArchBuildpack) Targets() []dist.Target { + if len(m.flagTargets) > 0 { + return m.flagTargets + } + return m.config.WithTargets +} + +func (m *multiArchExtension) Targets() []dist.Target { + if len(m.flagTargets) > 0 { + return m.flagTargets + } + return m.config.WithTargets +} + +func (m *multiArchBuildpack) MultiArchConfigs() (configs []MultiArchBuildpackConfig, err error) { + for _, target := range m.Targets() { + if err := target.Range(func(target dist.Target, distroName, distroVersion string) error { + cfg, err := m.processTarget(target, distroName, distroVersion) + configs = append(configs, cfg) + return err + }); err != nil { + return configs, err + } + } + return configs, nil +} + +func (m *multiArchExtension) MultiArchConfigs() (configs []MultiArchExtensionConfig, err error) { + for _, target := range m.Targets() { + if err := target.Range(func(target dist.Target, distroName, distroVersion string) error { + cfg, err := m.processTarget(target, distroName, distroVersion) + configs = append(configs, cfg) + return err + }); err != nil { + return configs, err + } + } + return configs, nil +} + +func (m *multiArchBuildpack) processTarget(target dist.Target, distroName, distroVersion string) (MultiArchBuildpackConfig, error) { + var bpType = Buildpack + if len(m.config.WithOrder) > 0 { + bpType = Composite + } + + if m.config.WithInfo.Version != "" { + target.Specs.OSVersion = m.config.WithInfo.Version + } + + rel, err := filepath.Abs(filepath.Join(m.relativeBaseDir, buildpack.PlatformRootDirectory(target, distroName, distroVersion), "buildpack.toml")) + if err != nil { + return MultiArchBuildpackConfig{}, err + } + + return MultiArchBuildpackConfig{ + BuildpackDescriptor: dist.BuildpackDescriptor{ + WithInfo: m.config.WithInfo, + WithTargets: []dist.Target{ + ProcessTarget(target, distroName, distroVersion), + }, + WithAPI: m.config.WithAPI, + WithLinuxBuild: m.config.WithLinuxBuild, + WithWindowsBuild: m.config.WithWindowsBuild, + WithStacks: m.config.WithStacks, + WithOrder: m.config.WithOrder, + }, + Platform: dist.Platform{OS: target.OS}, + Flatten: (m.flattenChanged && m.flatten) || (!m.flattenChanged && target.Specs.Flatten), // let the flag value take precedence over the config + FlattenExclude: target.Specs.FlattenExclude, + Labels: target.Specs.Labels, + relativeBaseDir: rel, + bpType: bpType, + }, nil +} + +func (m *multiArchExtension) processTarget(target dist.Target, distroName, distroVersion string) (MultiArchExtensionConfig, error) { + if m.config.WithInfo.Version != "" { + target.Specs.OSVersion = m.config.WithInfo.Version + } + + rel, err := filepath.Abs(filepath.Join(m.relativeBaseDir, buildpack.PlatformRootDirectory(target, distroName, distroVersion), "extension.toml")) + if err != nil { + return MultiArchExtensionConfig{}, err + } + + return MultiArchExtensionConfig{ + ExtensionDescriptor: dist.ExtensionDescriptor{ + WithInfo: m.config.WithInfo, + WithTargets: []dist.Target{ + ProcessTarget(target, distroName, distroVersion), + }, + WithAPI: m.config.WithAPI, + }, + Platform: dist.Platform{OS: target.OS}, + relativeBaseDir: rel, + }, nil +} + +func ProcessTarget(target dist.Target, distroName, distroVersion string) dist.Target { + target.Distributions = append([]dist.Distribution{}, dist.Distribution{Name: distroName, Versions: []string{distroVersion}}) + return target +} + +func (m *MultiArchBuildpackConfig) Path() string { + var target dist.Target + targets := m.Targets() + if len(targets) != 0 { + target = targets[0] + } + + if path := target.Specs.Path; path != "" { + return filepath.Join(path, "buildpack.toml") + } + + return m.relativeBaseDir +} + +func (m *MultiArchExtensionConfig) Path() string { + var target dist.Target + targets := m.Targets() + if len(targets) != 0 { + target = targets[0] + } + + if path := target.Specs.Path; path != "" { + return filepath.Join(path, "extension.toml") + } + + return m.relativeBaseDir +} + +func (m *MultiArchBuildpackConfig) CopyBuildpackToml(getIndexManifest GetIndexManifestFn) (err error) { + if uri := m.BuildpackDescriptor.WithInfo.ID; uri == "" { + return errors.New("invalid MultiArchBuildpackConfig") + } + + writeBPPath := m.Path() + if err := os.MkdirAll(filepath.Dir(writeBPPath), os.ModePerm); err != nil { + return err + } + + bpFile, err := os.Create(writeBPPath) + if err != nil { + return err + } + defer bpFile.Close() + + return toml.NewEncoder(bpFile).Encode(m.BuildpackDescriptor) +} + +func (m *MultiArchExtensionConfig) CopyExtensionToml(getIndexManifest GetIndexManifestFn) (err error) { + if uri := m.ExtensionDescriptor.WithInfo.ID; uri == "" { + return errors.New("invalid MultiArchBuildpackConfig") + } + + writeExtPath := m.Path() + if err := os.MkdirAll(filepath.Dir(writeExtPath), os.ModePerm); err != nil { + return err + } + + extFile, err := os.Create(writeExtPath) + if err != nil { + return err + } + defer extFile.Close() + + return toml.NewEncoder(extFile).Encode(m.ExtensionDescriptor) +} + +func (m *MultiArchBuildpackConfig) BuildpackType() BuildpackType { + return m.bpType +} + +func (m *MultiArchBuildpackConfig) RelativeBaseDir() string { + return m.relativeBaseDir +} + +func (m *MultiArchExtensionConfig) RelativeBaseDir() string { + return m.relativeBaseDir +} + +func (m *MultiArchBuildpackConfig) CleanBuildpackToml() error { + return os.Remove(m.Path()) +} + +func (m *MultiArchExtensionConfig) CleanExtensionToml() error { + return os.Remove(m.Path()) +} + +func (m *MultiArchPackage) RelativeBaseDir() string { + return m.relativeBaseDir +} + +func (m *MultiArchPackage) CopyPackageToml(relativeTo string, target dist.Target, distroName, version string, getIndexManifest GetIndexManifestFn) (err error) { + multiArchPKGConfig := *m + if (multiArchPKGConfig.Buildpack.URI == "" && multiArchPKGConfig.Extension.URI == "") || (multiArchPKGConfig.Buildpack.URI != "" && multiArchPKGConfig.Extension.URI != "") { + return errors.New("unexpected: one of Buildpack URI, Extension URI must be specified") + } + + if uri := multiArchPKGConfig.Buildpack.URI; uri != "" { + if multiArchPKGConfig.Buildpack.URI, err = GetRelativeURI(uri, multiArchPKGConfig.relativeBaseDir, &target, getIndexManifest); err != nil { + return err + } + } + + if uri := multiArchPKGConfig.Extension.URI; uri != "" { + if multiArchPKGConfig.Extension.URI, err = GetRelativeURI(uri, multiArchPKGConfig.relativeBaseDir, &target, getIndexManifest); err != nil { + return err + } + } + + for i, dep := range multiArchPKGConfig.Dependencies { + // dep.ImageName == dep.ImageRef.ImageName, dep.URI == dep.Buildpack.URI + if dep.URI != "" { + if m.Dependencies[i].URI, err = GetRelativeURI(dep.URI, multiArchPKGConfig.relativeBaseDir, &target, getIndexManifest); err != nil { + return err + } + } + } + + platformRootDir := buildpack.PlatformRootDirectory(target, distroName, version) + path, err := filepath.Abs(filepath.Join(relativeTo, platformRootDir, PackageToml)) + if err != nil { + return err + } + + // create parent folder if not exists + if err := os.MkdirAll(filepath.Dir(path), os.ModePerm); err != nil { + return err + } + + bpFile, err := os.Create(path) + if err != nil { + return err + } + defer bpFile.Close() + + return toml.NewEncoder(bpFile).Encode(multiArchPKGConfig.Config) +} + +func GetRelativeURI(uri, relativeBaseDir string, target *dist.Target, getIndexManifest GetIndexManifestFn) (string, error) { + locator, err := buildpack.GetLocatorType(uri, relativeBaseDir, []dist.ModuleInfo{}) + if err != nil { + return "", err + } + + switch locator { + case buildpack.URILocator: + // should return URLs in AS IS format + if URL, err := url.Parse(uri); err == nil && URL.Host != "" { + return uri, nil + } + + // returns file://-[os][-arch][-variant]-[name@version] + // for multi-arch we need target specific path appended at the end of name + uri, err = filepath.Abs(filepath.Join(relativeBaseDir, uri)) + if err != nil { + return "", err + } + + distro, target, version := dist.Distribution{}, *target, "" + if len(target.Distributions) > 0 { + distro = target.Distributions[0] + } + + if len(distro.Versions) > 0 { + version = distro.Versions[0] + } + + return paths.FilePathToURI(filepath.Join(uri, buildpack.PlatformRootDirectory(target, distro.Name, version)), "") + case buildpack.PackageLocator: + if target == nil { + return "", fmt.Errorf("nil target") + } + ref, err := ParseURItoString(buildpack.ParsePackageLocator(uri), *target, getIndexManifest) + return "docker://" + ref, err + case buildpack.RegistryLocator: + if target == nil { + return "", fmt.Errorf("nil target") + } + + rNS, rName, rVersion, err := buildpack.ParseRegistryID(uri) + if err != nil { + return uri, err + } + + ref, err := ParseURItoString(rNS+RegistryDelim+rName+DigestDelim+rVersion, *target, getIndexManifest) + return "urn:cnb:registry:" + ref, err + case buildpack.FromBuilderLocator: + fallthrough + case buildpack.IDLocator: + fallthrough + default: + return uri, nil + } +} + +func ParseURItoString(uri string, target dist.Target, getIndexManifest GetIndexManifestFn) (string, error) { + if strings.Contains(uri, DigestDelim) { + ref := strings.Split(uri, DigestDelim) + registry, hashStr := ref[0], ref[1] + if _, err := v1.NewHash(hashStr); err != nil { + uri = registry + TagDelim + hashStr + } + } + + ref, err := name.ParseReference(uri, name.Insecure, name.WeakValidation) + if err != nil { + return "", err + } + + idx, err := getIndexManifest(ref) + if err != nil { + return "", err + } + + if idx == nil { + return "", imgutil.ErrManifestUndefined + } + + fmt.Printf("fetching image from repo: %s \n", style.Symbol(uri)) + digest, err := DigestFromIndex(idx, target) + if err != nil { + return "", err + } + + return ref.Context().Digest(digest).Name(), nil +} + +func (m *MultiArchPackage) CleanPackageToml(relativeTo string, target dist.Target, distroName, version string) error { + path, err := filepath.Abs(filepath.Join(relativeTo, buildpack.PlatformRootDirectory(target, distroName, version), PackageToml)) + if err != nil { + return err + } + + return os.Remove(path) +} + +func DigestFromIndex(idx *v1.IndexManifest, target dist.Target) (string, error) { + if idx == nil { + return "", imgutil.ErrManifestUndefined + } + + targetPlatform := *target.Platform() + for _, mfest := range idx.Manifests { + if mfest.Platform == nil { + return "", imgutil.ErrPlatformUndefined + } + + if platform := mfest.Platform; platform.Satisfies(targetPlatform) { + return mfest.Digest.String(), nil + } + } + + return "", fmt.Errorf( + "no image found for given platform %s", + style.Symbol( + fmt.Sprintf( + "%s/%s/%s", + targetPlatform.OS, + targetPlatform.Architecture, + targetPlatform.Variant, + ), + ), + ) +} diff --git a/buildpackage/multi_arch_config_test.go b/buildpackage/multi_arch_config_test.go new file mode 100644 index 0000000000..d48b541eea --- /dev/null +++ b/buildpackage/multi_arch_config_test.go @@ -0,0 +1,808 @@ +package buildpackage_test + +import ( + "os" + "path/filepath" + "testing" + + "github.com/BurntSushi/toml" + "github.com/google/go-containerregistry/pkg/name" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/heroku/color" + "github.com/sclevine/spec" + "github.com/sclevine/spec/report" + + "github.com/buildpacks/lifecycle/api" + + "github.com/buildpacks/pack/buildpackage" + "github.com/buildpacks/pack/pkg/buildpack" + "github.com/buildpacks/pack/pkg/dist" + h "github.com/buildpacks/pack/testhelpers" +) + +func TestMultiArchBuildpackageConfig(t *testing.T) { + color.Disable(true) + defer color.Disable(false) + spec.Run(t, "Multi Arch Buildpackage Config Reader", testMultiArchBuildpackageConfigReader, spec.Parallel(), spec.Report(report.Terminal{})) +} + +func testMultiArchBuildpackageConfigReader(t *testing.T, when spec.G, it spec.S) { + var ( + bpPath = "./someDir" + targets = []dist.Target{ + { + OS: "linux", + Arch: "arm", + ArchVariant: "v6", + Distributions: []dist.Distribution{ + { + Name: "ubuntu", + Versions: []string{"22.04", "20.04"}, + }, + { + Name: "debian", + Versions: []string{"8.0"}, + }, + }, + Specs: dist.TargetSpecs{ + Features: []string{"feature1", "feature2"}, + OSFeatures: []string{"osFeature1", "osFeature2"}, + URLs: []string{"url1", "url2"}, + Annotations: map[string]string{"key1": "value1", "key2": "value2"}, + Flatten: false, + FlattenExclude: make([]string, 0), + Labels: map[string]string{"io.buildpacks.distro.name": "debian"}, + Path: "some-path", + }, + }, + { + OS: "linux", + Arch: "amd64", + Distributions: []dist.Distribution{ + { + Name: "ubuntu", + Versions: []string{"version1", "version2"}, + }, + }, + }, + } + target = dist.Target{ + OS: "some-os", + Arch: "some-arch", + ArchVariant: "some-arch", + Distributions: []dist.Distribution{ + { + Name: "some-name", + Versions: []string{"some-version", "some-other-version"}, + }, + { + Name: "some-name1", + Versions: []string{"some-version1", "some-other-version"}, + }, + }, + Specs: dist.TargetSpecs{ + Features: []string{"some-feature"}, + OSFeatures: []string{"some-osFeature1", "someOSFeature2"}, + URLs: []string{"some-url1", "some-url2"}, + Annotations: map[string]string{"some-key1": "some-key2", "some-key2": "some-value2"}, + Flatten: true, + FlattenExclude: []string{}, + Labels: make(map[string]string), + Path: ".", + }, + } + buildpackURICurrent = dist.BuildpackURI{ + URI: ".", + } + dependencies = []dist.ImageOrURI{ + { + BuildpackURI: dist.BuildpackURI{ + URI: ".", + }, + }, + { + BuildpackURI: dist.BuildpackURI{ + URI: "urn:cnb:registry:paketo-buildpacks/node-engine@3.2.1", + }, + }, + { + BuildpackURI: dist.BuildpackURI{ + URI: "https://example.com/buildpack.tgz", + }, + }, + { + BuildpackURI: dist.BuildpackURI{ + URI: "docker://cnbs/some-bp", + }, + }, + { + BuildpackURI: dist.BuildpackURI{ + URI: "docker://cnbs/some-bp@sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", + }, + }, + { + BuildpackURI: dist.BuildpackURI{ + URI: "docker://cnbs/some-bp:some-tag", + }, + }, + { + ImageRef: dist.ImageRef{ + // FIXME: not sure if this ImageName is valid + ImageName: "cnbs/sample-package@hello-universe", + }, + }, + } + platform = dist.Platform{OS: "linux"} + packageConfig = buildpackage.Config{ + Buildpack: buildpackURICurrent, + Dependencies: dependencies, + Platform: platform, + } + ) + when("#NewMultiArchBuildpack", func() { + var ( + platformAPIVersion = api.Platform.Latest() + moduleInfo = dist.ModuleInfo{ + ID: "some/buildpack", + Name: "SomeBuildpack", + Version: "", + Description: "some description", + } + BuildPackConfig = dist.BuildpackDescriptor{ + WithInfo: moduleInfo, + WithAPI: platformAPIVersion, + WithTargets: append(targets, target), + } + ) + it("should return new #multiArchBuildpack pointer", func() { + cfg := buildpackage.NewMultiArchBuildpack(BuildPackConfig, "", false, false, targets) + h.AssertNotNil(t, cfg) + }) + when("#multiArchBuildpack", func() { + it("should return config targets", func() { + cfg := buildpackage.NewMultiArchBuildpack(BuildPackConfig, "", false, false, nil) + h.AssertNotNil(t, cfg) + h.AssertEq(t, cfg.Targets(), append(targets, target)) + }) + it("should return cli targets", func() { + cfg := buildpackage.NewMultiArchBuildpack(BuildPackConfig, "", false, false, targets) + h.AssertNotNil(t, cfg) + h.AssertEq(t, cfg.Targets(), targets) + }) + it("should return BuildpackConfigs", func() { + expectedConfigsLen := 9 + cfg := buildpackage.NewMultiArchBuildpack(BuildPackConfig, "", false, false, nil) + h.AssertNotNil(t, cfg) + + cfgs, err := cfg.MultiArchConfigs() + h.AssertNil(t, err) + h.AssertEq(t, len(cfgs), expectedConfigsLen) + }) + it("shouldhave expected multiArch configs", func() { + cfg := buildpackage.NewMultiArchBuildpack(BuildPackConfig, "", false, false, nil) + h.AssertNotNil(t, cfg) + + cfgs, err := cfg.MultiArchConfigs() + h.AssertNil(t, err) + h.AssertEq(t, len(cfgs) > 1, true) + + splitedTargets := splitTargets(cfg.Targets()) + h.AssertEq(t, cfgs[0].BuildpackDescriptor.WithInfo, moduleInfo) + h.AssertEq(t, cfgs[0].BuildpackDescriptor.WithAPI, platformAPIVersion) + h.AssertEq(t, cfgs[0].Targets()[0], splitedTargets[0]) + + h.AssertEq(t, cfgs[1].BuildpackDescriptor.WithInfo, moduleInfo) + h.AssertEq(t, cfgs[1].BuildpackDescriptor.WithAPI, platformAPIVersion) + h.AssertEq(t, cfgs[1].Targets()[0], splitedTargets[1]) + }) + }) + }) + when("#NewMultiArchExtension", func() { + var ( + platformAPIVersion = api.Platform.Latest() + moduleInfo = dist.ModuleInfo{ + ID: "some/buildpack", + Name: "SomeBuildpack", + Version: "", + Description: "some description", + } + ExtensionConfig = dist.ExtensionDescriptor{ + WithInfo: moduleInfo, + WithAPI: platformAPIVersion, + WithTargets: append(targets, target), + } + ) + it("should return new #multiArchExtension pointer", func() { + cfg := buildpackage.NewMultiArchExtension(ExtensionConfig, "", targets) + h.AssertNotNil(t, cfg) + }) + when("#multiArchExtension", func() { + it("should return config targets", func() { + cfg := buildpackage.NewMultiArchExtension(ExtensionConfig, "", nil) + h.AssertNotNil(t, cfg) + h.AssertEq(t, cfg.Targets(), append(targets, target)) + }) + it("should return cli targets", func() { + cfg := buildpackage.NewMultiArchExtension(ExtensionConfig, "", targets) + h.AssertNotNil(t, cfg) + h.AssertEq(t, cfg.Targets(), targets) + }) + it("should return ExtensionConfigs", func() { + expectedConfigsLen := 9 + cfg := buildpackage.NewMultiArchExtension(ExtensionConfig, "", nil) + h.AssertNotNil(t, cfg) + + cfgs, err := cfg.MultiArchConfigs() + h.AssertNil(t, err) + h.AssertEq(t, len(cfgs), expectedConfigsLen) + }) + it("should have expected multiArch configs", func() { + cfg := buildpackage.NewMultiArchExtension(ExtensionConfig, "", nil) + h.AssertNotNil(t, cfg) + + cfgs, err := cfg.MultiArchConfigs() + h.AssertNil(t, err) + h.AssertEq(t, len(cfgs) > 1, true) + + splitedTargets := splitTargets(cfg.Targets()) + h.AssertEq(t, len(cfgs), len(splitedTargets)) + h.AssertEq(t, cfgs[0].ExtensionDescriptor.WithInfo, moduleInfo) + h.AssertEq(t, cfgs[0].ExtensionDescriptor.WithAPI, platformAPIVersion) + h.AssertEq(t, cfgs[0].Targets()[0], splitedTargets[0]) + + h.AssertEq(t, cfgs[1].ExtensionDescriptor.WithInfo, moduleInfo) + h.AssertEq(t, cfgs[1].ExtensionDescriptor.WithAPI, platformAPIVersion) + h.AssertEq(t, cfgs[1].Targets()[0], splitedTargets[1]) + }) + }) + }) + when("#NewMultiArchPackage", func() { + it("should return a new #MultiArchPackage", func() { + cfg := buildpackage.NewMultiArchPackage(packageConfig, "./some-dir") + h.AssertNotNil(t, cfg) + h.AssertEq(t, cfg.Buildpack, packageConfig.Buildpack) + h.AssertEq(t, cfg.Dependencies, packageConfig.Dependencies) + h.AssertEq(t, cfg.Extension, packageConfig.Extension) + h.AssertEq(t, cfg.Platform, packageConfig.Platform) + }) + }) + when("#MultiArchBuildpackConfig", func() { + var ( + bpPath = "./someBPPath" + BPAPI = api.Buildpack.Latest() + ModuleInfo = dist.ModuleInfo{ + ID: "some/bp", + } + buildpackDescriptor = dist.BuildpackDescriptor{ + WithAPI: BPAPI, + WithInfo: ModuleInfo, + WithTargets: append(targets, target), + } + order = dist.Order{ + dist.OrderEntry{ + Group: []dist.ModuleRef{ + { + ModuleInfo: dist.ModuleInfo{ + ID: "some/bp1", + }, + }, + { + ModuleInfo: dist.ModuleInfo{ + ID: "some/bp2", + Version: "22.04", + }, + }, + }, + }, + } + ) + it("should return target.Spec.Path if specified", func() { + multiArchBP := buildpackage.NewMultiArchBuildpack(buildpackDescriptor, bpPath, false, false, append(targets, target)) + configs, err := multiArchBP.MultiArchConfigs() + h.AssertNil(t, err) + + h.AssertEq(t, configs[0].Path(), "some-path/buildpack.toml") + h.AssertEq(t, configs[1].Path(), "some-path/buildpack.toml") + }) + it("should return relativeDir when target.Spec.Path not specified", func() { + targets := append(targets, target) + for i, t := range targets { + t.Specs.Path = "" + targets[i] = t + } + + multiArchBP := buildpackage.NewMultiArchBuildpack(buildpackDescriptor, bpPath, false, false, targets) + configs, err := multiArchBP.MultiArchConfigs() + h.AssertNil(t, err) + + h.AssertEq(t, configs[0].Path(), configs[0].RelativeBaseDir()) + h.AssertEq(t, configs[1].Path(), configs[1].RelativeBaseDir()) + }) + it("should return BP Targets", func() { + multiArchBP := buildpackage.NewMultiArchBuildpack(buildpackDescriptor, bpPath, false, false, nil) + h.AssertEq(t, multiArchBP.Targets(), append(targets, target)) + }) + it("should return Flag Targets", func() { + multiArchBP := buildpackage.NewMultiArchBuildpack(buildpackDescriptor, bpPath, false, false, targets) + h.AssertEq(t, multiArchBP.Targets(), targets) + }) + it("should return flatten when explitly to true", func() { + multiArchBP := buildpackage.NewMultiArchBuildpack(buildpackDescriptor, bpPath, true, true, nil) + configs, err := multiArchBP.MultiArchConfigs() + h.AssertNil(t, err) + + h.AssertEq(t, configs[0].Flatten, true) + }) + it("should return false when explicitly flatten set to false", func() { + for i := range buildpackDescriptor.WithTargets { + buildpackDescriptor.WithTargets[i].Specs.Flatten = true + } + + multiArchBP := buildpackage.NewMultiArchBuildpack(buildpackDescriptor, bpPath, false, true, nil) + configs, err := multiArchBP.MultiArchConfigs() + h.AssertNil(t, err) + + h.AssertEq(t, configs[0].Flatten, false) + }) + it("should return config value(false) when not explicitly flatten value defined", func() { + for i := range buildpackDescriptor.WithTargets { + buildpackDescriptor.WithTargets[i].Specs.Flatten = false + } + + multiArchBP := buildpackage.NewMultiArchBuildpack(buildpackDescriptor, bpPath, false, false, nil) + configs, err := multiArchBP.MultiArchConfigs() + h.AssertNil(t, err) + + h.AssertEq(t, configs[0].Flatten, false) + h.AssertEq(t, configs[1].Flatten, false) + }) + it("should return config value(true) when not explicitly flatten value defined", func() { + for i := range buildpackDescriptor.WithTargets { + buildpackDescriptor.WithTargets[i].Specs.Flatten = true + } + + multiArchBP := buildpackage.NewMultiArchBuildpack(buildpackDescriptor, bpPath, false, false, nil) + configs, err := multiArchBP.MultiArchConfigs() + h.AssertNil(t, err) + + h.AssertEq(t, configs[0].Flatten, true) + h.AssertEq(t, configs[1].Flatten, true) + }) + it("should return expected len of config's multi arch configs", func() { + multiArchBP := buildpackage.NewMultiArchBuildpack(buildpackDescriptor, bpPath, false, false, nil) + configs, err := multiArchBP.MultiArchConfigs() + h.AssertNil(t, err) + + expectedTargets := splitTargets(append(targets, target)) + h.AssertEq(t, len(configs), len(expectedTargets)) + + h.AssertEq(t, configs[0].BuildpackDescriptor.WithTargets[0], expectedTargets[0]) + h.AssertEq(t, configs[1].WithTargets[0], expectedTargets[1]) + + h.AssertEq(t, configs[0].BuildpackDescriptor.WithAPI, buildpackDescriptor.WithAPI) + h.AssertEq(t, configs[0].BuildpackDescriptor.WithInfo, buildpackDescriptor.WithInfo) + h.AssertEq(t, configs[0].BuildpackDescriptor.WithLinuxBuild, buildpackDescriptor.WithLinuxBuild) + h.AssertEq(t, configs[0].BuildpackDescriptor.WithOrder, buildpackDescriptor.WithOrder) + h.AssertEq(t, configs[0].BuildpackDescriptor.WithStacks, buildpackDescriptor.WithStacks) + h.AssertEq(t, configs[0].BuildpackDescriptor.WithWindowsBuild, buildpackDescriptor.WithWindowsBuild) + + h.AssertEq(t, configs[0].Flatten, false) + }) + it("should return expected len of flag defined targets", func() { + multiArchBP := buildpackage.NewMultiArchBuildpack(buildpackDescriptor, bpPath, false, false, targets) + configs, err := multiArchBP.MultiArchConfigs() + h.AssertNil(t, err) + + expectedConfigs := splitTargets(targets) + h.AssertEq(t, len(configs), len(expectedConfigs)) + + h.AssertEq(t, configs[0].WithTargets[0], expectedConfigs[0]) + h.AssertEq(t, configs[1].WithTargets[0], expectedConfigs[1]) + + h.AssertEq(t, configs[0].BuildpackDescriptor.WithAPI, buildpackDescriptor.WithAPI) + h.AssertEq(t, configs[0].BuildpackDescriptor.WithInfo, buildpackDescriptor.WithInfo) + h.AssertEq(t, configs[0].BuildpackDescriptor.WithLinuxBuild, buildpackDescriptor.WithLinuxBuild) + h.AssertEq(t, configs[0].BuildpackDescriptor.WithOrder, buildpackDescriptor.WithOrder) + h.AssertEq(t, configs[0].BuildpackDescriptor.WithStacks, buildpackDescriptor.WithStacks) + h.AssertEq(t, configs[0].BuildpackDescriptor.WithWindowsBuild, buildpackDescriptor.WithWindowsBuild) + + h.AssertEq(t, configs[0].Flatten, false) + }) + when("#Type", func() { + it("should return Composite", func() { + buildpackDescriptor.WithOrder = order + multiArchBP := buildpackage.NewMultiArchBuildpack(buildpackDescriptor, bpPath, false, false, targets) + configs, err := multiArchBP.MultiArchConfigs() + h.AssertNil(t, err) + + h.AssertEq(t, configs[0].BuildpackType(), buildpackage.Composite) + h.AssertEq(t, configs[1].BuildpackType(), buildpackage.Composite) + }) + it("should return Buildpack", func() { + multiArchBP := buildpackage.NewMultiArchBuildpack(buildpackDescriptor, bpPath, false, false, targets) + configs, err := multiArchBP.MultiArchConfigs() + h.AssertNil(t, err) + + h.AssertEq(t, configs[0].BuildpackType(), buildpackage.Buildpack) + h.AssertEq(t, configs[1].BuildpackType(), buildpackage.Buildpack) + }) + }) + when("#CopyBuildpackToml", func() { + it("should copy buildpack.toml to expected path", func() { + multiArchBP := buildpackage.NewMultiArchBuildpack(buildpackDescriptor, bpPath, false, false, append(targets, target)) + configs, err := multiArchBP.MultiArchConfigs() + h.AssertNil(t, err) + + h.AssertNil(t, configs[0].CopyBuildpackToml(h.FakeIndexManifestBuilderFn(append(targets, target)))) + + bp1Target := configs[0].Targets()[0] + // platformRootBP1Dir := buildpack.PlatformRootDirectory(bp1Target, bp1Target.Distributions[0].Name, bp1Target.Distributions[0].Versions[0]) + // BP1buildpackToml := filepath.Join(bpPath, platformRootBP1Dir, BuildpackTOMLStr) + + _, err = os.Stat(configs[0].Path()) + h.AssertNil(t, err) + + config1 := &dist.BuildpackDescriptor{} + tomlMetaDataBP1, err := toml.DecodeFile(configs[0].Path(), config1) + h.AssertEq(t, len(tomlMetaDataBP1.Undecoded()), 0) + h.AssertEq(t, err, nil) + + expectedBP1Config := dist.BuildpackDescriptor{ + WithAPI: BPAPI, + WithTargets: []dist.Target{bp1Target}, + WithStacks: configs[0].WithStacks, + WithOrder: configs[0].WithOrder, + WithWindowsBuild: configs[0].WithWindowsBuild, + WithLinuxBuild: configs[0].WithLinuxBuild, + WithInfo: configs[0].WithInfo, + } + h.AssertEq(t, configs[0].BuildpackDescriptor, expectedBP1Config) + + h.AssertNil(t, configs[1].CopyBuildpackToml(h.FakeIndexManifestBuilderFn(append(targets, target)))) + + bp2Target := configs[1].Targets()[0] + _, err = os.Stat(configs[1].Path()) + h.AssertNil(t, err) + + config2 := &dist.BuildpackDescriptor{} + tomlMetaDataBP2, err := toml.DecodeFile(configs[1].Path(), config2) + h.AssertEq(t, len(tomlMetaDataBP2.Undecoded()), 0) + h.AssertEq(t, err, nil) + + expectedBP2Config := dist.BuildpackDescriptor{ + WithAPI: BPAPI, + WithTargets: []dist.Target{bp2Target}, + WithStacks: configs[1].WithStacks, + WithOrder: configs[0].WithOrder, + WithWindowsBuild: configs[1].WithWindowsBuild, + WithLinuxBuild: configs[1].WithLinuxBuild, + WithInfo: configs[1].WithInfo, + } + h.AssertEq(t, configs[1].BuildpackDescriptor, expectedBP2Config) + + h.AssertNil(t, os.Remove(configs[0].Path())) + }) + }) + it("should cleanBuildpackToml", func() { + var targets []dist.Target + for _, t := range append(targets, target) { + t.Specs.Path = "" + targets = append(targets, t) + } + + multiArchBP := buildpackage.NewMultiArchBuildpack(buildpackDescriptor, bpPath, false, false, targets) + configs, err := multiArchBP.MultiArchConfigs() + h.AssertNil(t, err) + + config1, config2 := configs[0], configs[1] + + h.AssertNil(t, config1.CopyBuildpackToml(h.FakeIndexManifestBuilderFn(targets))) + h.AssertNil(t, config2.CopyBuildpackToml(h.FakeIndexManifestBuilderFn(targets))) + + _, err = os.Stat(config1.Path()) + h.AssertNil(t, err) + + _, err = os.Stat(config2.Path()) + h.AssertNil(t, err) + + // should only remove config1 + h.AssertNil(t, config1.CleanBuildpackToml()) + + _, err = os.Stat(config1.Path()) + h.AssertNotNil(t, err) + + _, err = os.Stat(config2.Path()) + h.AssertNil(t, err) + + h.AssertNil(t, config2.CleanBuildpackToml()) + + _, err = os.Stat(config1.Path()) + h.AssertNotNil(t, err) + + _, err = os.Stat(config2.Path()) + h.AssertNotNil(t, err) + }) + }) + when("#MultiArchExtensionConfig", func() { + var ( + bpPath = "./someBPPath" + BPAPI = api.Buildpack.Latest() + ModuleInfo = dist.ModuleInfo{ + ID: "some/bp", + } + extensionDescriptor = dist.ExtensionDescriptor{ + WithAPI: BPAPI, + WithInfo: ModuleInfo, + WithTargets: append(targets, target), + } + ) + it("should return target.Spec.Path if specified", func() { + multiArchBP := buildpackage.NewMultiArchExtension(extensionDescriptor, bpPath, append(targets, target)) + configs, err := multiArchBP.MultiArchConfigs() + h.AssertNil(t, err) + + h.AssertEq(t, configs[0].Path(), "some-path/extension.toml") + h.AssertEq(t, configs[1].Path(), "some-path/extension.toml") + }) + it("should return relativeDir when target.Spec.Path not specified", func() { + targets := append(targets, target) + for i, t := range targets { + t.Specs.Path = "" + targets[i] = t + } + + multiArchBP := buildpackage.NewMultiArchExtension(extensionDescriptor, bpPath, targets) + configs, err := multiArchBP.MultiArchConfigs() + h.AssertNil(t, err) + + h.AssertEq(t, configs[0].Path(), configs[0].RelativeBaseDir()) + h.AssertEq(t, configs[1].Path(), configs[1].RelativeBaseDir()) + }) + it("should return BP Targets", func() { + multiArchBP := buildpackage.NewMultiArchExtension(extensionDescriptor, bpPath, nil) + h.AssertEq(t, multiArchBP.Targets(), append(targets, target)) + }) + it("should return Flag Targets", func() { + multiArchBP := buildpackage.NewMultiArchExtension(extensionDescriptor, bpPath, targets) + h.AssertEq(t, multiArchBP.Targets(), targets) + }) + it("should return expected len of config's multi arch configs", func() { + multiArchBP := buildpackage.NewMultiArchExtension(extensionDescriptor, bpPath, nil) + configs, err := multiArchBP.MultiArchConfigs() + h.AssertNil(t, err) + + expectedTargets := splitTargets(append(targets, target)) + h.AssertEq(t, len(configs), len(expectedTargets)) + + h.AssertEq(t, configs[0].ExtensionDescriptor.WithTargets[0], expectedTargets[0]) + h.AssertEq(t, configs[1].WithTargets[0], expectedTargets[1]) + + h.AssertEq(t, configs[0].ExtensionDescriptor.WithAPI, extensionDescriptor.WithAPI) + h.AssertEq(t, configs[0].ExtensionDescriptor.WithInfo, extensionDescriptor.WithInfo) + }) + it("should return expected len of flag defined targets", func() { + multiArchBP := buildpackage.NewMultiArchExtension(extensionDescriptor, bpPath, targets) + configs, err := multiArchBP.MultiArchConfigs() + h.AssertNil(t, err) + + expectedConfigs := splitTargets(targets) + h.AssertEq(t, len(configs), len(expectedConfigs)) + + h.AssertEq(t, configs[0].WithTargets[0], expectedConfigs[0]) + h.AssertEq(t, configs[1].WithTargets[0], expectedConfigs[1]) + + h.AssertEq(t, configs[0].ExtensionDescriptor.WithAPI, extensionDescriptor.WithAPI) + h.AssertEq(t, configs[0].ExtensionDescriptor.WithInfo, extensionDescriptor.WithInfo) + }) + when("#CopyExtensionToml", func() { + it("should copy extension.toml to expected path", func() { + multiArchBP := buildpackage.NewMultiArchExtension(extensionDescriptor, bpPath, append(targets, target)) + configs, err := multiArchBP.MultiArchConfigs() + h.AssertNil(t, err) + + h.AssertNil(t, configs[0].CopyExtensionToml(h.FakeIndexManifestBuilderFn(append(targets, target)))) + + bp1Target := configs[0].Targets()[0] + // platformRootBP1Dir := buildpack.PlatformRootDirectory(bp1Target, bp1Target.Distributions[0].Name, bp1Target.Distributions[0].Versions[0]) + // BP1buildpackToml := filepath.Join(bpPath, platformRootBP1Dir, BuildpackTOMLStr) + + _, err = os.Stat(configs[0].Path()) + h.AssertNil(t, err) + + config1 := &dist.ExtensionDescriptor{} + tomlMetaDataBP1, err := toml.DecodeFile(configs[0].Path(), config1) + h.AssertEq(t, len(tomlMetaDataBP1.Undecoded()), 0) + h.AssertEq(t, err, nil) + + expectedBP1Config := dist.ExtensionDescriptor{ + WithAPI: BPAPI, + WithTargets: []dist.Target{bp1Target}, + WithInfo: configs[0].WithInfo, + } + h.AssertEq(t, configs[0].ExtensionDescriptor, expectedBP1Config) + + h.AssertNil(t, configs[1].CopyExtensionToml(h.FakeIndexManifestBuilderFn(append(targets, target)))) + + bp2Target := configs[1].Targets()[0] + _, err = os.Stat(configs[1].Path()) + h.AssertNil(t, err) + + config2 := &dist.ExtensionDescriptor{} + tomlMetaDataBP2, err := toml.DecodeFile(configs[1].Path(), config2) + h.AssertEq(t, len(tomlMetaDataBP2.Undecoded()), 0) + h.AssertEq(t, err, nil) + + expectedBP2Config := dist.ExtensionDescriptor{ + WithAPI: BPAPI, + WithTargets: []dist.Target{bp2Target}, + WithInfo: configs[1].WithInfo, + } + h.AssertEq(t, configs[1].ExtensionDescriptor, expectedBP2Config) + h.AssertNil(t, os.Remove(configs[0].Path())) + }) + }) + it("should cleanBuildpackToml", func() { + var targets []dist.Target + for _, t := range append(targets, target) { + t.Specs.Path = "" + targets = append(targets, t) + } + + multiArchBP := buildpackage.NewMultiArchExtension(extensionDescriptor, bpPath, targets) + configs, err := multiArchBP.MultiArchConfigs() + h.AssertNil(t, err) + + config1, config2 := configs[0], configs[1] + + h.AssertNil(t, config1.CopyExtensionToml(h.FakeIndexManifestBuilderFn(targets))) + h.AssertNil(t, config2.CopyExtensionToml(h.FakeIndexManifestBuilderFn(targets))) + + _, err = os.Stat(config1.Path()) + h.AssertNil(t, err) + + _, err = os.Stat(config2.Path()) + h.AssertNil(t, err) + + // should only remove config1 + h.AssertNil(t, config1.CleanExtensionToml()) + + _, err = os.Stat(config1.Path()) + h.AssertNotNil(t, err) + + _, err = os.Stat(config2.Path()) + h.AssertNil(t, err) + + h.AssertNil(t, config2.CleanExtensionToml()) + + _, err = os.Stat(config1.Path()) + h.AssertNotNil(t, err) + + _, err = os.Stat(config2.Path()) + h.AssertNotNil(t, err) + }) + }) + when("#MultiArchPackage", func() { + it("should copy package descriptor to expected location", func() { + tmpDir, err := os.MkdirTemp("", "someCPPKGDir") + h.AssertNil(t, err) + + defer os.RemoveAll(bpPath) + defer os.RemoveAll(tmpDir) + + cfg := buildpackage.NewMultiArchPackage(packageConfig, tmpDir) + h.AssertNotNil(t, cfg) + + distro := target.Distributions[0] + h.AssertNil(t, cfg.CopyPackageToml(bpPath, target, distro.Name, distro.Versions[0], h.FakeIndexManifestBuilderFn([]dist.Target{target}))) + + platformRootDir := buildpack.PlatformRootDirectory(target, distro.Name, distro.Versions[0]) + + config := &buildpackage.Config{} + tomlMetaData, err := toml.DecodeFile(filepath.Join(bpPath, platformRootDir, "package.toml"), config) + h.AssertEq(t, len(tomlMetaData.Undecoded()), 0) + h.AssertEq(t, err, nil) + + path, err := filepath.Abs(tmpDir) + h.AssertNilE(t, err) + expectedPackageConfig := buildpackage.Config{ + Buildpack: dist.BuildpackURI{ + URI: "file://" + filepath.Join(path, platformRootDir), + }, + Platform: dist.Platform{OS: "linux"}, + Dependencies: []dist.ImageOrURI{ + { + BuildpackURI: dist.BuildpackURI{ + URI: "file://" + filepath.Join(path, platformRootDir), + }, + }, + { + BuildpackURI: dist.BuildpackURI{ + URI: "https://example.com/buildpack.tgz", + }, + }, + }, + } + + h.AssertEq(t, config.Buildpack.URI, expectedPackageConfig.Buildpack.URI) + h.AssertEq(t, config.Extension, expectedPackageConfig.Extension) + h.AssertEq(t, config.Platform, expectedPackageConfig.Platform) + for _, expDep := range expectedPackageConfig.Dependencies { + contains := false + for _, orgDep := range config.Dependencies { + if expDep == orgDep { + contains = true + break + } + } + h.AssertEq(t, contains, true) + } + }) + it("should cleanPackageToml", func() { + bpPath := "./SomePKGCleanPath" + tmpDir, err := os.MkdirTemp("", "someCleanPKGOtherDir") + h.AssertNil(t, err) + + cfg := buildpackage.NewMultiArchPackage(packageConfig, tmpDir) + h.AssertNotNil(t, cfg) + + distro := target.Distributions[0] + h.AssertNil(t, cfg.CopyPackageToml(bpPath, target, distro.Name, distro.Versions[0], h.FakeIndexManifestBuilderFn([]dist.Target{target}))) + + platformRootDir := buildpack.PlatformRootDirectory(target, distro.Name, distro.Versions[0]) + packageToml := filepath.Join(bpPath, platformRootDir, "package.toml") + + _, err = os.Stat(packageToml) + h.AssertNil(t, err) + + h.AssertNil(t, cfg.CleanPackageToml(bpPath, target, distro.Name, distro.Versions[0])) + + _, err = os.Stat(packageToml) + h.AssertNotNil(t, err) + }) + }) + when("#DigestFromIndex", func() { + var ( + idxMfest *v1.IndexManifest + ) + it.Before(func() { + fakeIndexManifestFn := h.FakeIndexManifestBuilderFn(append(targets, target)) + fakeTag, err := name.NewTag("cnbs/samples", name.Insecure, name.WeakValidation) + h.AssertNil(t, err) + + idxMfest, err = fakeIndexManifestFn(fakeTag) + h.AssertNil(t, err) + }) + it("should return an error when IndexManifest is Nil", func() { + hashStr, err := buildpackage.DigestFromIndex(nil, dist.Target{OS: "linux", Arch: "amd64"}) + h.AssertNotNil(t, err) + h.AssertEq(t, hashStr, "") + }) + it("should return an error when target not found in index", func() { + hashStr, err := buildpackage.DigestFromIndex(idxMfest, dist.Target{OS: "someNotFoundOS", Arch: "someNotFoundArch"}) + h.AssertNotNil(t, err) + h.AssertEq(t, hashStr, "") + }) + it("should return an error when target not found in index", func() { + hashStr, err := buildpackage.DigestFromIndex(idxMfest, dist.Target{OS: "linux", Arch: "amd64"}) + h.AssertNil(t, err) + h.AssertNotEq(t, hashStr, "") + }) + }) +} + +func splitTargets(targets []dist.Target) (out []dist.Target) { + for _, t := range targets { + t.Range(func(target dist.Target, distroName, distroVersion string) error { + target.Distributions = []dist.Distribution{ + { + Name: distroName, + Versions: []string{distroVersion}, + }, + } + out = append(out, target) + return nil + }) + } + + return out +} diff --git a/cmd/cmd.go b/cmd/cmd.go index 7ece55f1e1..4f829b19fa 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -106,6 +106,7 @@ func NewPackCommand(logger ConfigurableLogger) (*cobra.Command, error) { rootCmd.AddCommand(commands.SetDefaultRegistry(logger, cfg, cfgPath)) rootCmd.AddCommand(commands.RemoveRegistry(logger, cfg, cfgPath)) rootCmd.AddCommand(commands.YankBuildpack(logger, cfg, packClient)) + rootCmd.AddCommand(commands.NewManifestCommand(logger, packClient)) } packHome, err := config.PackHome() diff --git a/go.mod b/go.mod index 18ac4b6cb5..0169250c47 100644 --- a/go.mod +++ b/go.mod @@ -5,10 +5,10 @@ require ( github.com/Masterminds/semver v1.5.0 github.com/Microsoft/go-winio v0.6.1 github.com/apex/log v1.9.0 - github.com/buildpacks/imgutil v0.0.0-20240118145509-e94a1b7de8a9 + github.com/buildpacks/imgutil v0.0.0-20240206215312-f8d38e1de03d github.com/buildpacks/lifecycle v0.18.4 github.com/docker/cli v24.0.7+incompatible - github.com/docker/docker v24.0.7+incompatible + github.com/docker/docker v25.0.3+incompatible github.com/docker/go-connections v0.5.0 github.com/dustin/go-humanize v1.0.1 github.com/gdamore/tcell/v2 v2.7.0 @@ -33,7 +33,7 @@ require ( golang.org/x/mod v0.15.0 golang.org/x/oauth2 v0.17.0 golang.org/x/sync v0.6.0 - golang.org/x/sys v0.17.0 + golang.org/x/sys v0.18.0 golang.org/x/term v0.17.0 golang.org/x/text v0.14.0 gopkg.in/yaml.v3 v3.0.1 @@ -53,26 +53,27 @@ require ( github.com/Azure/go-autorest/tracing v0.6.0 // indirect github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 // indirect github.com/agext/levenshtein v1.2.3 // indirect - github.com/aws/aws-sdk-go-v2 v1.21.2 // indirect - github.com/aws/aws-sdk-go-v2/config v1.19.0 // indirect - github.com/aws/aws-sdk-go-v2/credentials v1.13.43 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.13 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.43 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.37 // indirect - github.com/aws/aws-sdk-go-v2/internal/ini v1.3.45 // indirect - github.com/aws/aws-sdk-go-v2/service/ecr v1.20.2 // indirect - github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.18.2 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.37 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.15.2 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.17.3 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.23.2 // indirect - github.com/aws/smithy-go v1.15.0 // indirect - github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.0.0-20231003182221-725682229e60 // indirect + github.com/aws/aws-sdk-go-v2 v1.25.2 // indirect + github.com/aws/aws-sdk-go-v2/config v1.27.4 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.17.4 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.15.2 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.2 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.2 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect + github.com/aws/aws-sdk-go-v2/service/ecr v1.24.5 // indirect + github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.21.5 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.1 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.2 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.20.1 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.1 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.28.1 // indirect + github.com/aws/smithy-go v1.20.1 // indirect + github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.0.0-20231213181459-b0fcec718dc6 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/chrismellard/docker-credential-acr-env v0.0.0-20230304212654-82a0ddb27589 // indirect github.com/cloudflare/circl v1.3.7 // indirect - github.com/containerd/containerd v1.7.7 // indirect + github.com/containerd/containerd v1.7.13 // indirect github.com/containerd/log v0.1.0 // indirect github.com/containerd/stargz-snapshotter/estargz v0.14.3 // indirect github.com/containerd/typeurl v1.0.2 // indirect @@ -84,33 +85,36 @@ require ( github.com/docker/go-metrics v0.0.1 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/emirpasic/gods v1.18.1 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect github.com/gdamore/encoding v1.0.0 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/go-billy/v5 v5.5.0 // indirect + github.com/go-logr/logr v1.4.1 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt/v4 v4.5.0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/google/go-querystring v1.1.0 // indirect - github.com/google/uuid v1.4.0 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/gorilla/mux v1.8.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/kevinburke/ssh_config v1.2.0 // indirect - github.com/klauspost/compress v1.17.1 // indirect + github.com/klauspost/compress v1.17.2 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect - github.com/moby/buildkit v0.12.2 // indirect + github.com/moby/buildkit v0.12.5 // indirect github.com/moby/patternmatcher v0.6.0 // indirect github.com/moby/sys/sequential v0.5.0 // indirect + github.com/moby/sys/user v0.1.0 // indirect github.com/moby/term v0.5.0 // indirect github.com/morikuni/aec v1.0.0 // indirect - github.com/opencontainers/runc v1.1.9 // indirect github.com/opencontainers/selinux v1.11.0 // indirect github.com/pjbgf/sha1cd v0.3.0 // indirect github.com/prometheus/client_golang v1.17.0 // indirect @@ -124,15 +128,25 @@ require ( github.com/spf13/pflag v1.0.5 // indirect github.com/vbatts/tar-split v0.11.5 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.48.0 // indirect + go.opentelemetry.io/otel v1.23.0 // indirect + go.opentelemetry.io/otel/metric v1.23.0 // indirect + go.opentelemetry.io/otel/trace v1.23.0 // indirect golang.org/x/net v0.21.0 // indirect - golang.org/x/tools v0.16.1 // indirect + golang.org/x/tools v0.17.0 // indirect google.golang.org/appengine v1.6.8 // indirect - google.golang.org/protobuf v1.31.0 // indirect + google.golang.org/protobuf v1.32.0 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gotest.tools/v3 v3.4.0 // indirect ) go 1.21 -// Pin moby/buildkit until docker/docker is upgraded -replace github.com/moby/buildkit => github.com/moby/buildkit v0.11.6 +replace ( + github.com/buildpacks/imgutil => github.com/husni-faiz/imgutil v0.0.0-20240403093154-59056e113edb + + github.com/buildpacks/lifecycle => github.com/WYGIN/buildpacks-lifecycle v0.0.0-20240322102900-d5cc232ef875 + + // Pin moby/buildkit until docker/docker is upgraded + github.com/moby/buildkit => github.com/moby/buildkit v0.11.6 +) diff --git a/go.sum b/go.sum index bd8463bfeb..c3f4866aba 100644 --- a/go.sum +++ b/go.sum @@ -40,6 +40,8 @@ github.com/Microsoft/hcsshim v0.11.4 h1:68vKo2VN8DE9AdN4tnkWnmdhqdbpUFM8OF3Airm7 github.com/Microsoft/hcsshim v0.11.4/go.mod h1:smjE4dvqPX9Zldna+t5FG3rnoHhaB7QYxPRqGcpAD9w= github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 h1:kkhsdkhsCvIsutKu5zLMgWtgh9YxGCNAw8Ad8hjwfYg= github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= +github.com/WYGIN/buildpacks-lifecycle v0.0.0-20240322102900-d5cc232ef875 h1:AxOHfLethb8D2U96sGMhetQDj0GhE1++X7InuyaSjDU= +github.com/WYGIN/buildpacks-lifecycle v0.0.0-20240322102900-d5cc232ef875/go.mod h1:QP6XcTQfGSxsuKBwlZLKQiQk07wEN5GSkeZ5I8WbrNs= github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo= github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= @@ -54,46 +56,46 @@ github.com/aphistic/sweet v0.2.0/go.mod h1:fWDlIh/isSE9n6EPsRmC0det+whmX6dJid3st github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/aws/aws-sdk-go v1.20.6/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= -github.com/aws/aws-sdk-go-v2 v1.21.2 h1:+LXZ0sgo8quN9UOKXXzAWRT3FWd4NxeXWOZom9pE7GA= -github.com/aws/aws-sdk-go-v2 v1.21.2/go.mod h1:ErQhvNuEMhJjweavOYhxVkn2RUx7kQXVATHrjKtxIpM= -github.com/aws/aws-sdk-go-v2/config v1.19.0 h1:AdzDvwH6dWuVARCl3RTLGRc4Ogy+N7yLFxVxXe1ClQ0= -github.com/aws/aws-sdk-go-v2/config v1.19.0/go.mod h1:ZwDUgFnQgsazQTnWfeLWk5GjeqTQTL8lMkoE1UXzxdE= -github.com/aws/aws-sdk-go-v2/credentials v1.13.43 h1:LU8vo40zBlo3R7bAvBVy/ku4nxGEyZe9N8MqAeFTzF8= -github.com/aws/aws-sdk-go-v2/credentials v1.13.43/go.mod h1:zWJBz1Yf1ZtX5NGax9ZdNjhhI4rgjfgsyk6vTY1yfVg= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.13 h1:PIktER+hwIG286DqXyvVENjgLTAwGgoeriLDD5C+YlQ= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.13/go.mod h1:f/Ib/qYjhV2/qdsf79H3QP/eRE4AkVyEf6sk7XfZ1tg= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.43 h1:nFBQlGtkbPzp/NjZLuFxRqmT91rLJkgvsEQs68h962Y= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.43/go.mod h1:auo+PiyLl0n1l8A0e8RIeR8tOzYPfZZH/JNlrJ8igTQ= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.37 h1:JRVhO25+r3ar2mKGP7E0LDl8K9/G36gjlqca5iQbaqc= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.37/go.mod h1:Qe+2KtKml+FEsQF/DHmDV+xjtche/hwoF75EG4UlHW8= -github.com/aws/aws-sdk-go-v2/internal/ini v1.3.45 h1:hze8YsjSh8Wl1rYa1CJpRmXP21BvOBuc76YhW0HsuQ4= -github.com/aws/aws-sdk-go-v2/internal/ini v1.3.45/go.mod h1:lD5M20o09/LCuQ2mE62Mb/iSdSlCNuj6H5ci7tW7OsE= -github.com/aws/aws-sdk-go-v2/service/ecr v1.20.2 h1:y6LX9GUoEA3mO0qpFl1ZQHj1rFyPWVphlzebiSt2tKE= -github.com/aws/aws-sdk-go-v2/service/ecr v1.20.2/go.mod h1:Q0LcmaN/Qr8+4aSBrdrXXePqoX0eOuYpJLbYpilmWnA= -github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.18.2 h1:PpbXaecV3sLAS6rjQiaKw4/jyq3Z8gNzmoJupHAoBp0= -github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.18.2/go.mod h1:fUHpGXr4DrXkEDpGAjClPsviWf+Bszeb0daKE0blxv8= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.37 h1:WWZA/I2K4ptBS1kg0kV1JbBtG/umed0vwHRrmcr9z7k= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.37/go.mod h1:vBmDnwWXWxNPFRMmG2m/3MKOe+xEcMDo1tanpaWCcck= -github.com/aws/aws-sdk-go-v2/service/sso v1.15.2 h1:JuPGc7IkOP4AaqcZSIcyqLpFSqBWK32rM9+a1g6u73k= -github.com/aws/aws-sdk-go-v2/service/sso v1.15.2/go.mod h1:gsL4keucRCgW+xA85ALBpRFfdSLH4kHOVSnLMSuBECo= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.17.3 h1:HFiiRkf1SdaAmV3/BHOFZ9DjFynPHj8G/UIO1lQS+fk= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.17.3/go.mod h1:a7bHA82fyUXOm+ZSWKU6PIoBxrjSprdLoM8xPYvzYVg= -github.com/aws/aws-sdk-go-v2/service/sts v1.23.2 h1:0BkLfgeDjfZnZ+MhB3ONb01u9pwFYTCZVhlsSSBvlbU= -github.com/aws/aws-sdk-go-v2/service/sts v1.23.2/go.mod h1:Eows6e1uQEsc4ZaHANmsPRzAKcVDrcmjjWiih2+HUUQ= -github.com/aws/smithy-go v1.15.0 h1:PS/durmlzvAFpQHDs4wi4sNNP9ExsqZh6IlfdHXgKK8= -github.com/aws/smithy-go v1.15.0/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA= -github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.0.0-20231003182221-725682229e60 h1:ONd54l3oubhjMPcj7HpjPWvlFI6WXsu0/W7DsKCPI9w= -github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.0.0-20231003182221-725682229e60/go.mod h1:eSn65Noe23f/Z7A2ESqw3dbhAFSEyzZf38nXcKVNxtE= +github.com/aws/aws-sdk-go-v2 v1.25.2 h1:/uiG1avJRgLGiQM9X3qJM8+Qa6KRGK5rRPuXE0HUM+w= +github.com/aws/aws-sdk-go-v2 v1.25.2/go.mod h1:Evoc5AsmtveRt1komDwIsjHFyrP5tDuF1D1U+6z6pNo= +github.com/aws/aws-sdk-go-v2/config v1.27.4 h1:AhfWb5ZwimdsYTgP7Od8E9L1u4sKmDW2ZVeLcf2O42M= +github.com/aws/aws-sdk-go-v2/config v1.27.4/go.mod h1:zq2FFXK3A416kiukwpsd+rD4ny6JC7QSkp4QdN1Mp2g= +github.com/aws/aws-sdk-go-v2/credentials v1.17.4 h1:h5Vztbd8qLppiPwX+y0Q6WiwMZgpd9keKe2EAENgAuI= +github.com/aws/aws-sdk-go-v2/credentials v1.17.4/go.mod h1:+30tpwrkOgvkJL1rUZuRLoxcJwtI/OkeBLYnHxJtVe0= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.15.2 h1:AK0J8iYBFeUk2Ax7O8YpLtFsfhdOByh2QIkHmigpRYk= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.15.2/go.mod h1:iRlGzMix0SExQEviAyptRWRGdYNo3+ufW/lCzvKVTUc= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.2 h1:bNo4LagzUKbjdxE0tIcR9pMzLR2U/Tgie1Hq1HQ3iH8= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.2/go.mod h1:wRQv0nN6v9wDXuWThpovGQjqF1HFdcgWjporw14lS8k= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.2 h1:EtOU5jsPdIQNP+6Q2C5e3d65NKT1PeCiQk+9OdzO12Q= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.2/go.mod h1:tyF5sKccmDz0Bv4NrstEr+/9YkSPJHrcO7UsUKf7pWM= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY= +github.com/aws/aws-sdk-go-v2/service/ecr v1.24.5 h1:wLPDAUFT50NEXGXpywRU3AA74pg35RJjWol/68ruvQQ= +github.com/aws/aws-sdk-go-v2/service/ecr v1.24.5/go.mod h1:AOHmGMoPtSY9Zm2zBuwUJQBisIvYAZeA1n7b6f4e880= +github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.21.5 h1:PQp21GBlGNaQ+AVJAB8w2KTmLx0DkFS2fDET2Iy3+f0= +github.com/aws/aws-sdk-go-v2/service/ecrpublic v1.21.5/go.mod h1:WMntdAol8KgeYsa5sDZPsRTXs4jVZIMYu0eQVVIQxnc= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.1 h1:EyBZibRTVAs6ECHZOw5/wlylS9OcTzwyjeQMudmREjE= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.1/go.mod h1:JKpmtYhhPs7D97NL/ltqz7yCkERFW5dOlHyVl66ZYF8= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.2 h1:5ffmXjPtwRExp1zc7gENLgCPyHFbhEPwVTkTiH9niSk= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.2/go.mod h1:Ru7vg1iQ7cR4i7SZ/JTLYN9kaXtbL69UdgG0OQWQxW0= +github.com/aws/aws-sdk-go-v2/service/sso v1.20.1 h1:utEGkfdQ4L6YW/ietH7111ZYglLJvS+sLriHJ1NBJEQ= +github.com/aws/aws-sdk-go-v2/service/sso v1.20.1/go.mod h1:RsYqzYr2F2oPDdpy+PdhephuZxTfjHQe7SOBcZGoAU8= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.1 h1:9/GylMS45hGGFCcMrUZDVayQE1jYSIN6da9jo7RAYIw= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.1/go.mod h1:YjAPFn4kGFqKC54VsHs5fn5B6d+PCY2tziEa3U/GB5Y= +github.com/aws/aws-sdk-go-v2/service/sts v1.28.1 h1:3I2cBEYgKhrWlwyZgfpSO2BpaMY1LHPqXYk/QGlu2ew= +github.com/aws/aws-sdk-go-v2/service/sts v1.28.1/go.mod h1:uQ7YYKZt3adCRrdCBREm1CD3efFLOUNH77MrUCvx5oA= +github.com/aws/smithy-go v1.20.1 h1:4SZlSlMr36UEqC7XOyRVb27XMeZubNcBNN+9IgEPIQw= +github.com/aws/smithy-go v1.20.1/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E= +github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.0.0-20231213181459-b0fcec718dc6 h1:PlJRmqKlSlEUlwem1c3zdPaEMtJc/ktnV7naD5Qvsx4= +github.com/awslabs/amazon-ecr-credential-helper/ecr-login v0.0.0-20231213181459-b0fcec718dc6/go.mod h1:08sPJIlDHu4HwQ1xScPgsBWezvM6U10ghGKBJu0mowA= github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59/go.mod h1:q/89r3U2H7sSsE2t6Kca0lfwTK8JdoNGS/yzM/4iH5I= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/buildpacks/imgutil v0.0.0-20240118145509-e94a1b7de8a9 h1:kxe31xfMWJAIAzDfGQ3lL0j8QSSRfEHyLg7dRWIHA8I= -github.com/buildpacks/imgutil v0.0.0-20240118145509-e94a1b7de8a9/go.mod h1:PsazEB9yz+NG/cgm0Z1oQ0Xq6rD/U7eNMt5Su41afYY= -github.com/buildpacks/lifecycle v0.18.4 h1:LGl/4guzU+57hn08W8RwjLLizYtuNfCZHtxn8TP2+bE= -github.com/buildpacks/lifecycle v0.18.4/go.mod h1:DxxfyFaCi9ovbbP2fhcKBlImfbTPiPEtM5UqSlD1TJ8= github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= +github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= +github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chrismellard/docker-credential-acr-env v0.0.0-20230304212654-82a0ddb27589 h1:krfRl01rzPzxSxyLyrChD+U+MzsBXbm0OwYYB67uF+4= @@ -101,8 +103,8 @@ github.com/chrismellard/docker-credential-acr-env v0.0.0-20230304212654-82a0ddb2 github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= -github.com/containerd/containerd v1.7.7 h1:QOC2K4A42RQpcrZyptP6z9EJZnlHfHJUfZrAAHe15q4= -github.com/containerd/containerd v1.7.7/go.mod h1:3c4XZv6VeT9qgf9GMTxNTMFxGJrGpI2vz1yk4ye+YY8= +github.com/containerd/containerd v1.7.13 h1:wPYKIeGMN8vaggSKuV1X0wZulpMz4CrgEsZdaCyB6Is= +github.com/containerd/containerd v1.7.13/go.mod h1:zT3up6yTRfEUa6+GsITYIJNgSVL9NQ4x4h1RPzk0Wu4= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/containerd/stargz-snapshotter/estargz v0.14.3 h1:OqlDCK3ZVUO6C3B/5FSkDwbkEETK84kQgEeFwDC+62k= @@ -125,8 +127,8 @@ github.com/docker/cli v24.0.7+incompatible h1:wa/nIwYFW7BVTGa7SWPVyyXU9lgORqUb1x github.com/docker/cli v24.0.7+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/docker v24.0.7+incompatible h1:Wo6l37AuwP3JaMnZa226lzVXGA3F9Ig1seQen0cKYlM= -github.com/docker/docker v24.0.7+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v25.0.3+incompatible h1:D5fy/lYmY7bvZa0XTZ5/UJPljor41F+vdyJG5luQLfQ= +github.com/docker/docker v25.0.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker-credential-helpers v0.8.0 h1:YQFtbBQb4VrpoPxhFuzEBPQ9E16qz5SpHLS+uswaCp8= github.com/docker/docker-credential-helpers v0.8.0/go.mod h1:UGFXcuoQ5TxPiB54nHOZ32AWRqQdECoh/Mg0AlEYb40= github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= @@ -144,6 +146,8 @@ github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/El github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= @@ -163,8 +167,11 @@ github.com/go-git/go-git/v5 v5.11.0/go.mod h1:6GFcX2P3NM7FPBfpePbpLd21XxsgdAt+lK github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= -github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY= -github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= @@ -189,7 +196,6 @@ github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiu github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-containerregistry v0.19.0 h1:uIsMRBV7m/HDkDxE/nXMnv1q+lOOSPlQ/ywc5JbB8Ic= @@ -203,15 +209,20 @@ github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/ github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE= github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= -github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg= github.com/hectane/go-acl v0.0.0-20190604041725-da78bae5fc95 h1:S4qyfL2sEm5Budr4KVMyEniCy+PbS55651I/a+Kn/NQ= github.com/hectane/go-acl v0.0.0-20190604041725-da78bae5fc95/go.mod h1:QiyDdbZLaJ/mZP4Zwc9g2QsfaEA4o7XvvgZegSci5/E= github.com/heroku/color v0.0.6 h1:UTFFMrmMLFcL3OweqP1lAdp8i1y/9oHqkeHjQ/b/Ny0= github.com/heroku/color v0.0.6/go.mod h1:ZBvOcx7cTF2QKOv4LbmoBtNl5uB17qWxGuzZrsi1wLU= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/husni-faiz/imgutil v0.0.0-20240403093154-59056e113edb h1:ZOem0g9dhXj3jkDmJCVCvAItqrF9Kg+RpjbCSZxc2/E= +github.com/husni-faiz/imgutil v0.0.0-20240403093154-59056e113edb/go.mod h1:7zUmt4wkVJNuXCZhQndEd3kvVGmWLVyzRFIQXTaeXlU= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= @@ -229,8 +240,8 @@ github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4 github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.17.1 h1:NE3C767s2ak2bweCZo3+rdP4U/HoyVXLv/X9f2gPS5g= -github.com/klauspost/compress v1.17.1/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/klauspost/compress v1.17.2 h1:RlWWUY/Dr4fL8qk9YG7DTZ7PDgME2V4csBXA8L/ixi4= +github.com/klauspost/compress v1.17.2/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -269,6 +280,8 @@ github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkV github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc= github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo= +github.com/moby/sys/user v0.1.0 h1:WmZ93f5Ux6het5iituh9x2zAG7NFY9Aqi49jjE1PaQg= +github.com/moby/sys/user v0.1.0/go.mod h1:fKJhFOnsCN6xZ5gSfbM6zaHGgDJMrqt9/reuj4T7MmU= github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -289,8 +302,6 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.0-rc5 h1:Ygwkfw9bpDvs+c9E34SdgGOj41dX/cbdlwvlWt0pnFI= github.com/opencontainers/image-spec v1.1.0-rc5/go.mod h1:X4pATf0uXsnn3g5aiGIsVnJBR4mxhKzfwmvK/B2NTm8= -github.com/opencontainers/runc v1.1.9 h1:XR0VIHTGce5eWPkaPesqTBrhW2yAcaraWfsEalNwQLM= -github.com/opencontainers/runc v1.1.9/go.mod h1:CbUumNnWCuTGFukNXahoo/RFBZvDAgRh/smNYNOhA50= github.com/opencontainers/selinux v1.11.0 h1:+5Zbo97w3Lbmb3PeqQtpmTkMwsW5nRI3YaLpt7tQ7oU= github.com/opencontainers/selinux v1.11.0/go.mod h1:E5dMC3VPuVvVHDYmi78qvhJp8+M586T4DlDRYpFkyec= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= @@ -379,6 +390,22 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.48.0 h1:doUP+ExOpH3spVTLS0FcWGLnQrPct/hD/bCPbDRUEAU= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.48.0/go.mod h1:rdENBZMT2OE6Ne/KLwpiXudnAsbdrdBaqBvTN8M8BgA= +go.opentelemetry.io/otel v1.23.0 h1:Df0pqjqExIywbMCMTxkAwzjLZtRf+bBKLbUcpxO2C9E= +go.opentelemetry.io/otel v1.23.0/go.mod h1:YCycw9ZeKhcJFrb34iVSkyT0iczq/zYDtZYFufObyB0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU= +go.opentelemetry.io/otel/metric v1.23.0 h1:pazkx7ss4LFVVYSxYew7L5I6qvLXHA0Ap2pwV+9Cnpo= +go.opentelemetry.io/otel/metric v1.23.0/go.mod h1:MqUW2X2a6Q8RN96E2/nqNoT+z9BSms20Jb7Bbp+HiTo= +go.opentelemetry.io/otel/sdk v1.19.0 h1:6USY6zH+L8uMH8L3t1enZPR3WFEmSTADlqldyHtJi3o= +go.opentelemetry.io/otel/sdk v1.19.0/go.mod h1:NedEbbS4w3C6zElbLdPJKOpJQOrGUJ+GfzpjUvI0v1A= +go.opentelemetry.io/otel/trace v1.23.0 h1:37Ik5Ib7xfYVb4V1UtnT97T1jI+AoIYkJyPkuL4iJgI= +go.opentelemetry.io/otel/trace v1.23.0/go.mod h1:GSGTbIClEsuZrGIzoEHqsVfxgn5UkggkflQwDScNUsk= +go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= +go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -459,8 +486,8 @@ golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= -golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -482,8 +509,8 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= -golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= @@ -492,8 +519,8 @@ golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.16.1 h1:TLyB3WofjdOEepBHAU20JdNC1Zbg87elYofWYAY5oZA= -golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0= +golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc= +golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -501,10 +528,17 @@ golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8T google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= +google.golang.org/genproto v0.0.0-20240205150955-31a09d347014 h1:g/4bk7P6TPMkAUbUhquq98xey1slwvuVJPosdBqYJlU= +google.golang.org/genproto/googleapis/api v0.0.0-20230711160842-782d3b101e98 h1:FmF5cCW94Ij59cfpoLiwTgodWmm60eEV0CjlsVg2fuw= +google.golang.org/genproto/googleapis/api v0.0.0-20230711160842-782d3b101e98/go.mod h1:rsr7RhLuwsDKL7RmgDDCUc6yaGr1iqceVb5Wv6f6YvQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240213162025-012b6fc9bca9 h1:hZB7eLIaYlW9qXRfCq/qDaPdbeY3757uARz5Vvfv+cY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240213162025-012b6fc9bca9/go.mod h1:YUWgXUFRPfoYK1IHMuxH5K6nPEXSCzIMljnQ59lLRCk= +google.golang.org/grpc v1.61.1 h1:kLAiWrZs7YeDM6MumDe7m3y4aM6wacLzM1Y/wiLP9XY= +google.golang.org/grpc v1.61.1/go.mod h1:VUbo7IFqmF1QtCAstipjG0GIoq49KvMe9+h1jFLBNJs= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= -google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= +google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/internal/build/container_ops.go b/internal/build/container_ops.go index bab2efd328..d594318acf 100644 --- a/internal/build/container_ops.go +++ b/internal/build/container_ops.go @@ -12,12 +12,13 @@ import ( "github.com/buildpacks/lifecycle/platform/files" "github.com/docker/docker/api/types" dcontainer "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/network" "github.com/docker/docker/errdefs" darchive "github.com/docker/docker/pkg/archive" "github.com/pkg/errors" "github.com/buildpacks/pack/internal/builder" - "github.com/buildpacks/pack/internal/container" + pcontainer "github.com/buildpacks/pack/internal/container" "github.com/buildpacks/pack/internal/paths" "github.com/buildpacks/pack/pkg/archive" ) @@ -175,23 +176,23 @@ func copyDirWindows(ctx context.Context, ctrClient DockerClient, containerID str Binds: []string{fmt.Sprintf("%s:%s", mnt.Name, mnt.Destination)}, Isolation: dcontainer.IsolationProcess, }, - nil, nil, "", + &network.NetworkingConfig{EndpointsConfig: make(map[string]*network.EndpointSettings)}, nil, "", ) if err != nil { return errors.Wrapf(err, "creating prep container") } - defer ctrClient.ContainerRemove(context.Background(), ctr.ID, types.ContainerRemoveOptions{Force: true}) + defer ctrClient.ContainerRemove(context.Background(), ctr.ID, dcontainer.RemoveOptions{Force: true}) err = ctrClient.CopyToContainer(ctx, ctr.ID, "/windows", reader, types.CopyToContainerOptions{}) if err != nil { return errors.Wrap(err, "copy app to container") } - return container.RunWithHandler( + return pcontainer.RunWithHandler( ctx, ctrClient, ctr.ID, - container.DefaultHandler( + pcontainer.DefaultHandler( io.Discard, // Suppress xcopy output stderr, ), @@ -327,13 +328,13 @@ func EnsureVolumeAccess(uid, gid int, os string, volumeNames ...string) Containe if err != nil { return err } - defer ctrClient.ContainerRemove(context.Background(), ctr.ID, types.ContainerRemoveOptions{Force: true}) + defer ctrClient.ContainerRemove(context.Background(), ctr.ID, dcontainer.RemoveOptions{Force: true}) - return container.RunWithHandler( + return pcontainer.RunWithHandler( ctx, ctrClient, ctr.ID, - container.DefaultHandler( + pcontainer.DefaultHandler( io.Discard, // Suppress icacls output stderr, ), diff --git a/internal/build/container_ops_test.go b/internal/build/container_ops_test.go index 2814374208..463c97367f 100644 --- a/internal/build/container_ops_test.go +++ b/internal/build/container_ops_test.go @@ -13,7 +13,6 @@ import ( "testing" "github.com/buildpacks/lifecycle/platform/files" - "github.com/docker/docker/api/types" dcontainer "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/mount" "github.com/docker/docker/client" @@ -468,7 +467,7 @@ drwxrwxrwx 2 123 456 (.*) some-vol h.AssertNil(t, err) defer cleanupContainer(ctx, ctr.ID) - writeOp := build.WriteRunToml(containerPath, []builder.RunImageMetadata{builder.RunImageMetadata{ + writeOp := build.WriteRunToml(containerPath, []builder.RunImageMetadata{{ Image: "image-1", Mirrors: []string{ "mirror-1", @@ -709,7 +708,7 @@ func cleanupContainer(ctx context.Context, ctrID string) { } // remove container - err = ctrClient.ContainerRemove(ctx, ctrID, types.ContainerRemoveOptions{}) + err = ctrClient.ContainerRemove(ctx, ctrID, dcontainer.RemoveOptions{}) if err != nil { return } diff --git a/internal/build/docker.go b/internal/build/docker.go index 58a53f343d..a44852dad7 100644 --- a/internal/build/docker.go +++ b/internal/build/docker.go @@ -6,19 +6,20 @@ import ( "github.com/docker/docker/api/types" containertypes "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/image" networktypes "github.com/docker/docker/api/types/network" specs "github.com/opencontainers/image-spec/specs-go/v1" ) type DockerClient interface { - ImageRemove(ctx context.Context, image string, options types.ImageRemoveOptions) ([]types.ImageDeleteResponseItem, error) + ImageRemove(ctx context.Context, image string, options types.ImageRemoveOptions) ([]image.DeleteResponse, error) VolumeRemove(ctx context.Context, volumeID string, force bool) error ContainerWait(ctx context.Context, container string, condition containertypes.WaitCondition) (<-chan containertypes.WaitResponse, <-chan error) - ContainerAttach(ctx context.Context, container string, options types.ContainerAttachOptions) (types.HijackedResponse, error) - ContainerStart(ctx context.Context, container string, options types.ContainerStartOptions) error + ContainerAttach(ctx context.Context, container string, options containertypes.AttachOptions) (types.HijackedResponse, error) + ContainerStart(ctx context.Context, container string, options containertypes.StartOptions) error ContainerCreate(ctx context.Context, config *containertypes.Config, hostConfig *containertypes.HostConfig, networkingConfig *networktypes.NetworkingConfig, platform *specs.Platform, containerName string) (containertypes.CreateResponse, error) CopyFromContainer(ctx context.Context, container, srcPath string) (io.ReadCloser, types.ContainerPathStat, error) ContainerInspect(ctx context.Context, container string) (types.ContainerJSON, error) - ContainerRemove(ctx context.Context, container string, options types.ContainerRemoveOptions) error + ContainerRemove(ctx context.Context, container string, options containertypes.RemoveOptions) error CopyToContainer(ctx context.Context, container, path string, content io.Reader, options types.CopyToContainerOptions) error } diff --git a/internal/build/phase.go b/internal/build/phase.go index 76c4bdfea6..80727e2942 100644 --- a/internal/build/phase.go +++ b/internal/build/phase.go @@ -4,8 +4,8 @@ import ( "context" "io" - "github.com/docker/docker/api/types" dcontainer "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/network" "github.com/pkg/errors" "github.com/buildpacks/pack/internal/container" @@ -18,6 +18,7 @@ type Phase struct { docker DockerClient handler container.Handler ctrConf *dcontainer.Config + endpoint *network.EndpointSettings hostConf *dcontainer.HostConfig ctr dcontainer.CreateResponse uid, gid int @@ -29,7 +30,7 @@ type Phase struct { func (p *Phase) Run(ctx context.Context) error { var err error - p.ctr, err = p.docker.ContainerCreate(ctx, p.ctrConf, p.hostConf, nil, nil, "") + p.ctr, err = p.docker.ContainerCreate(ctx, p.ctrConf, p.hostConf, &network.NetworkingConfig{EndpointsConfig: make(map[string]*network.EndpointSettings)}, nil, "") if err != nil { return errors.Wrapf(err, "failed to create '%s' container", p.name) } @@ -64,5 +65,5 @@ func (p *Phase) Run(ctx context.Context) error { } func (p *Phase) Cleanup() error { - return p.docker.ContainerRemove(context.Background(), p.ctr.ID, types.ContainerRemoveOptions{Force: true}) + return p.docker.ContainerRemove(context.Background(), p.ctr.ID, dcontainer.RemoveOptions{Force: true}) } diff --git a/internal/build/phase_config_provider.go b/internal/build/phase_config_provider.go index 0f3ee82a6b..d33fddce67 100644 --- a/internal/build/phase_config_provider.go +++ b/internal/build/phase_config_provider.go @@ -7,6 +7,7 @@ import ( "strings" "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/network" pcontainer "github.com/buildpacks/pack/internal/container" "github.com/buildpacks/pack/internal/style" @@ -23,6 +24,7 @@ type PhaseConfigProviderOperation func(*PhaseConfigProvider) type PhaseConfigProvider struct { ctrConf *container.Config + endpoint *network.EndpointSettings hostConf *container.HostConfig name string os string @@ -37,6 +39,7 @@ func NewPhaseConfigProvider(name string, lifecycleExec *LifecycleExecution, ops provider := &PhaseConfigProvider{ ctrConf: new(container.Config), hostConf: new(container.HostConfig), + endpoint: new(network.EndpointSettings), name: name, os: lifecycleExec.os, infoWriter: logging.GetWriterForLevel(lifecycleExec.logger, logging.InfoLevel), @@ -47,7 +50,7 @@ func NewPhaseConfigProvider(name string, lifecycleExec *LifecycleExecution, ops provider.ctrConf.Labels = map[string]string{"author": "pack"} if lifecycleExec.opts.MacAddress != "" { - provider.ctrConf.MacAddress = lifecycleExec.opts.MacAddress + provider.endpoint.MacAddress = lifecycleExec.opts.MacAddress lifecycleExec.logger.Debugf("MAC Address: %s", style.Symbol(lifecycleExec.opts.MacAddress)) } @@ -106,6 +109,10 @@ func (p *PhaseConfigProvider) ContainerConfig() *container.Config { return p.ctrConf } +func (p *PhaseConfigProvider) Endpoint() *network.EndpointSettings { + return p.endpoint +} + func (p *PhaseConfigProvider) ContainerOps() []ContainerOperation { return p.containerOps } diff --git a/internal/build/phase_config_provider_test.go b/internal/build/phase_config_provider_test.go index 07a92239e8..11144d5ed1 100644 --- a/internal/build/phase_config_provider_test.go +++ b/internal/build/phase_config_provider_test.go @@ -82,7 +82,7 @@ func testPhaseConfigProvider(t *testing.T, when spec.G, it spec.S) { phaseConfigProvider := build.NewPhaseConfigProvider("some-name", lifecycle) - h.AssertEq(t, phaseConfigProvider.ContainerConfig().MacAddress, expectedMacAddress) + h.AssertEq(t, phaseConfigProvider.Endpoint().MacAddress, expectedMacAddress) }) }) diff --git a/internal/build/phase_factory.go b/internal/build/phase_factory.go index 97ae788349..6291df5416 100644 --- a/internal/build/phase_factory.go +++ b/internal/build/phase_factory.go @@ -24,6 +24,7 @@ func NewDefaultPhaseFactory(lifecycleExec *LifecycleExecution) PhaseFactory { func (m *DefaultPhaseFactory) New(provider *PhaseConfigProvider) RunnerCleaner { return &Phase{ ctrConf: provider.ContainerConfig(), + endpoint: provider.Endpoint(), hostConf: provider.HostConfig(), name: provider.Name(), docker: m.lifecycleExec.docker, diff --git a/internal/commands/builder_create.go b/internal/commands/builder_create.go index 56ac071fe2..c66fa922f9 100644 --- a/internal/commands/builder_create.go +++ b/internal/commands/builder_create.go @@ -3,6 +3,7 @@ package commands import ( "fmt" "path/filepath" + "runtime" "github.com/pkg/errors" "github.com/spf13/cobra" @@ -10,8 +11,10 @@ import ( "github.com/buildpacks/pack/builder" "github.com/buildpacks/pack/internal/config" "github.com/buildpacks/pack/internal/style" + "github.com/buildpacks/pack/internal/target" "github.com/buildpacks/pack/pkg/buildpack" "github.com/buildpacks/pack/pkg/client" + "github.com/buildpacks/pack/pkg/dist" "github.com/buildpacks/pack/pkg/image" "github.com/buildpacks/pack/pkg/logging" ) @@ -24,6 +27,7 @@ type BuilderCreateFlags struct { Policy string Flatten []string Label map[string]string + Targets []string } // CreateBuilder creates a builder image, based on a builder config @@ -55,7 +59,12 @@ Creating a custom builder allows you to control what buildpacks are used and wha return errors.Wrapf(err, "parsing pull policy %s", flags.Policy) } - builderConfig, warns, err := builder.ReadConfig(flags.BuilderTomlPath) + targets, err := target.ParseTargets(flags.Targets, logger) + if err != nil { + return err + } + + builderConfig, warns, err := builder.ReadMultiArchConfig(flags.BuilderTomlPath, targets) if err != nil { return errors.Wrap(err, "invalid builder toml") } @@ -63,7 +72,7 @@ Creating a custom builder allows you to control what buildpacks are used and wha logger.Warnf("builder configuration: %s", w) } - if hasExtensions(builderConfig) { + if hasExtensions(builderConfig.Config) { if !cfg.Experimental { return errors.New("builder config contains image extensions; support for image extensions is currently experimental") } @@ -88,7 +97,7 @@ Creating a custom builder allows you to control what buildpacks are used and wha } imageName := args[0] - if err := pack.CreateBuilder(cmd.Context(), client.CreateBuilderOptions{ + builderOpts := client.CreateBuilderOptions{ RelativeBaseDir: relativeBaseDir, BuildConfigEnv: envMap, BuilderName: imageName, @@ -98,9 +107,28 @@ Creating a custom builder allows you to control what buildpacks are used and wha PullPolicy: pullPolicy, Flatten: toFlatten, Labels: flags.Label, - }); err != nil { - return err } + + switch multiArch, publish := builderConfig.MultiArch(), flags.Publish; { + case multiArch && !publish: + builderConfig.WithTargets = []dist.Target{{OS: runtime.GOOS, Arch: runtime.GOARCH}} + if err := pack.CreateBuilder(cmd.Context(), builderOpts); err != nil { + return err + } + case multiArch && publish: + if err := pack.CreateMultiArchBuilder(cmd.Context(), builderOpts); err != nil { + return err + } + default: + if len(builderConfig.Targets()) == 0 { + logger.Warnf("A new '--target' flag is available to set the platform for a builder, using '%s' as default", style.Symbol("---os---")) + } + + if err := pack.CreateBuilder(cmd.Context(), builderOpts); err != nil { + return err + } + } + logger.Infof("Successfully created builder image %s", style.Symbol(imageName)) logging.Tip(logger, "Run %s to use this builder", style.Symbol(fmt.Sprintf("pack build --builder %s", imageName))) return nil @@ -116,6 +144,15 @@ Creating a custom builder allows you to control what buildpacks are used and wha cmd.Flags().StringVar(&flags.Policy, "pull-policy", "", "Pull policy to use. Accepted values are always, never, and if-not-present. The default is always") cmd.Flags().StringArrayVar(&flags.Flatten, "flatten", nil, "List of buildpacks to flatten together into a single layer (format: '@,@'") cmd.Flags().StringToStringVarP(&flags.Label, "label", "l", nil, "Labels to add to the builder image, in the form of '='") + cmd.Flags().StringSliceVarP(&flags.Targets, "target", "t", nil, + `Targets are the platforms list to build. one can provide target platforms in format [os][/arch][/variant]:[distroname@osversion@anotherversion];[distroname@osversion] + - Base case for two different architectures : '--target "linux/amd64" --target "linux/arm64"' + - case for distribution version: '--target "windows/amd64:windows-nano@10.0.19041.1415"' + - case for different architecture with distributed versions : '--target "linux/arm/v6:ubuntu@14.04" --target "linux/arm/v6:ubuntu@16.04"' + `) + if !cfg.Experimental { + cmd.Flags().MarkHidden("target") + } AddHelpFlag(cmd, "create") return cmd diff --git a/internal/commands/builder_create_test.go b/internal/commands/builder_create_test.go index b89e7ab534..5f7d9f773a 100644 --- a/internal/commands/builder_create_test.go +++ b/internal/commands/builder_create_test.go @@ -7,6 +7,7 @@ import ( "path/filepath" "testing" + "github.com/BurntSushi/toml" "github.com/golang/mock/gomock" "github.com/heroku/color" "github.com/sclevine/spec" @@ -17,6 +18,7 @@ import ( "github.com/buildpacks/pack/internal/commands" "github.com/buildpacks/pack/internal/commands/testmocks" "github.com/buildpacks/pack/internal/config" + "github.com/buildpacks/pack/pkg/dist" "github.com/buildpacks/pack/pkg/logging" h "github.com/buildpacks/pack/testhelpers" ) @@ -441,5 +443,104 @@ func testCreateCommand(t *testing.T, when spec.G, it spec.S) { }) }) }) + + when("--targets", func() { + var ( + builderName = "some/builder" + moduleCollection = builder.ModuleCollection{ + { + ImageOrURI: dist.ImageOrURI{ + BuildpackURI: dist.BuildpackURI{ + URI: "some/bp", + }, + }, + }, + } + config = builder.Config{ + Buildpacks: moduleCollection, + Order: dist.Order{ + dist.OrderEntry{ + Group: []dist.ModuleRef{ + { + ModuleInfo: dist.ModuleInfo{ + Name: "some/bp", + }, + }, + }, + }, + }, + } + ) + it.Before(func() { + configFile, err := os.Create(builderConfigPath) + h.AssertNil(t, err) + h.AssertNil(t, toml.NewEncoder(configFile).Encode(config)) + }) + it("--publish not specified, should build with current platform as target", func() { + command.SetArgs([]string{ + builderName, + "--config", builderConfigPath, + "--target", "linux/amd64", + "--target", "linux/arm/v6", + }) + // expectedConfig := config + // expectedConfig.WithTargets = []dist.Target{ + // { + // OS: runtime.GOOS, + // Arch: runtime.GOARCH, + // }, + // } + mockClient.EXPECT().CreateBuilder(gomock.Any(), gomock.Any()). + Times(1). + Return(nil) + h.AssertNil(t, command.Execute()) + }) + it("should build multi arch", func() { + command.SetArgs([]string{ + builderName, + "--config", builderConfigPath, + "--target", "linux/amd64", + "--target", "linux/arm/v6", + "--publish", + }) + // expectedConfig := config + // expectedConfig.WithTargets = []dist.Target{ + // { + // OS: "linux", + // Arch: "amd64", + // }, + // { + // OS: "linux", + // Arch: "arm", + // ArchVariant: "v6", + // }, + // } + mockClient.EXPECT().CreateMultiArchBuilder(gomock.Any(), gomock.Any()). + Times(1). + Return(nil) + h.AssertNil(t, command.Execute()) + }) + it("should build single arch", func() { + command.SetArgs([]string{ + builderName, + "--config", builderConfigPath, + "--publish", + }) + expectedConfig := config + expectedConfig.WithTargets = []dist.Target{ + { + OS: "linux", + Arch: "amd64", + }, + } + configFile, err := os.Create(builderConfigPath) + h.AssertNil(t, err) + toml.NewEncoder(configFile).Encode(expectedConfig) + mockClient.EXPECT().CreateBuilder(gomock.Any(), gomock.Any()). + Times(1). + Return(nil) + h.AssertNil(t, command.Execute()) + }) + }) }) } diff --git a/internal/commands/buildpack_inspect.go b/internal/commands/buildpack_inspect.go index e75c479f89..2829967368 100644 --- a/internal/commands/buildpack_inspect.go +++ b/internal/commands/buildpack_inspect.go @@ -42,7 +42,7 @@ func BuildpackInspect(logger logging.Logger, cfg config.Config, client PackClien return cmd } -func buildpackInspect(logger logging.Logger, buildpackName, registryName string, flags BuildpackInspectFlags, cfg config.Config, pack PackClient) error { +func buildpackInspect(logger logging.Logger, buildpackName, registryName string, flags BuildpackInspectFlags, _ config.Config, pack PackClient) error { logger.Infof("Inspecting buildpack: %s\n", style.Symbol(buildpackName)) inspectedBuildpacksOutput, err := inspectAllBuildpacks( diff --git a/internal/commands/buildpack_package.go b/internal/commands/buildpack_package.go index adb95c3e03..14b900a72e 100644 --- a/internal/commands/buildpack_package.go +++ b/internal/commands/buildpack_package.go @@ -2,16 +2,22 @@ package commands import ( "context" + "fmt" + "os" "path/filepath" + "runtime" "strings" + "github.com/BurntSushi/toml" "github.com/pkg/errors" "github.com/spf13/cobra" pubbldpkg "github.com/buildpacks/pack/buildpackage" "github.com/buildpacks/pack/internal/config" "github.com/buildpacks/pack/internal/style" + "github.com/buildpacks/pack/internal/target" "github.com/buildpacks/pack/pkg/client" + "github.com/buildpacks/pack/pkg/dist" "github.com/buildpacks/pack/pkg/image" "github.com/buildpacks/pack/pkg/logging" ) @@ -27,16 +33,20 @@ type BuildpackPackageFlags struct { Label map[string]string Publish bool Flatten bool + Targets []string } // BuildpackPackager packages buildpacks type BuildpackPackager interface { PackageBuildpack(ctx context.Context, options client.PackageBuildpackOptions) error + PackageMultiArchBuildpack(ctx context.Context, opts client.PackageBuildpackOptions) error } // PackageConfigReader reads BuildpackPackage configs type PackageConfigReader interface { Read(path string) (pubbldpkg.Config, error) + ReadBuildpackDescriptor(path string) (dist.BuildpackDescriptor, error) + ReadExtensionDescriptor(path string) (dist.ExtensionDescriptor, error) } // BuildpackPackage packages (a) buildpack(s) into OCI format, based on a package config @@ -66,43 +76,78 @@ func BuildpackPackage(logger logging.Logger, cfg config.Config, packager Buildpa if err != nil { return errors.Wrap(err, "parsing pull policy") } + + targets, err := target.ParseTargets(flags.Targets, logger) + if err != nil { + return err + } + bpPackageCfg := pubbldpkg.DefaultConfig() - var bpPath string + relativeBaseDir := "" + if flags.PackageTomlPath != "" { + if bpPackageCfg, err = packageConfigReader.Read(flags.PackageTomlPath); err != nil { + return errors.Wrap(err, "reading config") + } + + if relativeBaseDir, err = filepath.Abs(filepath.Dir(flags.PackageTomlPath)); err != nil { + return errors.Wrap(err, "getting absolute path for config") + } + } + + bpPath, pkgMultiArchConfig := "", pubbldpkg.NewMultiArchPackage(bpPackageCfg, relativeBaseDir) if flags.Path != "" { if bpPath, err = filepath.Abs(flags.Path); err != nil { return errors.Wrap(err, "resolving buildpack path") } bpPackageCfg.Buildpack.URI = bpPath } - relativeBaseDir := "" - if flags.PackageTomlPath != "" { - bpPackageCfg, err = packageConfigReader.Read(flags.PackageTomlPath) - if err != nil { - return errors.Wrap(err, "reading config") - } - relativeBaseDir, err = filepath.Abs(filepath.Dir(flags.PackageTomlPath)) - if err != nil { - return errors.Wrap(err, "getting absolute path for config") + bpConfigPathAbs, err := filepath.Abs(bpPath) + if err != nil { + return err + } + bpConfigPath := filepath.Join(bpConfigPathAbs, "buildpack.toml") + if _, err = os.Stat(bpConfigPath); err != nil { + return fmt.Errorf("cannot find %s: %s", style.Symbol("buildpack.toml"), style.Symbol(bpConfigPath)) + } + + bpConfig, err := packageConfigReader.ReadBuildpackDescriptor(bpConfigPath) + if err != nil { + return err + } + bpMultiArchConfig := pubbldpkg.NewMultiArchBuildpack(bpConfig, bpPath, flags.Flatten, cmd.Flags().Changed("flatten"), targets) + bpConfigs, err := bpMultiArchConfig.MultiArchConfigs() + if err != nil { + return err + } + + // if not publishing image and is a MultiArch image, only build the target with target's specific device's platform + if !flags.Publish && len(bpConfigs) > 1 { + targets = []dist.Target{{OS: runtime.GOOS, Arch: runtime.GOARCH}} + bpMultiArchConfig = pubbldpkg.NewMultiArchBuildpack(bpConfig, bpPath, flags.Flatten, cmd.Flags().Changed("flatten"), targets) + if bpConfigs, err = bpMultiArchConfig.MultiArchConfigs(); err != nil { + return err } } - name := args[0] + + bpName := args[0] if flags.Format == client.FormatFile { - switch ext := filepath.Ext(name); ext { + switch ext := filepath.Ext(bpName); ext { case client.CNBExtension: case "": - name += client.CNBExtension + bpName += client.CNBExtension default: logger.Warnf("%s is not a valid extension for a packaged buildpack. Packaged buildpacks must have a %s extension", style.Symbol(ext), style.Symbol(client.CNBExtension)) } } + if flags.Flatten { logger.Warn("Flattening a buildpack package could break the distribution specification. Please use it with caution.") } - if err := packager.PackageBuildpack(cmd.Context(), client.PackageBuildpackOptions{ + pkgBPOpts := client.PackageBuildpackOptions{ RelativeBaseDir: relativeBaseDir, - Name: name, + Name: bpName, Format: flags.Format, Config: bpPackageCfg, Publish: flags.Publish, @@ -111,8 +156,39 @@ func BuildpackPackage(logger logging.Logger, cfg config.Config, packager Buildpa Flatten: flags.Flatten, FlattenExclude: flags.FlattenExclude, Labels: flags.Label, - }); err != nil { - return err + Version: bpConfig.WithInfo.Version, + IndexOptions: pubbldpkg.IndexOptions{ + Targets: []dist.Target{{ + OS: bpPackageCfg.Platform.OS, + }}, + }, + } + + if len(bpConfigs) > 1 { + pkgBPOpts.RelativeBaseDir = bpConfigPath + pkgBPOpts.IndexOptions = pubbldpkg.IndexOptions{ + BPConfigs: &bpConfigs, + PkgConfig: pkgMultiArchConfig, + Logger: logger, + } + + err = packager.PackageMultiArchBuildpack(cmd.Context(), pkgBPOpts) + if err := revertBPConfig(bpConfigPath, bpConfig); err != nil { + return fmt.Errorf("unable to revert changes of buildpack %s", style.Symbol(bpName)) + } + if err != nil { + return err + } + } else { + if len(bpConfigs) == 1 { + pkgBPOpts.IndexOptions.Targets = bpConfigs[0].Targets() + } else { + logger.Warnf("A new '--target' flag is available to set the platform for the buildpack package, using '%s' as default", bpPackageCfg.Platform.OS) + } + + if err := packager.PackageBuildpack(cmd.Context(), pkgBPOpts); err != nil { + return err + } } action := "created" @@ -124,7 +200,7 @@ func BuildpackPackage(logger logging.Logger, cfg config.Config, packager Buildpa if flags.Format == client.FormatFile { location = "file" } - logger.Infof("Successfully %s package %s and saved to %s", action, style.Symbol(name), location) + logger.Infof("Successfully %s package %s and saved to %s", action, style.Symbol(bpName), location) return nil }), } @@ -138,7 +214,14 @@ func BuildpackPackage(logger logging.Logger, cfg config.Config, packager Buildpa cmd.Flags().BoolVar(&flags.Flatten, "flatten", false, "Flatten the buildpack into a single layer") cmd.Flags().StringSliceVarP(&flags.FlattenExclude, "flatten-exclude", "e", nil, "Buildpacks to exclude from flattening, in the form of '@'") cmd.Flags().StringToStringVarP(&flags.Label, "label", "l", nil, "Labels to add to packaged Buildpack, in the form of '='") + cmd.Flags().StringSliceVarP(&flags.Targets, "target", "t", nil, + `Targets are the platforms list to build. one can provide target platforms in format [os][/arch][/variant]:[distroname@osversion@anotherversion];[distroname@osversion] + - Base case for two different architectures : '--target "linux/amd64" --target "linux/arm64"' + - case for distribution version: '--target "windows/amd64:windows-nano@10.0.19041.1415"' + - case for different architecture with distributed versions : '--target "linux/arm/v6:ubuntu@14.04" --target "linux/arm/v6:ubuntu@16.04"' + `) if !cfg.Experimental { + cmd.Flags().MarkHidden("target") cmd.Flags().MarkHidden("flatten") cmd.Flags().MarkHidden("flatten-exclude") } @@ -146,6 +229,15 @@ func BuildpackPackage(logger logging.Logger, cfg config.Config, packager Buildpa return cmd } +func revertBPConfig(bpConfigPath string, bpConfig dist.BuildpackDescriptor) error { + bpConfigFile, err := os.OpenFile(bpConfigPath, os.O_WRONLY|os.O_TRUNC, os.ModePerm) + if err != nil { + return err + } + + return toml.NewEncoder(bpConfigFile).Encode(bpConfig) +} + func validateBuildpackPackageFlags(cfg config.Config, p *BuildpackPackageFlags) error { if p.Publish && p.Policy == image.PullNever.String() { return errors.Errorf("--publish and --pull-policy never cannot be used together. The --publish flag requires the use of remote images.") diff --git a/internal/commands/buildpack_package_test.go b/internal/commands/buildpack_package_test.go index f527c01d08..96d7b80041 100644 --- a/internal/commands/buildpack_package_test.go +++ b/internal/commands/buildpack_package_test.go @@ -3,18 +3,24 @@ package commands_test import ( "bytes" "fmt" + "os" "path/filepath" "testing" + "github.com/BurntSushi/toml" + "github.com/golang/mock/gomock" "github.com/heroku/color" "github.com/pkg/errors" "github.com/sclevine/spec" "github.com/sclevine/spec/report" "github.com/spf13/cobra" + "github.com/buildpacks/lifecycle/api" + pubbldpkg "github.com/buildpacks/pack/buildpackage" "github.com/buildpacks/pack/internal/commands" "github.com/buildpacks/pack/internal/commands/fakes" + "github.com/buildpacks/pack/internal/commands/testmocks" "github.com/buildpacks/pack/internal/config" "github.com/buildpacks/pack/pkg/dist" "github.com/buildpacks/pack/pkg/image" @@ -30,12 +36,45 @@ func TestPackageCommand(t *testing.T) { func testPackageCommand(t *testing.T, when spec.G, it spec.S) { var ( - logger *logging.LogWithWriters - outBuf bytes.Buffer + logger *logging.LogWithWriters + outBuf bytes.Buffer + mockController *gomock.Controller + mockClient *testmocks.MockPackClient + command *cobra.Command + cfg config.Config + bpPackager *fakes.FakeBuildpackPackager + bpConfigReader *fakes.FakePackageConfigReader + bpConfigFolder = os.TempDir() + bpConfigPath = filepath.Join(bpConfigFolder, "buildpack.toml") + pkgConfigPath = filepath.Join(bpConfigFolder, "package.toml") + pkgConfig = pubbldpkg.DefaultConfig() + bpConfig = dist.BuildpackDescriptor{ + WithAPI: minimalLifecycleDescriptor.API.BuildpackVersion, + WithInfo: dist.ModuleInfo{ + ID: "some/bp", + Name: "some/bp", + Version: "latest", + }, + } ) it.Before(func() { logger = logging.NewLogWithWriters(&outBuf, &outBuf) + mockController = gomock.NewController(t) + mockClient = testmocks.NewMockPackClient(mockController) + cfg = config.Config{} + bpPackager = &fakes.FakeBuildpackPackager{} + bpConfigReader = fakes.NewFakePackageConfigReader() + command = commands.BuildpackPackage(logger, cfg, bpPackager, bpConfigReader) + + bpFile, err := os.Create(bpConfigPath) + h.AssertNil(t, err) + + pkgConfigFile, err := os.Create(pkgConfigPath) + h.AssertNil(t, err) + + h.AssertNil(t, toml.NewEncoder(bpFile).Encode(bpConfig)) + h.AssertNil(t, toml.NewEncoder(pkgConfigFile).Encode(pkgConfig)) }) when("Package#Execute", func() { @@ -72,6 +111,55 @@ func testPackageCommand(t *testing.T, when spec.G, it spec.S) { h.AssertEq(t, receivedOptions.Name, "my-specific-image") }) + it("#MultiArchBuildpack", func() { + tmpDir, err := os.MkdirTemp("", "tmpDir") + h.AssertNilE(t, err) + + bpFile, err := os.CreateTemp(tmpDir, "buildpack-*.toml") + h.AssertNilE(t, err) + + bpConfig := dist.BuildpackDescriptor{ + WithAPI: api.Platform.Latest(), + WithInfo: dist.ModuleInfo{ + ID: "some/bp", + }, + WithTargets: []dist.Target{ + { + OS: "linux", + Arch: "arm", + ArchVariant: "v6", + Distributions: []dist.Distribution{ + { + Name: "ubuntu", + Versions: []string{"22.04", "20.04"}, + }, + { + Name: "debian", + Versions: []string{"8.0"}, + }, + }, + Specs: dist.TargetSpecs{ + Features: []string{"feature1", "feature2"}, + OSFeatures: []string{"osFeature1", "osFeature2"}, + URLs: []string{"url1", "url2"}, + Annotations: map[string]string{"key1": "value1", "key2": "value2"}, + Flatten: false, + FlattenExclude: make([]string, 0), + Labels: map[string]string{"io.buildpacks.distro.name": "debian"}, + Path: "some-path", + }, + }, + }, + } + + h.AssertNilE(t, toml.NewEncoder(bpFile).Encode(bpConfig)) + h.AssertNilE(t, os.Chdir(tmpDir)) + + cmd := packageCommand(withBuildpackPackager(fakeBuildpackPackager)) + cmd.SetArgs([]string{"some/bp", "-p", "./buildpack.toml"}) + h.AssertNil(t, cmd.Execute()) + }) + it("creates package with config returned by the reader", func() { myConfig := pubbldpkg.Config{ Buildpack: dist.BuildpackURI{URI: "test"}, @@ -259,6 +347,22 @@ func testPackageCommand(t *testing.T, when spec.G, it spec.S) { }) }) }) + + when("--target", func() { + it("should package multi-arch bp", func() { + command.SetArgs([]string{ + "some/image", + "--config", pkgConfigPath, + "--path", bpConfigFolder, + "--target", "linux/amd64", + "--target", "linux/arm/v6", + }) + mockClient.EXPECT().PackageBuildpack(gomock.Any(), gomock.Any()).Times(1).Return(nil) + h.AssertNil(t, command.Execute()) + }) + it("should package single-arch bp when --publish is not specified", func() {}) + it("should package single-arch bp", func() {}) + }) }) when("invalid flags", func() { diff --git a/internal/commands/commands.go b/internal/commands/commands.go index 11fa645ca5..e6a7bc33aa 100644 --- a/internal/commands/commands.go +++ b/internal/commands/commands.go @@ -22,9 +22,12 @@ type PackClient interface { InspectImage(string, bool) (*client.ImageInfo, error) Rebase(context.Context, client.RebaseOptions) error CreateBuilder(context.Context, client.CreateBuilderOptions) error + CreateMultiArchBuilder(context.Context, client.CreateBuilderOptions) error NewBuildpack(context.Context, client.NewBuildpackOptions) error PackageBuildpack(ctx context.Context, opts client.PackageBuildpackOptions) error + PackageMultiArchBuildpack(ctx context.Context, opts client.PackageBuildpackOptions) error PackageExtension(ctx context.Context, opts client.PackageBuildpackOptions) error + PackageMultiArchExtension(ctx context.Context, opts client.PackageBuildpackOptions) error Build(context.Context, client.BuildOptions) error RegisterBuildpack(context.Context, client.RegisterBuildpackOptions) error YankBuildpack(client.YankBuildpackOptions) error @@ -32,6 +35,14 @@ type PackClient interface { InspectExtension(client.InspectExtensionOptions) (*client.ExtensionInfo, error) PullBuildpack(context.Context, client.PullBuildpackOptions) error DownloadSBOM(name string, options client.DownloadSBOMOptions) error + CreateManifest(ctx context.Context, name string, images []string, opts client.CreateManifestOptions) error + AnnotateManifest(ctx context.Context, name string, image string, opts client.ManifestAnnotateOptions) error + ExistsManifest(ctx context.Context, image string) error + AddManifest(ctx context.Context, index string, images string, opts client.ManifestAddOptions) error + DeleteManifest(ctx context.Context, name []string) []error + RemoveManifest(ctx context.Context, name string, images []string) []error + PushManifest(ctx context.Context, index string, opts client.PushManifestOptions) error + InspectManifest(ctx context.Context, name string) error } func AddHelpFlag(cmd *cobra.Command, commandName string) { diff --git a/internal/commands/create_builder.go b/internal/commands/create_builder.go index 9999392ef6..063fc20d9b 100644 --- a/internal/commands/create_builder.go +++ b/internal/commands/create_builder.go @@ -3,6 +3,7 @@ package commands import ( "fmt" "path/filepath" + "runtime" "github.com/pkg/errors" "github.com/spf13/cobra" @@ -10,7 +11,9 @@ import ( "github.com/buildpacks/pack/builder" "github.com/buildpacks/pack/internal/config" "github.com/buildpacks/pack/internal/style" + "github.com/buildpacks/pack/internal/target" "github.com/buildpacks/pack/pkg/client" + "github.com/buildpacks/pack/pkg/dist" "github.com/buildpacks/pack/pkg/image" "github.com/buildpacks/pack/pkg/logging" ) @@ -48,7 +51,12 @@ Creating a custom builder allows you to control what buildpacks are used and wha return errors.Wrapf(err, "parsing pull policy %s", flags.Policy) } - builderConfig, warnings, err := builder.ReadConfig(flags.BuilderTomlPath) + targets, err := target.ParseTargets(flags.Targets, logger) + if err != nil { + return err + } + + builderConfig, warnings, err := builder.ReadMultiArchConfig(flags.BuilderTomlPath, targets) if err != nil { return errors.Wrap(err, "invalid builder toml") } @@ -56,7 +64,7 @@ Creating a custom builder allows you to control what buildpacks are used and wha logger.Warnf("builder configuration: %s", w) } - if hasExtensions(builderConfig) { + if hasExtensions(builderConfig.Config) { if !cfg.Experimental { return errors.New("builder config contains image extensions; support for image extensions is currently experimental") } @@ -68,16 +76,35 @@ Creating a custom builder allows you to control what buildpacks are used and wha } imageName := args[0] - if err := pack.CreateBuilder(cmd.Context(), client.CreateBuilderOptions{ + builderOpts := client.CreateBuilderOptions{ RelativeBaseDir: relativeBaseDir, BuilderName: imageName, Config: builderConfig, Publish: flags.Publish, Registry: flags.Registry, PullPolicy: pullPolicy, - }); err != nil { - return err } + + switch multiArch, publish := builderConfig.MultiArch(), flags.Publish; { + case multiArch && !publish: + builderConfig.WithTargets = []dist.Target{{OS: runtime.GOOS, Arch: runtime.GOARCH}} + if err := pack.CreateBuilder(cmd.Context(), builderOpts); err != nil { + return err + } + case multiArch && publish: + if err := pack.CreateMultiArchBuilder(cmd.Context(), builderOpts); err != nil { + return err + } + default: + if len(builderConfig.Targets()) == 0 { + logger.Warnf("A new '--target' flag is available to set the platform for a builder, using '%s' as default", style.Symbol("---os---")) + } + + if err := pack.CreateBuilder(cmd.Context(), builderOpts); err != nil { + return err + } + } + logger.Infof("Successfully created builder image %s", style.Symbol(imageName)) logging.Tip(logger, "Run %s to use this builder", style.Symbol(fmt.Sprintf("pack build --builder %s", imageName))) return nil @@ -91,5 +118,14 @@ Creating a custom builder allows you to control what buildpacks are used and wha cmd.Flags().StringVarP(&flags.BuilderTomlPath, "config", "c", "", "Path to builder TOML file (required)") cmd.Flags().BoolVar(&flags.Publish, "publish", false, "Publish the builder directly to the container registry specified in , instead of the daemon.") cmd.Flags().StringVar(&flags.Policy, "pull-policy", "", "Pull policy to use. Accepted values are always, never, and if-not-present. The default is always") + cmd.Flags().StringSliceVarP(&flags.Targets, "target", "t", nil, + `Targets are the platforms list to build. one can provide target platforms in format [os][/arch][/variant]:[distroname@osversion@anotherversion];[distroname@osversion] + - Base case for two different architectures : '--target "linux/amd64" --target "linux/arm64"' + - case for distribution version: '--target "windows/amd64:windows-nano@10.0.19041.1415"' + - case for different architecture with distributed versions : '--target "linux/arm/v6:ubuntu@14.04" --target "linux/arm/v6:ubuntu@16.04"' + `) + if !cfg.Experimental { + cmd.Flags().MarkHidden("target") + } return cmd } diff --git a/internal/commands/create_builder_test.go b/internal/commands/create_builder_test.go index f960ebb2c1..dc828cacf3 100644 --- a/internal/commands/create_builder_test.go +++ b/internal/commands/create_builder_test.go @@ -6,15 +6,18 @@ import ( "path/filepath" "testing" + "github.com/BurntSushi/toml" "github.com/golang/mock/gomock" "github.com/heroku/color" "github.com/sclevine/spec" "github.com/sclevine/spec/report" "github.com/spf13/cobra" + "github.com/buildpacks/pack/builder" "github.com/buildpacks/pack/internal/commands" "github.com/buildpacks/pack/internal/commands/testmocks" "github.com/buildpacks/pack/internal/config" + "github.com/buildpacks/pack/pkg/dist" "github.com/buildpacks/pack/pkg/logging" h "github.com/buildpacks/pack/testhelpers" ) @@ -178,5 +181,103 @@ func testCreateBuilderCommand(t *testing.T, when spec.G, it spec.S) { h.AssertError(t, command.Execute(), "builder config contains image extensions; support for image extensions is currently experimental") }) }) + when("--targets", func() { + var ( + builderName = "some/builder" + moduleCollection = builder.ModuleCollection{ + { + ImageOrURI: dist.ImageOrURI{ + BuildpackURI: dist.BuildpackURI{ + URI: "some/bp", + }, + }, + }, + } + config = builder.Config{ + Buildpacks: moduleCollection, + Order: dist.Order{ + dist.OrderEntry{ + Group: []dist.ModuleRef{ + { + ModuleInfo: dist.ModuleInfo{ + Name: "some/bp", + }, + }, + }, + }, + }, + } + ) + it.Before(func() { + configFile, err := os.Create(builderConfigPath) + h.AssertNil(t, err) + h.AssertNil(t, toml.NewEncoder(configFile).Encode(config)) + }) + it("--publish not specified, should build with current platform as target", func() { + command.SetArgs([]string{ + builderName, + "--config", builderConfigPath, + "--target", "linux/amd64", + "--target", "linux/arm/v6", + }) + // expectedConfig := config + // expectedConfig.WithTargets = []dist.Target{ + // { + // OS: runtime.GOOS, + // Arch: runtime.GOARCH, + // }, + // } + mockClient.EXPECT().CreateBuilder(gomock.Any(), gomock.Any()). + Times(1). + Return(nil) + h.AssertNil(t, command.Execute()) + }) + it("should build multi arch", func() { + command.SetArgs([]string{ + builderName, + "--config", builderConfigPath, + "--target", "linux/amd64", + "--target", "linux/arm/v6", + "--publish", + }) + // expectedConfig := config + // expectedConfig.WithTargets = []dist.Target{ + // { + // OS: "linux", + // Arch: "amd64", + // }, + // { + // OS: "linux", + // Arch: "arm", + // ArchVariant: "v6", + // }, + // } + mockClient.EXPECT().CreateMultiArchBuilder(gomock.Any(), gomock.Any()). + Times(1). + Return(nil) + h.AssertNil(t, command.Execute()) + }) + it("should build single arch", func() { + command.SetArgs([]string{ + builderName, + "--config", builderConfigPath, + "--publish", + }) + expectedConfig := config + expectedConfig.WithTargets = []dist.Target{ + { + OS: "linux", + Arch: "amd64", + }, + } + configFile, err := os.Create(builderConfigPath) + h.AssertNil(t, err) + toml.NewEncoder(configFile).Encode(expectedConfig) + mockClient.EXPECT().CreateBuilder(gomock.Any(), gomock.Any()). + Times(1). + Return(nil) + h.AssertNil(t, command.Execute()) + }) + }) }) } diff --git a/internal/commands/extension_inspect.go b/internal/commands/extension_inspect.go index b7a09bee4e..42863f78bf 100644 --- a/internal/commands/extension_inspect.go +++ b/internal/commands/extension_inspect.go @@ -26,7 +26,7 @@ func ExtensionInspect(logger logging.Logger, cfg config.Config, client PackClien return cmd } -func extensionInspect(logger logging.Logger, extensionName string, cfg config.Config, pack PackClient) error { +func extensionInspect(logger logging.Logger, extensionName string, _ config.Config, pack PackClient) error { logger.Infof("Inspecting extension: %s\n", style.Symbol(extensionName)) inspectedExtensionsOutput, err := inspectAllExtensions( diff --git a/internal/commands/extension_package.go b/internal/commands/extension_package.go index 0415823e57..6e909fe286 100644 --- a/internal/commands/extension_package.go +++ b/internal/commands/extension_package.go @@ -2,15 +2,21 @@ package commands import ( "context" + "fmt" + "os" "path/filepath" + "runtime" + "github.com/BurntSushi/toml" "github.com/pkg/errors" "github.com/spf13/cobra" pubbldpkg "github.com/buildpacks/pack/buildpackage" "github.com/buildpacks/pack/internal/config" "github.com/buildpacks/pack/internal/style" + "github.com/buildpacks/pack/internal/target" "github.com/buildpacks/pack/pkg/client" + "github.com/buildpacks/pack/pkg/dist" "github.com/buildpacks/pack/pkg/image" "github.com/buildpacks/pack/pkg/logging" ) @@ -21,11 +27,13 @@ type ExtensionPackageFlags struct { Format string Publish bool Policy string + Targets []string } // ExtensionPackager packages extensions type ExtensionPackager interface { PackageExtension(ctx context.Context, options client.PackageBuildpackOptions) error + PackageMultiArchExtension(ctx context.Context, opts client.PackageBuildpackOptions) error } // ExtensionPackage packages (a) extension(s) into OCI format, based on a package config @@ -50,19 +58,56 @@ func ExtensionPackage(logger logging.Logger, cfg config.Config, packager Extensi return errors.Wrap(err, "parsing pull policy") } - exPackageCfg := pubbldpkg.DefaultExtensionConfig() - relativeBaseDir := "" + relativeBaseDir, exPackageCfg := "", pubbldpkg.DefaultExtensionConfig() if flags.PackageTomlPath != "" { - exPackageCfg, err = packageConfigReader.Read(flags.PackageTomlPath) - if err != nil { + if exPackageCfg, err = packageConfigReader.Read(flags.PackageTomlPath); err != nil { return errors.Wrap(err, "reading config") } - relativeBaseDir, err = filepath.Abs(filepath.Dir(flags.PackageTomlPath)) - if err != nil { + if relativeBaseDir, err = filepath.Abs(filepath.Dir(flags.PackageTomlPath)); err != nil { return errors.Wrap(err, "getting absolute path for config") } } + + extPath, pkgMultiArchConfig := "", pubbldpkg.NewMultiArchPackage(exPackageCfg, relativeBaseDir) + if extPath, err = filepath.Abs("."); err != nil { + return errors.Wrap(err, "resolving extension path") + } + exPackageCfg.Buildpack.URI = extPath + + extConfigPathAbs, err := filepath.Abs(extPath) + if err != nil { + return err + } + extConfigPath := filepath.Join(extConfigPathAbs, "extension.toml") + if _, err = os.Stat(extConfigPath); err != nil { + return fmt.Errorf("cannot find %s: %s", style.Symbol("extension.toml"), style.Symbol(extConfigPath)) + } + + extConfig, err := packageConfigReader.ReadExtensionDescriptor(extConfigPath) + if err != nil { + return err + } + + targets, err := target.ParseTargets(flags.Targets, logger) + if err != nil { + return err + } + + extMultiArchConfig := pubbldpkg.NewMultiArchExtension(extConfig, extPath, targets) + extConfigs, err := extMultiArchConfig.MultiArchConfigs() + if err != nil { + return err + } + + if !flags.Publish && len(extConfigs) > 1 { + targets = []dist.Target{{OS: runtime.GOOS, Arch: runtime.GOARCH}} + extMultiArchConfig = pubbldpkg.NewMultiArchExtension(extConfig, extPath, targets) + if extConfigs, err = extMultiArchConfig.MultiArchConfigs(); err != nil { + return err + } + } + name := args[0] if flags.Format == client.FormatFile { switch ext := filepath.Ext(name); ext { @@ -74,15 +119,41 @@ func ExtensionPackage(logger logging.Logger, cfg config.Config, packager Extensi } } - if err := packager.PackageExtension(cmd.Context(), client.PackageBuildpackOptions{ + pkgExtOpts := client.PackageBuildpackOptions{ RelativeBaseDir: relativeBaseDir, Name: name, Format: flags.Format, Config: exPackageCfg, Publish: flags.Publish, PullPolicy: pullPolicy, - }); err != nil { - return err + } + + if len(extConfigs) > 1 { + pkgExtOpts.RelativeBaseDir = extConfigPath + pkgExtOpts.IndexOptions = pubbldpkg.IndexOptions{ + ExtConfigs: &extConfigs, + PkgConfig: pkgMultiArchConfig, + Logger: logger, + } + + err = packager.PackageMultiArchExtension(cmd.Context(), pkgExtOpts) + if err := revertExtConfig(extConfigPath, extConfig); err != nil { + return fmt.Errorf("unable to revert changes of extension %s", style.Symbol(name)) + } + + if err != nil { + return err + } + } else { + if len(extConfigs) == 1 { + pkgExtOpts.IndexOptions.Targets = extConfigs[0].Targets() + } else { + logger.Warnf("A new '--target' flag is available to set the platform for the extension package, using '%s' as default", exPackageCfg.Platform.OS) + } + + if err := packager.PackageExtension(cmd.Context(), pkgExtOpts); err != nil { + return err + } } action := "created" @@ -104,10 +175,28 @@ func ExtensionPackage(logger logging.Logger, cfg config.Config, packager Extensi cmd.Flags().StringVarP(&flags.Format, "format", "f", "", `Format to save package as ("image" or "file")`) cmd.Flags().BoolVar(&flags.Publish, "publish", false, `Publish the extension directly to the container registry specified in , instead of the daemon (applies to "--format=image" only).`) cmd.Flags().StringVar(&flags.Policy, "pull-policy", "", "Pull policy to use. Accepted values are always, never, and if-not-present. The default is always") + cmd.Flags().StringSliceVarP(&flags.Targets, "target", "t", nil, + `Targets are the platforms list to build. one can provide target platforms in format [os][/arch][/variant]:[distroname@osversion@anotherversion];[distroname@osversion] + - Base case for two different architectures : '--target "linux/amd64" --target "linux/arm64"' + - case for distribution version: '--target "windows/amd64:windows-nano@10.0.19041.1415"' + - case for different architecture with distributed versions : '--target "linux/arm/v6:ubuntu@14.04" --target "linux/arm/v6:ubuntu@16.04"' + `) + if !cfg.Experimental { + cmd.Flags().MarkHidden("target") + } AddHelpFlag(cmd, "package") return cmd } +func revertExtConfig(extConfigPath string, extConfig dist.ExtensionDescriptor) error { + extConfigFile, err := os.OpenFile(extConfigPath, os.O_WRONLY|os.O_TRUNC, os.ModePerm) + if err != nil { + return err + } + + return toml.NewEncoder(extConfigFile).Encode(extConfig) +} + func validateExtensionPackageFlags(p *ExtensionPackageFlags) error { if p.Publish && p.Policy == image.PullNever.String() { return errors.Errorf("--publish and --pull-policy=never cannot be used together. The --publish flag requires the use of remote images.") diff --git a/internal/commands/fakes/fake_buildpack_packager.go b/internal/commands/fakes/fake_buildpack_packager.go index d58ab831c9..198379e26a 100644 --- a/internal/commands/fakes/fake_buildpack_packager.go +++ b/internal/commands/fakes/fake_buildpack_packager.go @@ -10,8 +10,21 @@ type FakeBuildpackPackager struct { CreateCalledWithOptions client.PackageBuildpackOptions } +// PackageMultiArchExtension implements commands.ExtensionPackager. +func (c *FakeBuildpackPackager) PackageMultiArchExtension(ctx context.Context, opts client.PackageBuildpackOptions) error { + c.CreateCalledWithOptions = opts + + return nil +} + func (c *FakeBuildpackPackager) PackageBuildpack(ctx context.Context, opts client.PackageBuildpackOptions) error { c.CreateCalledWithOptions = opts return nil } + +func (c *FakeBuildpackPackager) PackageMultiArchBuildpack(ctx context.Context, opts client.PackageBuildpackOptions) error { + c.CreateCalledWithOptions = opts + + return nil +} diff --git a/internal/commands/fakes/fake_package_config_reader.go b/internal/commands/fakes/fake_package_config_reader.go index e0610a33da..fd11ab1b4a 100644 --- a/internal/commands/fakes/fake_package_config_reader.go +++ b/internal/commands/fakes/fake_package_config_reader.go @@ -2,12 +2,28 @@ package fakes import ( pubbldpkg "github.com/buildpacks/pack/buildpackage" + "github.com/buildpacks/pack/pkg/dist" ) type FakePackageConfigReader struct { ReadCalledWithArg string ReadReturnConfig pubbldpkg.Config ReadReturnError error + + ReadBuildpackDescriptorCalledWithArg string + ReadBuildpackDescriptorReturn dist.BuildpackDescriptor + ReadBuildpackDescriptorReturnError error + + ReadExtensionDescriptorCalledWithArg string + ReadExtensionDescriptorReturn dist.ExtensionDescriptor + ReadExtensionDescriptorReturnError error +} + +// ReadExtensionDescriptor implements commands.PackageConfigReader. +func (r *FakePackageConfigReader) ReadExtensionDescriptor(path string) (dist.ExtensionDescriptor, error) { + r.ReadExtensionDescriptorCalledWithArg = path + + return r.ReadExtensionDescriptorReturn, r.ReadExtensionDescriptorReturnError } func (r *FakePackageConfigReader) Read(path string) (pubbldpkg.Config, error) { @@ -16,10 +32,20 @@ func (r *FakePackageConfigReader) Read(path string) (pubbldpkg.Config, error) { return r.ReadReturnConfig, r.ReadReturnError } +func (r *FakePackageConfigReader) ReadBuildpackDescriptor(path string) (dist.BuildpackDescriptor, error) { + r.ReadBuildpackDescriptorCalledWithArg = path + + return r.ReadBuildpackDescriptorReturn, r.ReadBuildpackDescriptorReturnError +} + func NewFakePackageConfigReader(ops ...func(*FakePackageConfigReader)) *FakePackageConfigReader { fakePackageConfigReader := &FakePackageConfigReader{ - ReadReturnConfig: pubbldpkg.Config{}, - ReadReturnError: nil, + ReadReturnConfig: pubbldpkg.Config{}, + ReadBuildpackDescriptorReturn: dist.BuildpackDescriptor{}, + ReadReturnError: nil, + ReadBuildpackDescriptorReturnError: nil, + ReadExtensionDescriptorReturn: dist.ExtensionDescriptor{}, + ReadExtensionDescriptorReturnError: nil, } for _, op := range ops { diff --git a/internal/commands/manifest.go b/internal/commands/manifest.go new file mode 100644 index 0000000000..6ec1367d81 --- /dev/null +++ b/internal/commands/manifest.go @@ -0,0 +1,27 @@ +package commands + +import ( + "github.com/spf13/cobra" + + "github.com/buildpacks/pack/pkg/logging" +) + +func NewManifestCommand(logger logging.Logger, client PackClient) *cobra.Command { + cmd := &cobra.Command{ + Use: "manifest", + Short: "Interact with image index", + RunE: nil, + } + + cmd.AddCommand(ManifestCreate(logger, client)) + cmd.AddCommand(ManifestAdd(logger, client)) + cmd.AddCommand(ManifestAnnotate(logger, client)) + cmd.AddCommand(ManifestDelete(logger, client)) + cmd.AddCommand(ManifestInspect(logger, client)) + cmd.AddCommand(ManifestPush(logger, client)) + cmd.AddCommand(ManifestRemove(logger, client)) + cmd.AddCommand(ManifestExists(logger, client)) + + AddHelpFlag(cmd, "manifest") + return cmd +} diff --git a/internal/commands/manifest_add.go b/internal/commands/manifest_add.go new file mode 100644 index 0000000000..d09b60e3bc --- /dev/null +++ b/internal/commands/manifest_add.go @@ -0,0 +1,67 @@ +package commands + +import ( + "errors" + + "github.com/spf13/cobra" + + "github.com/buildpacks/pack/pkg/client" + "github.com/buildpacks/pack/pkg/logging" +) + +// ManifestAddFlags define flags provided to the ManifestAdd +type ManifestAddFlags struct { + os, osVersion, osArch, osVariant string + osFeatures, features []string + annotations map[string]string + all bool +} + +// ManifestAdd modifies a manifest list (Image index) and add a new image to the list of manifests. +func ManifestAdd(logger logging.Logger, pack PackClient) *cobra.Command { + var flags ManifestAddFlags + + cmd := &cobra.Command{ + Use: "add [OPTIONS] [flags]", + Args: cobra.MatchAll(cobra.ExactArgs(2), cobra.OnlyValidArgs), + Short: "Add an image to a manifest list or image index.", + Example: `pack manifest add cnbs/sample-package:hello-multiarch-universe \ + cnbs/sample-package:hello-universe-riscv-linux`, + Long: `manifest add modifies a manifest list (Image index) and add a new image to the list of manifests.`, + RunE: logError(logger, func(cmd *cobra.Command, args []string) (err error) { + if err := validateManifestAddFlags(flags); err != nil { + return err + } + + return pack.AddManifest(cmd.Context(), args[0], args[1], client.ManifestAddOptions{ + OS: flags.os, + OSVersion: flags.osVersion, + OSArch: flags.osArch, + OSVariant: flags.osVariant, + OSFeatures: flags.osFeatures, + Features: flags.features, + Annotations: flags.annotations, + All: flags.all, + }) + }), + } + + cmd.Flags().BoolVar(&flags.all, "all", false, "add all of the contents to the local list (applies only if is an index)") + cmd.Flags().StringVar(&flags.os, "os", "", "Set the operating system") + cmd.Flags().StringVar(&flags.osArch, "arch", "", "Set the architecture") + cmd.Flags().StringVar(&flags.osVariant, "variant", "", "Set the architecture variant") + cmd.Flags().StringVar(&flags.osVersion, "os-version", "", "Set the os-version") + cmd.Flags().StringSliceVar(&flags.osFeatures, "os-features", make([]string, 0), "Set the OSFeatures") + cmd.Flags().StringSliceVar(&flags.features, "features", make([]string, 0), "Set the Features") + cmd.Flags().StringToStringVar(&flags.annotations, "annotations", make(map[string]string, 0), "Set the annotations") + + AddHelpFlag(cmd, "add") + return cmd +} + +func validateManifestAddFlags(flags ManifestAddFlags) error { + if (flags.os != "" && flags.osArch == "") || (flags.os == "" && flags.osArch != "") { + return errors.New("'os' or 'arch' is undefined") + } + return nil +} diff --git a/internal/commands/manifest_add_test.go b/internal/commands/manifest_add_test.go new file mode 100644 index 0000000000..82685cbd2a --- /dev/null +++ b/internal/commands/manifest_add_test.go @@ -0,0 +1,133 @@ +package commands_test + +import ( + "bytes" + "testing" + + "github.com/golang/mock/gomock" + "github.com/heroku/color" + "github.com/sclevine/spec" + "github.com/sclevine/spec/report" + "github.com/spf13/cobra" + + "github.com/buildpacks/pack/internal/commands" + "github.com/buildpacks/pack/internal/commands/testmocks" + "github.com/buildpacks/pack/pkg/logging" + h "github.com/buildpacks/pack/testhelpers" +) + +func TestManifestAddCommand(t *testing.T) { + color.Disable(true) + defer color.Disable(false) + + spec.Run(t, "Commands", testManifestAddCommand, spec.Random(), spec.Report(report.Terminal{})) +} + +func testManifestAddCommand(t *testing.T, when spec.G, it spec.S) { + var ( + command *cobra.Command + logger *logging.LogWithWriters + outBuf bytes.Buffer + mockController *gomock.Controller + mockClient *testmocks.MockPackClient + ) + + it.Before(func() { + logger = logging.NewLogWithWriters(&outBuf, &outBuf) + mockController = gomock.NewController(t) + mockClient = testmocks.NewMockPackClient(mockController) + + command = commands.ManifestAdd(logger, mockClient) + }) + it("should add image with current platform specs", func() { + prepareAddManifest(mockClient) + + command.SetArgs([]string{"some-index", "busybox:1.36-musl"}) + err := command.Execute() + h.AssertNil(t, err) + h.AssertEq(t, outBuf.String(), "") + }) + it("should add images with given platform", func() { + prepareAddManifest(mockClient) + + command.SetArgs([]string{ + "some-index", + "busybox:1.36-musl", + "--os", + "linux", + "--arch", + "arm", + "--variant", + "v6", + "--os-version", + "22.04", + }) + err := command.Execute() + h.AssertNil(t, err) + h.AssertEq(t, outBuf.String(), "") + }) + it("should add return an error when platform's os and arch not defined", func() { + prepareAddManifest(mockClient) + + command.SetArgs([]string{"some-index", "busybox:1.36-musl", "--os", "linux"}) + err := command.Execute() + h.AssertEq(t, err.Error(), "'os' or 'arch' is undefined") + h.AssertEq(t, outBuf.String(), "ERROR: 'os' or 'arch' is undefined\n") + }) + it("should add all images", func() { + prepareAddManifest(mockClient) + + command.SetArgs([]string{"some-index", "busybox:1.36-musl", "--all"}) + err := command.Execute() + h.AssertNil(t, err) + h.AssertEq(t, outBuf.String(), "") + }) + it("should return an error when features defined invalidly", func() { + prepareAddManifest(mockClient) + + command.SetArgs([]string{"some-index", "busybox:1.36-musl", "--features"}) + err := command.Execute() + h.AssertEq(t, err.Error(), "flag needs an argument: --features") + }) + it("should return an error when osFeatures defined invalidly", func() { + prepareAddManifest(mockClient) + + command.SetArgs([]string{"some-index", "busybox:1.36-musl", "--os-features"}) + err := command.Execute() + h.AssertEq(t, err.Error(), "flag needs an argument: --os-features") + }) + it("should return an error when invalid arg passed", func() { + prepareAddManifest(mockClient) + + command.SetArgs([]string{"some-index", "busybox:1.36-musl", "--urls"}) + err := command.Execute() + h.AssertEq(t, err.Error(), "unknown flag: --urls") + }) + it("should return an error when annotations defined invalidly", func() { + prepareAddManifest(mockClient) + + command.SetArgs([]string{"some-index", "busybox:1.36-musl", "--annotations", "some-key"}) + err := command.Execute() + h.AssertEq(t, err.Error(), `invalid argument "some-key" for "--annotations" flag: some-key must be formatted as key=value`) + }) + it("should have help flag", func() { + prepareAddManifest(mockClient) + + command.SetArgs([]string{"--help"}) + h.AssertNilE(t, command.Execute()) + h.AssertEq(t, outBuf.String(), "") + }) +} + +func prepareAddManifest(mockClient *testmocks.MockPackClient) { + mockClient. + EXPECT(). + AddManifest( + gomock.Any(), + gomock.Any(), + gomock.Any(), + gomock.Any(), + ). + AnyTimes(). + Return(nil) +} diff --git a/internal/commands/manifest_annotate.go b/internal/commands/manifest_annotate.go new file mode 100644 index 0000000000..4dbf65eccf --- /dev/null +++ b/internal/commands/manifest_annotate.go @@ -0,0 +1,65 @@ +package commands + +import ( + "github.com/pkg/errors" + "github.com/spf13/cobra" + + "github.com/buildpacks/pack/pkg/client" + "github.com/buildpacks/pack/pkg/logging" +) + +// ManifestAnnotateFlags define flags provided to the ManifestAnnotate +type ManifestAnnotateFlags struct { + os, arch, variant, osVersion string + features, osFeatures, urls []string + annotations map[string]string +} + +// ManifestAnnotate modifies a manifest list (Image index) and update the platform information for an image included in the manifest list. +func ManifestAnnotate(logger logging.Logger, pack PackClient) *cobra.Command { + var flags ManifestAnnotateFlags + + cmd := &cobra.Command{ + Use: "annotate [OPTIONS] [flags]", + Args: cobra.MatchAll(cobra.ExactArgs(2), cobra.OnlyValidArgs), + Short: "Add or update information about an entry in a manifest list or image index.", + Example: `pack manifest annotate cnbs/sample-package:hello-universe-multiarch \ + cnbs/sample-package:hello-universe --arch amd64`, + Long: `manifest annotate modifies a manifest list (Image index) and update the platform information for an image included in the manifest list.`, + RunE: logError(logger, func(cmd *cobra.Command, args []string) (err error) { + if err := validateManifestAnnotateFlags(flags); err != nil { + return err + } + + return pack.AnnotateManifest(cmd.Context(), args[0], args[1], client.ManifestAnnotateOptions{ + OS: flags.os, + OSVersion: flags.osVersion, + OSArch: flags.arch, + OSVariant: flags.variant, + OSFeatures: flags.osFeatures, + Features: flags.features, + URLs: flags.urls, + Annotations: flags.annotations, + }) + }), + } + + cmd.Flags().StringVar(&flags.os, "os", "", "Set the architecture") + cmd.Flags().StringVar(&flags.arch, "arch", "", "Set the architecture") + cmd.Flags().StringVar(&flags.variant, "variant", "", "Set the architecture") + cmd.Flags().StringVar(&flags.osVersion, "os-version", "", "override the os `version` of the specified image") + cmd.Flags().StringSliceVar(&flags.features, "features", make([]string, 0), "override the `features` of the specified image") + cmd.Flags().StringSliceVar(&flags.urls, "urls", make([]string, 0), "override the `urls` of the specified image") + cmd.Flags().StringSliceVar(&flags.osFeatures, "os-features", make([]string, 0), "override the os `features` of the specified image") + cmd.Flags().StringToStringVar(&flags.annotations, "annotations", make(map[string]string, 0), "set an `annotation` for the specified image") + + AddHelpFlag(cmd, "annotate") + return cmd +} + +func validateManifestAnnotateFlags(flags ManifestAnnotateFlags) error { + if (flags.os != "" && flags.arch == "") || (flags.os == "" && flags.arch != "") { + return errors.New("'os' or 'arch' is undefined") + } + return nil +} diff --git a/internal/commands/manifest_annotate_test.go b/internal/commands/manifest_annotate_test.go new file mode 100644 index 0000000000..5c82ca90dc --- /dev/null +++ b/internal/commands/manifest_annotate_test.go @@ -0,0 +1,116 @@ +package commands_test + +import ( + "bytes" + "testing" + + "github.com/golang/mock/gomock" + "github.com/heroku/color" + "github.com/sclevine/spec" + "github.com/sclevine/spec/report" + "github.com/spf13/cobra" + + "github.com/buildpacks/pack/internal/commands" + "github.com/buildpacks/pack/internal/commands/testmocks" + "github.com/buildpacks/pack/pkg/logging" + h "github.com/buildpacks/pack/testhelpers" +) + +func TestManifestAnnotationsCommand(t *testing.T) { + color.Disable(true) + defer color.Disable(false) + + spec.Run(t, "Commands", testManifestAnnotateCommand, spec.Random(), spec.Report(report.Terminal{})) +} + +func testManifestAnnotateCommand(t *testing.T, when spec.G, it spec.S) { + var ( + command *cobra.Command + logger *logging.LogWithWriters + outBuf bytes.Buffer + mockController *gomock.Controller + mockClient *testmocks.MockPackClient + ) + + it.Before(func() { + logger = logging.NewLogWithWriters(&outBuf, &outBuf) + mockController = gomock.NewController(t) + mockClient = testmocks.NewMockPackClient(mockController) + + command = commands.ManifestAnnotate(logger, mockClient) + }) + it("should annotate images with given flags", func() { + prepareAnnotateManifest(mockClient) + + command.SetArgs([]string{ + "some-index", + "busybox@sha256:6457d53fb065d6f250e1504b9bc42d5b6c65941d57532c072d929dd0628977d0", + "--os", + "linux", + "--arch", + "arm", + "--variant", + "v6", + "--os-version", + "22.04", + }) + h.AssertNil(t, command.Execute()) + h.AssertEq(t, outBuf.String(), "") + }) + it("should return an error when platform's os and arch not defined", func() { + prepareAnnotateManifest(mockClient) + + command.SetArgs([]string{"some-index", "busybox@sha256:6457d53fb065d6f250e1504b9bc42d5b6c65941d57532c072d929dd0628977d0", "--os", "linux"}) + err := command.Execute() + h.AssertEq(t, err.Error(), "'os' or 'arch' is undefined") + h.AssertEq(t, outBuf.String(), "ERROR: 'os' or 'arch' is undefined\n") + }) + it("should return an error when features defined invalidly", func() { + prepareAnnotateManifest(mockClient) + + command.SetArgs([]string{"some-index", "busybox@sha256:6457d53fb065d6f250e1504b9bc42d5b6c65941d57532c072d929dd0628977d0", "--features"}) + err := command.Execute() + h.AssertEq(t, err.Error(), "flag needs an argument: --features") + }) + it("should return an error when osFeatures defined invalidly", func() { + prepareAnnotateManifest(mockClient) + + command.SetArgs([]string{"some-index", "busybox@sha256:6457d53fb065d6f250e1504b9bc42d5b6c65941d57532c072d929dd0628977d0", "--os-features"}) + err := command.Execute() + h.AssertEq(t, err.Error(), "flag needs an argument: --os-features") + }) + it("should return an error when urls defined invalidly", func() { + prepareAnnotateManifest(mockClient) + + command.SetArgs([]string{"some-index", "busybox@sha256:6457d53fb065d6f250e1504b9bc42d5b6c65941d57532c072d929dd0628977d0", "--urls"}) + err := command.Execute() + h.AssertEq(t, err.Error(), "flag needs an argument: --urls") + }) + it("should return an error when annotations defined invalidly", func() { + prepareAnnotateManifest(mockClient) + + command.SetArgs([]string{"some-index", "busybox@sha256:6457d53fb065d6f250e1504b9bc42d5b6c65941d57532c072d929dd0628977d0", "--annotations", "some-key"}) + err := command.Execute() + h.AssertEq(t, err.Error(), `invalid argument "some-key" for "--annotations" flag: some-key must be formatted as key=value`) + }) + it("should have help flag", func() { + prepareAnnotateManifest(mockClient) + + command.SetArgs([]string{"--help"}) + h.AssertNilE(t, command.Execute()) + h.AssertEq(t, outBuf.String(), "") + }) +} + +func prepareAnnotateManifest(mockClient *testmocks.MockPackClient) { + mockClient. + EXPECT(). + AnnotateManifest( + gomock.Any(), + gomock.Any(), + gomock.Any(), + gomock.Any(), + ). + AnyTimes(). + Return(nil) +} diff --git a/internal/commands/manifest_create.go b/internal/commands/manifest_create.go new file mode 100644 index 0000000000..982d81459b --- /dev/null +++ b/internal/commands/manifest_create.go @@ -0,0 +1,66 @@ +package commands + +import ( + "github.com/pkg/errors" + "github.com/spf13/cobra" + + "github.com/buildpacks/pack/pkg/client" + "github.com/buildpacks/pack/pkg/logging" +) + +// ManifestCreateFlags define flags provided to the ManifestCreate +type ManifestCreateFlags struct { + format, os, arch string + insecure, publish, all bool +} + +// ManifestCreate creates an image-index/image-list for a multi-arch image +func ManifestCreate(logger logging.Logger, pack PackClient) *cobra.Command { + var flags ManifestCreateFlags + + cmd := &cobra.Command{ + Use: "create [ ... ] [flags]", + Args: cobra.MatchAll(cobra.MinimumNArgs(2), cobra.OnlyValidArgs), + Short: "Create a manifest list or image index.", + Example: `pack manifest create cnbs/sample-package:hello-multiarch-universe \ + cnbs/sample-package:hello-universe \ + cnbs/sample-package:hello-universe-windows`, + Long: `Generate manifest list for a multi-arch image which will be stored locally for manipulating images within index`, + RunE: logError(logger, func(cmd *cobra.Command, args []string) error { + if err := validateManifestCreateFlags(flags); err != nil { + return err + } + + return pack.CreateManifest( + cmd.Context(), + args[0], + args[1:], + client.CreateManifestOptions{ + Format: flags.format, + Insecure: flags.insecure, + Publish: flags.publish, + All: flags.all, + }, + ) + }), + } + + cmdFlags := cmd.Flags() + + cmdFlags.StringVarP(&flags.format, "format", "f", "v2s2", "Format to save image index as ('OCI' or 'V2S2')") + cmdFlags.StringVar(&flags.os, "os", "", "If any of the specified images is a list/index, choose the one for `os`") + cmdFlags.StringVar(&flags.arch, "arch", "", "If any of the specified images is a list/index, choose the one for `arch`") + cmdFlags.BoolVar(&flags.insecure, "insecure", false, "Allow publishing to insecure registry") + cmdFlags.BoolVar(&flags.publish, "publish", false, "Publish to registry") + cmdFlags.BoolVar(&flags.all, "all", false, "Add all of the list's images if the images to add are lists/index") + + AddHelpFlag(cmd, "create") + return cmd +} + +func validateManifestCreateFlags(flags ManifestCreateFlags) error { + if (flags.os != "" && flags.arch == "") || (flags.os == "" && flags.arch != "") { + return errors.New("'os' or 'arch' is undefined") + } + return nil +} diff --git a/internal/commands/manifest_create_test.go b/internal/commands/manifest_create_test.go new file mode 100644 index 0000000000..dd3efbb2ce --- /dev/null +++ b/internal/commands/manifest_create_test.go @@ -0,0 +1,88 @@ +package commands_test + +import ( + "bytes" + "testing" + + "github.com/golang/mock/gomock" + "github.com/heroku/color" + "github.com/sclevine/spec" + "github.com/sclevine/spec/report" + "github.com/spf13/cobra" + + "github.com/buildpacks/pack/internal/commands" + "github.com/buildpacks/pack/internal/commands/testmocks" + "github.com/buildpacks/pack/pkg/logging" + h "github.com/buildpacks/pack/testhelpers" +) + +func TestManifestCreateCommand(t *testing.T) { + color.Disable(true) + defer color.Disable(false) + + spec.Run(t, "Commands", testManifestCreateCommand, spec.Random(), spec.Report(report.Terminal{})) +} + +func testManifestCreateCommand(t *testing.T, when spec.G, it spec.S) { + var ( + command *cobra.Command + logger *logging.LogWithWriters + outBuf bytes.Buffer + mockController *gomock.Controller + mockClient *testmocks.MockPackClient + ) + + it.Before(func() { + logger = logging.NewLogWithWriters(&outBuf, &outBuf) + mockController = gomock.NewController(t) + mockClient = testmocks.NewMockPackClient(mockController) + + command = commands.ManifestCreate(logger, mockClient) + }) + it("should annotate images with given flags", func() { + prepareCreateManifest(mockClient) + + command.SetArgs([]string{ + "some-index", + "busybox@sha256:6457d53fb065d6f250e1504b9bc42d5b6c65941d57532c072d929dd0628977d0", + "--os", + "linux", + "--arch", + "arm", + "--format", + "v2s2", + "--insecure", + "--publish", + }) + h.AssertNil(t, command.Execute()) + h.AssertEq(t, outBuf.String(), "") + }) + it("should return an error when platform's os and arch not defined", func() { + prepareCreateManifest(mockClient) + + command.SetArgs([]string{"some-index", "busybox@sha256:6457d53fb065d6f250e1504b9bc42d5b6c65941d57532c072d929dd0628977d0", "--os", "linux"}) + err := command.Execute() + h.AssertEq(t, err.Error(), "'os' or 'arch' is undefined") + h.AssertEq(t, outBuf.String(), "ERROR: 'os' or 'arch' is undefined\n") + }) + it("should have help flag", func() { + prepareCreateManifest(mockClient) + + command.SetArgs([]string{"--help"}) + h.AssertNilE(t, command.Execute()) + h.AssertEq(t, outBuf.String(), "") + }) +} + +func prepareCreateManifest(mockClient *testmocks.MockPackClient) { + mockClient. + EXPECT(). + CreateManifest( + gomock.Any(), + gomock.Any(), + gomock.Any(), + gomock.Any(), + ). + AnyTimes(). + Return(nil) +} diff --git a/internal/commands/manifest_exists.go b/internal/commands/manifest_exists.go new file mode 100644 index 0000000000..79a0e770c8 --- /dev/null +++ b/internal/commands/manifest_exists.go @@ -0,0 +1,24 @@ +package commands + +import ( + "github.com/spf13/cobra" + + "github.com/buildpacks/pack/pkg/logging" +) + +// ManifestExists checks if a manifest list exists in local storage +func ManifestExists(logger logging.Logger, pack PackClient) *cobra.Command { + cmd := &cobra.Command{ + Use: "exists [manifest-list]", + Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs), + Short: "Check if the given manifest list exists in local storage", + Example: `pack manifest exists cnbs/sample-package:hello-multiarch-universe`, + Long: `Checks if a manifest list exists in local storage`, + RunE: logError(logger, func(cmd *cobra.Command, args []string) error { + return pack.ExistsManifest(cmd.Context(), args[0]) + }), + } + + AddHelpFlag(cmd, "exists") + return cmd +} diff --git a/internal/commands/manifest_exists_test.go b/internal/commands/manifest_exists_test.go new file mode 100644 index 0000000000..2811992140 --- /dev/null +++ b/internal/commands/manifest_exists_test.go @@ -0,0 +1,88 @@ +package commands_test + +import ( + "bytes" + "testing" + + "github.com/golang/mock/gomock" + "github.com/heroku/color" + "github.com/pkg/errors" + "github.com/sclevine/spec" + "github.com/sclevine/spec/report" + "github.com/spf13/cobra" + + "github.com/buildpacks/pack/internal/commands" + "github.com/buildpacks/pack/internal/commands/testmocks" + "github.com/buildpacks/pack/pkg/logging" + h "github.com/buildpacks/pack/testhelpers" +) + +func TestManifestExistsCommand(t *testing.T) { + color.Disable(true) + defer color.Disable(false) + + spec.Run(t, "Commands", testManifestExistsCommand, spec.Random(), spec.Report(report.Terminal{})) +} + +func testManifestExistsCommand(t *testing.T, when spec.G, it spec.S) { + var ( + command *cobra.Command + logger *logging.LogWithWriters + outBuf bytes.Buffer + mockController *gomock.Controller + mockClient *testmocks.MockPackClient + ) + + it.Before(func() { + logger = logging.NewLogWithWriters(&outBuf, &outBuf) + mockController = gomock.NewController(t) + mockClient = testmocks.NewMockPackClient(mockController) + + command = commands.ManifestExists(logger, mockClient) + }) + it("should annotate images with given flags", func() { + prepareExistsManifest(mockClient) + + command.SetArgs([]string{ + "some-index", + }) + h.AssertNil(t, command.Execute()) + h.AssertEq(t, outBuf.String(), "") + }) + it("should return an error when index doesn't exists", func() { + prepareNotExistsManifest(mockClient) + + command.SetArgs([]string{"some-other-index"}) + err := command.Execute() + h.AssertEq(t, err.Error(), "no index found with given name") + }) + it("should have help flag", func() { + prepareExistsManifest(mockClient) + + command.SetArgs([]string{"--help"}) + h.AssertNilE(t, command.Execute()) + h.AssertEq(t, outBuf.String(), "") + }) +} + +func prepareExistsManifest(mockClient *testmocks.MockPackClient) { + mockClient. + EXPECT(). + ExistsManifest( + gomock.Any(), + gomock.Any(), + ). + AnyTimes(). + Return(nil) +} + +func prepareNotExistsManifest(mockClient *testmocks.MockPackClient) { + mockClient. + EXPECT(). + ExistsManifest( + gomock.Any(), + gomock.Any(), + ). + AnyTimes(). + Return(errors.New("no index found with given name")) +} diff --git a/internal/commands/manifest_inspect.go b/internal/commands/manifest_inspect.go new file mode 100644 index 0000000000..6a61934787 --- /dev/null +++ b/internal/commands/manifest_inspect.go @@ -0,0 +1,25 @@ +package commands + +import ( + "github.com/spf13/cobra" + + "github.com/buildpacks/pack/pkg/logging" +) + +// ManifestInspect shows the manifest information stored in local storage +func ManifestInspect(logger logging.Logger, pack PackClient) *cobra.Command { + cmd := &cobra.Command{ + Use: "inspect ", + Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs), + Short: "Display a manifest list or image index.", + Example: `pack manifest inspect cnbs/sample-builder:multiarch`, + Long: `manifest inspect shows the manifest information stored in local storage. + The inspect command will help users to view how their local manifest list looks like`, + RunE: logError(logger, func(cmd *cobra.Command, args []string) error { + return pack.InspectManifest(cmd.Context(), args[0]) + }), + } + + AddHelpFlag(cmd, "inspect") + return cmd +} diff --git a/internal/commands/manifest_inspect_test.go b/internal/commands/manifest_inspect_test.go new file mode 100644 index 0000000000..18088fa90f --- /dev/null +++ b/internal/commands/manifest_inspect_test.go @@ -0,0 +1,75 @@ +package commands_test + +import ( + "bytes" + "testing" + + "github.com/golang/mock/gomock" + "github.com/heroku/color" + "github.com/sclevine/spec" + "github.com/sclevine/spec/report" + "github.com/spf13/cobra" + + "github.com/buildpacks/pack/internal/commands" + "github.com/buildpacks/pack/internal/commands/testmocks" + "github.com/buildpacks/pack/pkg/logging" + h "github.com/buildpacks/pack/testhelpers" +) + +func TestManifestInspectCommand(t *testing.T) { + color.Disable(true) + defer color.Disable(false) + + spec.Run(t, "Commands", testManifestInspectCommand, spec.Random(), spec.Report(report.Terminal{})) +} + +func testManifestInspectCommand(t *testing.T, when spec.G, it spec.S) { + var ( + command *cobra.Command + logger *logging.LogWithWriters + outBuf bytes.Buffer + mockController *gomock.Controller + mockClient *testmocks.MockPackClient + ) + + it.Before(func() { + logger = logging.NewLogWithWriters(&outBuf, &outBuf) + mockController = gomock.NewController(t) + mockClient = testmocks.NewMockPackClient(mockController) + + command = commands.ManifestInspect(logger, mockClient) + }) + it("should annotate images with given flags", func() { + prepareInspectManifest(mockClient) + + command.SetArgs([]string{ + "some-index", + }) + h.AssertNil(t, command.Execute()) + }) + it("should return an error when index not passed", func() { + prepareInspectManifest(mockClient) + + command.SetArgs([]string(nil)) + err := command.Execute() + h.AssertNotNil(t, err) + }) + it("should have help flag", func() { + prepareInspectManifest(mockClient) + + command.SetArgs([]string{"--help"}) + h.AssertNilE(t, command.Execute()) + h.AssertEq(t, outBuf.String(), "") + }) +} + +func prepareInspectManifest(mockClient *testmocks.MockPackClient) { + mockClient. + EXPECT(). + InspectManifest( + gomock.Any(), + gomock.Any(), + ). + AnyTimes(). + Return(nil) +} diff --git a/internal/commands/manifest_push.go b/internal/commands/manifest_push.go new file mode 100644 index 0000000000..a62abec273 --- /dev/null +++ b/internal/commands/manifest_push.go @@ -0,0 +1,42 @@ +package commands + +import ( + "github.com/spf13/cobra" + + "github.com/buildpacks/pack/pkg/client" + "github.com/buildpacks/pack/pkg/logging" +) + +// ManifestPushFlags define flags provided to the ManifestPush +type ManifestPushFlags struct { + format string + insecure, purge bool +} + +// ManifestPush pushes a manifest list (Image index) to a registry. +func ManifestPush(logger logging.Logger, pack PackClient) *cobra.Command { + var flags ManifestPushFlags + + cmd := &cobra.Command{ + Use: "push [OPTIONS] [flags]", + Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs), + Short: "Push a manifest list or image index to a registry.", + Example: `pack manifest push cnbs/sample-package:hello-multiarch-universe`, + Long: `manifest push pushes a manifest list (Image index) to a registry. + Once a manifest list is ready to be published into the registry, the push command can be used`, + RunE: logError(logger, func(cmd *cobra.Command, args []string) error { + return pack.PushManifest(cmd.Context(), args[0], client.PushManifestOptions{ + Format: flags.format, + Insecure: flags.insecure, + Purge: flags.purge, + }) + }), + } + + cmd.Flags().StringVarP(&flags.format, "format", "f", "", "Format to save image index as ('OCI' or 'V2S2')") + cmd.Flags().BoolVar(&flags.insecure, "insecure", false, "Allow publishing to insecure registry") + cmd.Flags().BoolVar(&flags.purge, "purge", false, "Delete the manifest list or image index from local storage if pushing succeeds") + + AddHelpFlag(cmd, "push") + return cmd +} diff --git a/internal/commands/manifest_push_test.go b/internal/commands/manifest_push_test.go new file mode 100644 index 0000000000..aa67b6872b --- /dev/null +++ b/internal/commands/manifest_push_test.go @@ -0,0 +1,93 @@ +package commands_test + +import ( + "bytes" + "errors" + "testing" + + "github.com/golang/mock/gomock" + "github.com/heroku/color" + "github.com/sclevine/spec" + "github.com/sclevine/spec/report" + "github.com/spf13/cobra" + + "github.com/buildpacks/pack/internal/commands" + "github.com/buildpacks/pack/internal/commands/testmocks" + "github.com/buildpacks/pack/pkg/logging" + h "github.com/buildpacks/pack/testhelpers" +) + +func TestManifestPushCommand(t *testing.T) { + color.Disable(true) + defer color.Disable(false) + + spec.Run(t, "Commands", testManifestPushCommand, spec.Random(), spec.Report(report.Terminal{})) +} + +func testManifestPushCommand(t *testing.T, when spec.G, it spec.S) { + var ( + command *cobra.Command + logger *logging.LogWithWriters + outBuf bytes.Buffer + mockController *gomock.Controller + mockClient *testmocks.MockPackClient + ) + + it.Before(func() { + logger = logging.NewLogWithWriters(&outBuf, &outBuf) + mockController = gomock.NewController(t) + mockClient = testmocks.NewMockPackClient(mockController) + + command = commands.ManifestPush(logger, mockClient) + }) + it("should annotate images with given flags", func() { + preparePushManifest(mockClient) + + command.SetArgs([]string{ + "some-index", + "-f", + "v2s2", + "--purge", + "--insecure", + }) + h.AssertNil(t, command.Execute()) + }) + it("should return an error when index not exists locally", func() { + preparePushManifestWithError(mockClient) + + command.SetArgs([]string{"some-index"}) + err := command.Execute() + h.AssertNotNil(t, err) + }) + it("should have help flag", func() { + preparePushManifest(mockClient) + + command.SetArgs([]string{"--help"}) + h.AssertNilE(t, command.Execute()) + h.AssertEq(t, outBuf.String(), "") + }) +} + +func preparePushManifest(mockClient *testmocks.MockPackClient) { + mockClient. + EXPECT(). + PushManifest( + gomock.Any(), + gomock.Any(), + gomock.Any(), + ). + AnyTimes(). + Return(nil) +} + +func preparePushManifestWithError(mockClient *testmocks.MockPackClient) { + mockClient. + EXPECT(). + PushManifest( + gomock.Any(), + gomock.Any(), + gomock.Any(), + ). + AnyTimes(). + Return(errors.New("unable to push Image")) +} diff --git a/internal/commands/manifest_remove.go b/internal/commands/manifest_remove.go new file mode 100644 index 0000000000..7a4f99f85c --- /dev/null +++ b/internal/commands/manifest_remove.go @@ -0,0 +1,49 @@ +package commands + +import ( + "github.com/pkg/errors" + "github.com/spf13/cobra" + + "github.com/buildpacks/pack/pkg/logging" +) + +// ManifestDelete deletes one or more manifest lists from local storage +func ManifestDelete(logger logging.Logger, pack PackClient) *cobra.Command { + cmd := &cobra.Command{ + Use: "remove [manifest-list] [manifest-list...]", + Args: cobra.MatchAll(cobra.MinimumNArgs(1), cobra.OnlyValidArgs), + Short: "Remove an image from a manifest list or image index.", + Example: `pack manifest remove cnbs/sample-package:hello-multiarch-universe`, + Long: `Delete one or more manifest lists from local storage. + When a manifest list exits locally, users can remove existing images from a manifest list`, + RunE: logError(logger, func(cmd *cobra.Command, args []string) error { + return NewErrors(pack.DeleteManifest(cmd.Context(), args)).Error() + }), + } + + AddHelpFlag(cmd, "remove") + return cmd +} + +type Errors struct { + errs []error +} + +func NewErrors(errs []error) Errors { + return Errors{ + errs: errs, + } +} + +func (e Errors) Error() error { + var errMsg string + if len(e.errs) == 0 { + return nil + } + + for _, err := range e.errs { + errMsg += err.Error() + } + + return errors.New(errMsg) +} diff --git a/internal/commands/manifest_remove_test.go b/internal/commands/manifest_remove_test.go new file mode 100644 index 0000000000..f09da3cca0 --- /dev/null +++ b/internal/commands/manifest_remove_test.go @@ -0,0 +1,89 @@ +package commands_test + +import ( + "bytes" + "errors" + "testing" + + "github.com/golang/mock/gomock" + "github.com/heroku/color" + "github.com/sclevine/spec" + "github.com/sclevine/spec/report" + "github.com/spf13/cobra" + + "github.com/buildpacks/pack/internal/commands" + "github.com/buildpacks/pack/internal/commands/testmocks" + "github.com/buildpacks/pack/pkg/logging" + h "github.com/buildpacks/pack/testhelpers" +) + +func TestManifestDeleteCommand(t *testing.T) { + color.Disable(true) + defer color.Disable(false) + + spec.Run(t, "Commands", testManifestDeleteCommand, spec.Random(), spec.Report(report.Terminal{})) +} + +func testManifestDeleteCommand(t *testing.T, when spec.G, it spec.S) { + var ( + command *cobra.Command + logger *logging.LogWithWriters + outBuf bytes.Buffer + mockController *gomock.Controller + mockClient *testmocks.MockPackClient + ) + + it.Before(func() { + logger = logging.NewLogWithWriters(&outBuf, &outBuf) + mockController = gomock.NewController(t) + mockClient = testmocks.NewMockPackClient(mockController) + + command = commands.ManifestDelete(logger, mockClient) + }) + it("should delete index", func() { + prepareDeleteManifest(mockClient) + + command.SetArgs([]string{ + "some-index", + }) + h.AssertNil(t, command.Execute()) + }) + it("should have help flag", func() { + command.SetArgs([]string{"--help"}) + h.AssertNilE(t, command.Execute()) + h.AssertEq(t, outBuf.String(), "") + }) + it("should return an error", func() { + prepareDeleteManifest(mockClient) + + command.SetArgs([]string{"some-index"}) + err := command.Execute() + h.AssertNil(t, err) + + err = command.Execute() + h.AssertNotNil(t, err) + }) +} + +func prepareDeleteManifest(mockClient *testmocks.MockPackClient) { + mockClient. + EXPECT(). + DeleteManifest( + gomock.Any(), + gomock.Any(), + ). + AnyTimes(). + After( + mockClient. + EXPECT(). + DeleteManifest( + gomock.Any(), + gomock.Any(), + ). + Times(1). + Return(nil), + ). + Return([]error{ + errors.New("image index doesn't exists"), + }) +} diff --git a/internal/commands/manifest_rm.go b/internal/commands/manifest_rm.go new file mode 100644 index 0000000000..33bf998576 --- /dev/null +++ b/internal/commands/manifest_rm.go @@ -0,0 +1,27 @@ +package commands + +import ( + "github.com/spf13/cobra" + + "github.com/buildpacks/pack/pkg/logging" +) + +// ManifestRemove will remove the specified image manifest if it is already referenced in the index +func ManifestRemove(logger logging.Logger, pack PackClient) *cobra.Command { + cmd := &cobra.Command{ + Use: "rm [manifest-list] [manifest] [manifest...] [flags]", + Args: cobra.MatchAll(cobra.MinimumNArgs(2), cobra.OnlyValidArgs), + Short: "Remove manifest list or image index from local storage.", + Example: `pack manifest rm cnbs/sample-package:hello-multiarch-universe \ + cnbs/sample-package@sha256:42969d8175941c21ab739d3064e9cd7e93c972a0a6050602938ed501d156e452`, + Long: `manifest rm will remove the specified image manifest if it is already referenced in the index. + User must pass digest of the image in oder to delete it from index. + Sometimes users can just experiment with the feature locally and they want to discard all the local information created by pack. 'rm' command just delete the local manifest list`, + RunE: logError(logger, func(cmd *cobra.Command, args []string) error { + return NewErrors(pack.RemoveManifest(cmd.Context(), args[0], args[1:])).Error() + }), + } + + AddHelpFlag(cmd, "rm") + return cmd +} diff --git a/internal/commands/manifest_rm_test.go b/internal/commands/manifest_rm_test.go new file mode 100644 index 0000000000..30c515484a --- /dev/null +++ b/internal/commands/manifest_rm_test.go @@ -0,0 +1,92 @@ +package commands_test + +import ( + "bytes" + "errors" + "testing" + + "github.com/golang/mock/gomock" + "github.com/heroku/color" + "github.com/sclevine/spec" + "github.com/sclevine/spec/report" + "github.com/spf13/cobra" + + "github.com/buildpacks/pack/internal/commands" + "github.com/buildpacks/pack/internal/commands/testmocks" + "github.com/buildpacks/pack/pkg/logging" + h "github.com/buildpacks/pack/testhelpers" +) + +func TestManifestRemoveCommand(t *testing.T) { + color.Disable(true) + defer color.Disable(false) + + spec.Run(t, "Commands", testManifestRemoveCommand, spec.Random(), spec.Report(report.Terminal{})) +} + +func testManifestRemoveCommand(t *testing.T, when spec.G, it spec.S) { + var ( + command *cobra.Command + logger *logging.LogWithWriters + outBuf bytes.Buffer + mockController *gomock.Controller + mockClient *testmocks.MockPackClient + ) + + it.Before(func() { + logger = logging.NewLogWithWriters(&outBuf, &outBuf) + mockController = gomock.NewController(t) + mockClient = testmocks.NewMockPackClient(mockController) + + command = commands.ManifestRemove(logger, mockClient) + }) + it("should remove index", func() { + prepareRemoveManifest(mockClient) + + command.SetArgs([]string{ + "some-index", + "some-image", + }) + h.AssertNil(t, command.Execute()) + }) + it("should return an error", func() { + prepareRemoveManifest(mockClient) + + command.SetArgs([]string{"some-index", "some-image"}) + err := command.Execute() + h.AssertNil(t, err) + + err = command.Execute() + h.AssertNotNil(t, err) + }) + it("should have help flag", func() { + command.SetArgs([]string{"--help"}) + h.AssertNilE(t, command.Execute()) + h.AssertEq(t, outBuf.String(), "") + }) +} + +func prepareRemoveManifest(mockClient *testmocks.MockPackClient) { + mockClient. + EXPECT(). + RemoveManifest( + gomock.Any(), + gomock.Any(), + gomock.Any(), + ). + AnyTimes(). + After( + mockClient. + EXPECT(). + RemoveManifest( + gomock.Any(), + gomock.Any(), + gomock.Any(), + ). + Times(1). + Return(nil), + ). + Return([]error{ + errors.New("image doesn't exists"), + }) +} diff --git a/internal/commands/manifest_test.go b/internal/commands/manifest_test.go new file mode 100644 index 0000000000..067271ef5d --- /dev/null +++ b/internal/commands/manifest_test.go @@ -0,0 +1,55 @@ +package commands_test + +import ( + "bytes" + "testing" + + "github.com/golang/mock/gomock" + "github.com/heroku/color" + "github.com/sclevine/spec" + "github.com/sclevine/spec/report" + "github.com/spf13/cobra" + + "github.com/buildpacks/pack/internal/commands" + "github.com/buildpacks/pack/internal/commands/testmocks" + "github.com/buildpacks/pack/pkg/logging" + h "github.com/buildpacks/pack/testhelpers" +) + +func TestNewManifestCommand(t *testing.T) { + color.Disable(true) + defer color.Disable(false) + + spec.Run(t, "Commands", testNewManifestCommand, spec.Random(), spec.Report(report.Terminal{})) +} + +func testNewManifestCommand(t *testing.T, when spec.G, it spec.S) { + var ( + command *cobra.Command + logger *logging.LogWithWriters + outBuf bytes.Buffer + mockController *gomock.Controller + mockClient *testmocks.MockPackClient + ) + + it.Before(func() { + logger = logging.NewLogWithWriters(&outBuf, &outBuf) + mockController = gomock.NewController(t) + mockClient = testmocks.NewMockPackClient(mockController) + + command = commands.NewManifestCommand(logger, mockClient) + command.SetOut(logging.GetWriterForLevel(logger, logging.InfoLevel)) + }) + it("should have help flag", func() { + command.SetArgs([]string{}) + err := command.Execute() + h.AssertNilE(t, err) + + output := outBuf.String() + h.AssertContains(t, output, "Interact with image index") + h.AssertContains(t, output, "Usage:") + for _, command := range []string{"create", "add", "annotate", "inspect", "exists", "remove", "rm"} { + h.AssertContains(t, output, command) + } + }) +} diff --git a/internal/commands/testmocks/mock_pack_client.go b/internal/commands/testmocks/mock_pack_client.go index f49b92def9..33bd3f1e24 100644 --- a/internal/commands/testmocks/mock_pack_client.go +++ b/internal/commands/testmocks/mock_pack_client.go @@ -36,6 +36,34 @@ func (m *MockPackClient) EXPECT() *MockPackClientMockRecorder { return m.recorder } +// AddManifest mocks base method. +func (m *MockPackClient) AddManifest(arg0 context.Context, arg1, arg2 string, arg3 client.ManifestAddOptions) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AddManifest", arg0, arg1, arg2, arg3) + ret0, _ := ret[0].(error) + return ret0 +} + +// AddManifest indicates an expected call of AddManifest. +func (mr *MockPackClientMockRecorder) AddManifest(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddManifest", reflect.TypeOf((*MockPackClient)(nil).AddManifest), arg0, arg1, arg2, arg3) +} + +// AnnotateManifest mocks base method. +func (m *MockPackClient) AnnotateManifest(arg0 context.Context, arg1, arg2 string, arg3 client.ManifestAnnotateOptions) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AnnotateManifest", arg0, arg1, arg2, arg3) + ret0, _ := ret[0].(error) + return ret0 +} + +// AnnotateManifest indicates an expected call of AnnotateManifest. +func (mr *MockPackClientMockRecorder) AnnotateManifest(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AnnotateManifest", reflect.TypeOf((*MockPackClient)(nil).AnnotateManifest), arg0, arg1, arg2, arg3) +} + // Build mocks base method. func (m *MockPackClient) Build(arg0 context.Context, arg1 client.BuildOptions) error { m.ctrl.T.Helper() @@ -64,6 +92,48 @@ func (mr *MockPackClientMockRecorder) CreateBuilder(arg0, arg1 interface{}) *gom return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateBuilder", reflect.TypeOf((*MockPackClient)(nil).CreateBuilder), arg0, arg1) } +// CreateManifest mocks base method. +func (m *MockPackClient) CreateManifest(arg0 context.Context, arg1 string, arg2 []string, arg3 client.CreateManifestOptions) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateManifest", arg0, arg1, arg2, arg3) + ret0, _ := ret[0].(error) + return ret0 +} + +// CreateManifest indicates an expected call of CreateManifest. +func (mr *MockPackClientMockRecorder) CreateManifest(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateManifest", reflect.TypeOf((*MockPackClient)(nil).CreateManifest), arg0, arg1, arg2, arg3) +} + +// CreateMultiArchBuilder mocks base method. +func (m *MockPackClient) CreateMultiArchBuilder(arg0 context.Context, arg1 client.CreateBuilderOptions) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateMultiArchBuilder", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// CreateMultiArchBuilder indicates an expected call of CreateMultiArchBuilder. +func (mr *MockPackClientMockRecorder) CreateMultiArchBuilder(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateMultiArchBuilder", reflect.TypeOf((*MockPackClient)(nil).CreateMultiArchBuilder), arg0, arg1) +} + +// DeleteManifest mocks base method. +func (m *MockPackClient) DeleteManifest(arg0 context.Context, arg1 []string) []error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteManifest", arg0, arg1) + ret0, _ := ret[0].([]error) + return ret0 +} + +// DeleteManifest indicates an expected call of DeleteManifest. +func (mr *MockPackClientMockRecorder) DeleteManifest(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteManifest", reflect.TypeOf((*MockPackClient)(nil).DeleteManifest), arg0, arg1) +} + // DownloadSBOM mocks base method. func (m *MockPackClient) DownloadSBOM(arg0 string, arg1 client.DownloadSBOMOptions) error { m.ctrl.T.Helper() @@ -78,6 +148,20 @@ func (mr *MockPackClientMockRecorder) DownloadSBOM(arg0, arg1 interface{}) *gomo return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DownloadSBOM", reflect.TypeOf((*MockPackClient)(nil).DownloadSBOM), arg0, arg1) } +// ExistsManifest mocks base method. +func (m *MockPackClient) ExistsManifest(arg0 context.Context, arg1 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ExistsManifest", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// ExistsManifest indicates an expected call of ExistsManifest. +func (mr *MockPackClientMockRecorder) ExistsManifest(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExistsManifest", reflect.TypeOf((*MockPackClient)(nil).ExistsManifest), arg0, arg1) +} + // InspectBuilder mocks base method. func (m *MockPackClient) InspectBuilder(arg0 string, arg1 bool, arg2 ...client.BuilderInspectionModifier) (*client.BuilderInfo, error) { m.ctrl.T.Helper() @@ -143,6 +227,20 @@ func (mr *MockPackClientMockRecorder) InspectImage(arg0, arg1 interface{}) *gomo return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InspectImage", reflect.TypeOf((*MockPackClient)(nil).InspectImage), arg0, arg1) } +// InspectManifest mocks base method. +func (m *MockPackClient) InspectManifest(arg0 context.Context, arg1 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "InspectManifest", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// InspectManifest indicates an expected call of InspectManifest. +func (mr *MockPackClientMockRecorder) InspectManifest(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InspectManifest", reflect.TypeOf((*MockPackClient)(nil).InspectManifest), arg0, arg1) +} + // NewBuildpack mocks base method. func (m *MockPackClient) NewBuildpack(arg0 context.Context, arg1 client.NewBuildpackOptions) error { m.ctrl.T.Helper() @@ -185,6 +283,34 @@ func (mr *MockPackClientMockRecorder) PackageExtension(arg0, arg1 interface{}) * return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PackageExtension", reflect.TypeOf((*MockPackClient)(nil).PackageExtension), arg0, arg1) } +// PackageMultiArchBuildpack mocks base method. +func (m *MockPackClient) PackageMultiArchBuildpack(arg0 context.Context, arg1 client.PackageBuildpackOptions) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PackageMultiArchBuildpack", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// PackageMultiArchBuildpack indicates an expected call of PackageMultiArchBuildpack. +func (mr *MockPackClientMockRecorder) PackageMultiArchBuildpack(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PackageMultiArchBuildpack", reflect.TypeOf((*MockPackClient)(nil).PackageMultiArchBuildpack), arg0, arg1) +} + +// PackageMultiArchExtension mocks base method. +func (m *MockPackClient) PackageMultiArchExtension(arg0 context.Context, arg1 client.PackageBuildpackOptions) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PackageMultiArchExtension", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// PackageMultiArchExtension indicates an expected call of PackageMultiArchExtension. +func (mr *MockPackClientMockRecorder) PackageMultiArchExtension(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PackageMultiArchExtension", reflect.TypeOf((*MockPackClient)(nil).PackageMultiArchExtension), arg0, arg1) +} + // PullBuildpack mocks base method. func (m *MockPackClient) PullBuildpack(arg0 context.Context, arg1 client.PullBuildpackOptions) error { m.ctrl.T.Helper() @@ -199,6 +325,20 @@ func (mr *MockPackClientMockRecorder) PullBuildpack(arg0, arg1 interface{}) *gom return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PullBuildpack", reflect.TypeOf((*MockPackClient)(nil).PullBuildpack), arg0, arg1) } +// PushManifest mocks base method. +func (m *MockPackClient) PushManifest(arg0 context.Context, arg1 string, arg2 client.PushManifestOptions) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PushManifest", arg0, arg1, arg2) + ret0, _ := ret[0].(error) + return ret0 +} + +// PushManifest indicates an expected call of PushManifest. +func (mr *MockPackClientMockRecorder) PushManifest(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PushManifest", reflect.TypeOf((*MockPackClient)(nil).PushManifest), arg0, arg1, arg2) +} + // Rebase mocks base method. func (m *MockPackClient) Rebase(arg0 context.Context, arg1 client.RebaseOptions) error { m.ctrl.T.Helper() @@ -227,6 +367,20 @@ func (mr *MockPackClientMockRecorder) RegisterBuildpack(arg0, arg1 interface{}) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RegisterBuildpack", reflect.TypeOf((*MockPackClient)(nil).RegisterBuildpack), arg0, arg1) } +// RemoveManifest mocks base method. +func (m *MockPackClient) RemoveManifest(arg0 context.Context, arg1 string, arg2 []string) []error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RemoveManifest", arg0, arg1, arg2) + ret0, _ := ret[0].([]error) + return ret0 +} + +// RemoveManifest indicates an expected call of RemoveManifest. +func (mr *MockPackClientMockRecorder) RemoveManifest(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveManifest", reflect.TypeOf((*MockPackClient)(nil).RemoveManifest), arg0, arg1, arg2) +} + // YankBuildpack mocks base method. func (m *MockPackClient) YankBuildpack(arg0 client.YankBuildpackOptions) error { m.ctrl.T.Helper() diff --git a/internal/container/run.go b/internal/container/run.go index 4f804ec15c..4f67c9b8f4 100644 --- a/internal/container/run.go +++ b/internal/container/run.go @@ -15,8 +15,8 @@ type Handler func(bodyChan <-chan dcontainer.WaitResponse, errChan <-chan error, type DockerClient interface { ContainerWait(ctx context.Context, container string, condition dcontainer.WaitCondition) (<-chan dcontainer.WaitResponse, <-chan error) - ContainerAttach(ctx context.Context, container string, options types.ContainerAttachOptions) (types.HijackedResponse, error) - ContainerStart(ctx context.Context, container string, options types.ContainerStartOptions) error + ContainerAttach(ctx context.Context, container string, options dcontainer.AttachOptions) (types.HijackedResponse, error) + ContainerStart(ctx context.Context, container string, options dcontainer.StartOptions) error } func ContainerWaitWrapper(ctx context.Context, docker DockerClient, container string, condition dcontainer.WaitCondition) (<-chan dcontainer.WaitResponse, <-chan error) { @@ -46,7 +46,7 @@ func ContainerWaitWrapper(ctx context.Context, docker DockerClient, container st func RunWithHandler(ctx context.Context, docker DockerClient, ctrID string, handler Handler) error { bodyChan, errChan := ContainerWaitWrapper(ctx, docker, ctrID, dcontainer.WaitConditionNextExit) - resp, err := docker.ContainerAttach(ctx, ctrID, types.ContainerAttachOptions{ + resp, err := docker.ContainerAttach(ctx, ctrID, dcontainer.AttachOptions{ Stream: true, Stdout: true, Stderr: true, @@ -56,7 +56,7 @@ func RunWithHandler(ctx context.Context, docker DockerClient, ctrID string, hand } defer resp.Close() - if err := docker.ContainerStart(ctx, ctrID, types.ContainerStartOptions{}); err != nil { + if err := docker.ContainerStart(ctx, ctrID, dcontainer.StartOptions{}); err != nil { return errors.Wrap(err, "container start") } diff --git a/pkg/buildpack/builder.go b/pkg/buildpack/builder.go index 443f0de322..4e0c034417 100644 --- a/pkg/buildpack/builder.go +++ b/pkg/buildpack/builder.go @@ -7,49 +7,512 @@ import ( "io" "os" "path/filepath" + "slices" "strconv" + "strings" "github.com/buildpacks/imgutil" + "github.com/buildpacks/imgutil/index" "github.com/buildpacks/imgutil/layer" + "github.com/google/go-containerregistry/pkg/name" v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/empty" "github.com/google/go-containerregistry/pkg/v1/layout" "github.com/google/go-containerregistry/pkg/v1/mutate" + "github.com/google/go-containerregistry/pkg/v1/partial" "github.com/google/go-containerregistry/pkg/v1/tarball" + "github.com/google/go-containerregistry/pkg/v1/types" "github.com/pkg/errors" "github.com/buildpacks/pack/pkg/logging" "github.com/buildpacks/pack/internal/stack" + "github.com/buildpacks/pack/internal/stringset" "github.com/buildpacks/pack/internal/style" "github.com/buildpacks/pack/pkg/archive" "github.com/buildpacks/pack/pkg/dist" + pkgImg "github.com/buildpacks/pack/pkg/image" ) type ImageFactory interface { - NewImage(repoName string, local bool, imageOS string) (imgutil.Image, error) + NewImage(repoName string, local bool, platform v1.Platform) (imgutil.Image, error) +} + +type IndexFactory interface { + // load ManifestList from local storage with the given name + LoadIndex(reponame string, opts ...index.Option) (imgutil.ImageIndex, error) } type WorkableImage interface { + // Getters + OS() (string, error) + Architecture() (string, error) + Variant() (string, error) + OSVersion() (string, error) + Features() ([]string, error) + OSFeatures() ([]string, error) + URLs() ([]string, error) + Annotations() (map[string]string, error) + + // Setters SetLabel(string, string) error + SetOS(string) error + SetArchitecture(string) error + SetVariant(string) error + SetOSVersion(string) error + SetFeatures([]string) error + SetOSFeatures([]string) error + SetURLs([]string) error + SetAnnotations(map[string]string) error + Save(...string) error + AddLayerWithDiffID(path, diffID string) error + + // Misc + + Digest() (v1.Hash, error) + MediaType() (types.MediaType, error) + ManifestSize() (int64, error) } type layoutImage struct { v1.Image + os, arch, variant, osVersion string + features, osFeatures, urls []string + annotations map[string]string } +var _ WorkableImage = (*layoutImage)(nil) +var _ imgutil.EditableImage = (*layoutImage)(nil) + type toAdd struct { tarPath string diffID string module BuildModule } +func (i *layoutImage) OS() (os string, err error) { + if i.os != "" { + return i.os, nil + } + + cfg, err := i.Image.ConfigFile() + if err != nil { + return os, err + } + if cfg == nil { + return os, imgutil.ErrConfigFileUndefined + } + + if cfg.OS != "" { + return cfg.OS, nil + } + + digest, err := i.Digest() + if err != nil { + return os, err + } + + format, err := i.MediaType() + if err != nil { + return os, err + } + + return os, imgutil.ErrOSUndefined(format, digest.String()) +} + +func (i *layoutImage) Architecture() (arch string, err error) { + if i.arch != "" { + return i.arch, nil + } + + cfg, err := i.Image.ConfigFile() + if err != nil { + return arch, err + } + if cfg == nil { + return arch, imgutil.ErrConfigFileUndefined + } + + if cfg.Architecture != "" { + return cfg.Architecture, nil + } + + digest, err := i.Digest() + if err != nil { + return arch, err + } + + format, err := i.MediaType() + if err != nil { + return arch, err + } + + return arch, imgutil.ErrArchUndefined(format, digest.String()) +} + +func (i *layoutImage) Variant() (variant string, err error) { + if i.variant != "" { + return i.variant, nil + } + + cfg, err := i.Image.ConfigFile() + if err != nil { + return variant, err + } + if cfg == nil { + return variant, imgutil.ErrConfigFileUndefined + } + + if cfg.Variant != "" { + return cfg.Variant, nil + } + + digest, err := i.Digest() + if err != nil { + return variant, err + } + + format, err := i.MediaType() + if err != nil { + return variant, err + } + + return variant, imgutil.ErrVariantUndefined(format, digest.String()) +} + +func (i *layoutImage) OSVersion() (osVersion string, err error) { + if i.osVersion != "" { + return i.osVersion, nil + } + + cfg, err := i.Image.ConfigFile() + if err != nil { + return osVersion, err + } + if cfg == nil { + return osVersion, imgutil.ErrConfigFileUndefined + } + + if cfg.OSVersion != "" { + return cfg.OSVersion, nil + } + + digest, err := i.Digest() + if err != nil { + return osVersion, err + } + + format, err := i.MediaType() + if err != nil { + return osVersion, err + } + + return osVersion, imgutil.ErrOSVersionUndefined(format, digest.String()) +} + +func (i *layoutImage) Features() (features []string, err error) { + if len(i.features) != 0 { + return i.features, nil + } + + mfest, err := i.Image.Manifest() + if err != nil { + return features, err + } + if mfest == nil { + return features, imgutil.ErrManifestUndefined + } + + if mfest.Subject == nil { + return features, imgutil.ErrManifestUndefined + } + + if mfest.Subject.Platform == nil { + return features, imgutil.ErrPlatformUndefined + } + + if features = mfest.Subject.Platform.Features; len(features) != 0 { + return features, nil + } + + digest, err := i.Digest() + if err != nil { + return features, err + } + + format, err := i.MediaType() + if err != nil { + return features, err + } + + return features, imgutil.ErrFeaturesUndefined(format, digest.String()) +} + +func (i *layoutImage) OSFeatures() (osFeatures []string, err error) { + if len(i.osFeatures) != 0 { + return i.osFeatures, nil + } + + cfg, err := i.Image.ConfigFile() + if err != nil { + return osFeatures, err + } + if cfg == nil { + return osFeatures, imgutil.ErrConfigFileUndefined + } + + if len(cfg.OSFeatures) != 0 { + return cfg.OSFeatures, nil + } + + digest, err := i.Digest() + if err != nil { + return osFeatures, err + } + + format, err := i.MediaType() + if err != nil { + return osFeatures, err + } + + return osFeatures, imgutil.ErrOSVersionUndefined(format, digest.String()) +} + +func (i *layoutImage) URLs() (urls []string, err error) { + if len(i.urls) != 0 { + return i.urls, nil + } + + mfest, err := i.Image.Manifest() + if err != nil { + return urls, err + } + if mfest == nil { + return urls, imgutil.ErrManifestUndefined + } + + if urls = mfest.Config.URLs; len(urls) != 0 { + return urls, nil + } + + digest, err := i.Digest() + if err != nil { + return urls, err + } + + format, err := i.MediaType() + if err != nil { + return urls, err + } + + return urls, imgutil.ErrURLsUndefined(format, digest.String()) +} + +func (i *layoutImage) Annotations() (annos map[string]string, err error) { + if len(i.annotations) != 0 { + return i.annotations, nil + } + + mfest, err := i.Image.Manifest() + if err != nil { + return annos, err + } + if mfest == nil { + return annos, imgutil.ErrManifestUndefined + } + + if annos = mfest.Annotations; len(annos) != 0 { + return annos, nil + } + + digest, err := i.Digest() + if err != nil { + return annos, err + } + + format, err := i.MediaType() + if err != nil { + return annos, err + } + + return annos, imgutil.ErrAnnotationsUndefined(format, digest.String()) +} + +func (i *layoutImage) SetOS(os string) error { + i.os = os + return nil +} + +func (i *layoutImage) SetArchitecture(arch string) error { + i.arch = arch + return nil +} + +func (i *layoutImage) SetVariant(variant string) error { + i.variant = variant + return nil +} + +func (i *layoutImage) SetOSVersion(osVersion string) error { + i.osVersion = osVersion + return nil +} + +func (i *layoutImage) SetFeatures(features []string) error { + if len(features) == 0 { + features = make([]string, 0) + } + + if len(i.features) == 0 { + i.features = make([]string, 0) + } + + i.features = append(i.features, features...) + return nil +} + +func (i *layoutImage) SetOSFeatures(osFeatures []string) error { + if len(osFeatures) == 0 { + osFeatures = make([]string, 0) + } + + if len(i.osFeatures) == 0 { + i.osFeatures = make([]string, 0) + } + + i.osFeatures = append(i.osFeatures, osFeatures...) + return nil +} + +func (i *layoutImage) SetURLs(urls []string) error { + if len(urls) == 0 { + urls = make([]string, 0) + } + + if len(i.urls) == 0 { + i.urls = make([]string, 0) + } + + i.urls = append(i.urls, urls...) + return nil +} + +func (i *layoutImage) SetAnnotations(annos map[string]string) error { + if len(annos) == 0 { + annos = make(map[string]string, 0) + } + + if len(i.annotations) == 0 { + i.annotations = make(map[string]string, 0) + } + + for k, v := range annos { + i.annotations[k] = v + } + return nil +} + +func (i *layoutImage) ManifestSize() (int64, error) { + return i.Image.Size() +} + +func (i *layoutImage) Save(_ ...string) error { + config, err := i.ConfigFile() + if err != nil { + return err + } + if config == nil { + return imgutil.ErrConfigFileUndefined + } + + mfest, err := i.Manifest() + if err != nil { + return err + } + if mfest == nil { + return imgutil.ErrManifestUndefined + } + + digest, err := i.Digest() + if err != nil { + return err + } + + mutateImage := false + cfg := config.DeepCopy() + desc := mfest.Config.DeepCopy() + desc.Size, _ = partial.Size(i) + desc.MediaType = mfest.MediaType + desc.Digest = digest + if desc.Platform == nil { + desc.Platform = &v1.Platform{} + } + + if i.os != "" && i.os != config.OS { + mutateImage = true + cfg.OS = i.os + desc.Platform.OS = i.os + } + + if i.arch != "" && i.arch != config.Architecture { + mutateImage = true + cfg.Architecture = i.arch + desc.Platform.Architecture = i.arch + } + + if i.variant != "" && i.variant != config.Variant { + mutateImage = true + cfg.Variant = i.variant + desc.Platform.Variant = i.variant + } + + if i.osVersion != "" && i.osVersion != config.OSVersion { + mutateImage = true + cfg.OSVersion = i.osVersion + desc.Platform.OSVersion = i.osVersion + } + + if len(i.features) != 0 && !slices.Equal(i.features, desc.Platform.Features) { + mutateImage = true + desc.Platform.Features = append(desc.Platform.Features, i.features...) + } + + if len(i.osFeatures) != 0 && !slices.Equal(i.osFeatures, config.OSFeatures) { + mutateImage = true + cfg.OSFeatures = append(cfg.OSFeatures, i.osFeatures...) + desc.Platform.OSFeatures = cfg.OSFeatures + } + + if len(i.urls) != 0 && !slices.Equal(i.urls, desc.URLs) { + mutateImage = true + desc.URLs = append(desc.URLs, i.urls...) + } + + if len(i.annotations) != 0 && !mapContains(i.annotations, mfest.Annotations) { + mutateImage = true + for k, v := range i.annotations { + desc.Annotations[k] = v + } + i.Image = mutate.Annotations(i, desc.Annotations).(v1.Image) + } + + if mutateImage { + i.Image, err = mutate.ConfigFile(i.Image, cfg) + i.Image = mutate.Subject(i, *desc).(v1.Image) + } + return err +} + func (i *layoutImage) SetLabel(key string, val string) error { configFile, err := i.ConfigFile() if err != nil { return err } + if configFile == nil { + return imgutil.ErrConfigFileUndefined + } + config := *configFile.Config.DeepCopy() if config.Labels == nil { config.Labels = map[string]string{} @@ -87,12 +550,13 @@ type PackageBuilder struct { layerWriterFactory archive.TarWriterFactory dependencies ManagedCollection imageFactory ImageFactory + indexFactory IndexFactory flattenAllBuildpacks bool flattenExcludeBuildpacks []string } // TODO: Rename to PackageBuilder -func NewBuilder(imageFactory ImageFactory, ops ...PackageBuilderOption) *PackageBuilder { +func NewBuilder(imageFactory ImageFactory, indexFactory IndexFactory, ops ...PackageBuilderOption) *PackageBuilder { opts := &options{} for _, op := range ops { if err := op(opts); err != nil { @@ -102,6 +566,7 @@ func NewBuilder(imageFactory ImageFactory, ops ...PackageBuilderOption) *Package moduleManager := NewManagedCollectionV1(opts.flatten) return &PackageBuilder{ imageFactory: imageFactory, + indexFactory: indexFactory, dependencies: moduleManager, flattenAllBuildpacks: opts.flatten, flattenExcludeBuildpacks: opts.exclude, @@ -268,11 +733,7 @@ func (b *PackageBuilder) finalizeImage(image WorkableImage, tmpDir string) error dist.AddToLayersMD(bpLayers, bp.Descriptor(), module.diffID) } - if err := dist.SetLabel(image, dist.BuildpackLayersLabel, bpLayers); err != nil { - return err - } - - return nil + return dist.SetLabel(image, dist.BuildpackLayersLabel, bpLayers) } func (b *PackageBuilder) finalizeExtensionImage(image WorkableImage, tmpDir string) error { @@ -343,19 +804,20 @@ func (b *PackageBuilder) resolvedStacks() []dist.Stack { return stacks } -func (b *PackageBuilder) SaveAsFile(path, imageOS string, labels map[string]string) error { +func (b *PackageBuilder) SaveAsFile(path, version string, target dist.Target, idx imgutil.ImageIndex, labels map[string]string) error { if err := b.validate(); err != nil { return err } - layoutImage, err := newLayoutImage(imageOS) + platform := target.Platform() + platform.OSVersion = version + layoutImage, err := newLayoutImage(*platform) if err != nil { return errors.Wrap(err, "creating layout image") } for labelKey, labelValue := range labels { - err = layoutImage.SetLabel(labelKey, labelValue) - if err != nil { + if err = layoutImage.SetLabel(labelKey, labelValue); err != nil { return errors.Wrapf(err, "adding label %s=%s", labelKey, labelValue) } } @@ -382,6 +844,15 @@ func (b *PackageBuilder) SaveAsFile(path, imageOS string, labels map[string]stri return err } } + + if err := updateLayoutImagePlatform(layoutImage, target); err != nil { + return err + } + + if err := layoutImage.Save(); err != nil { + return err + } + layoutDir, err := os.MkdirTemp(tmpDir, "oci-layout") if err != nil { return errors.Wrap(err, "creating oci-layout temp dir") @@ -396,6 +867,118 @@ func (b *PackageBuilder) SaveAsFile(path, imageOS string, labels map[string]stri return errors.Wrap(err, "writing layout") } + _, digest, err := getImageDigest(path, layoutImage) + if err != nil { + return err + } + + if idx != nil { + if err := idx.Add(digest, imgutil.WithLocalImage(layoutImage)); err != nil { + return err + } + + if err = idx.Save(); err != nil { + return err + } + } + + outputFile, err := os.Create(path) + if err != nil { + return errors.Wrap(err, "creating output file") + } + defer outputFile.Close() + + tw := tar.NewWriter(outputFile) + defer tw.Close() + + return archive.WriteDirToTar(tw, layoutDir, "/", 0, 0, 0755, true, false, nil) +} + +func (b *PackageBuilder) SaveAsMultiArchFile(path, version string, targets []dist.Target, idx imgutil.ImageIndex, labels map[string]string) error { + if err := b.validate(); err != nil { + return err + } + + tempDirName := "" + if b.buildpack != nil { + tempDirName = "package-buildpack" + } else if b.extension != nil { + tempDirName = "extension-buildpack" + } + + tmpDir, err := os.MkdirTemp("", tempDirName) + if err != nil { + return err + } + defer os.RemoveAll(tmpDir) + + layoutDir, err := os.MkdirTemp(tmpDir, "oci-layout") + if err != nil { + return errors.Wrap(err, "creating oci-layout temp dir") + } + + p, err := layout.Write(layoutDir, empty.Index) + if err != nil { + return errors.Wrap(err, "writing index") + } + + for _, target := range targets { + if err := target.Range(func(target dist.Target, distroName, distroVersion string) error { + target.Distributions = []dist.Distribution{{Name: distroName, Versions: []string{distroVersion}}} + platform := target.Platform() + platform.OSVersion = version + layoutImage, err := newLayoutImage(*platform) + if err != nil { + return errors.Wrap(err, "creating layout image") + } + + for labelKey, labelValue := range labels { + if err = layoutImage.SetLabel(labelKey, labelValue); err != nil { + return errors.Wrapf(err, "adding label %s=%s", labelKey, labelValue) + } + } + + if b.buildpack != nil { + if err := b.finalizeImage(layoutImage, tmpDir); err != nil { + return err + } + } else if b.extension != nil { + if err := b.finalizeExtensionImage(layoutImage, tmpDir); err != nil { + return err + } + } + + if err := updateLayoutImagePlatform(layoutImage, target); err != nil { + return err + } + + if err := layoutImage.Save(); err != nil { + return err + } + + if err := p.AppendImage(layoutImage); err != nil { + return errors.Wrap(err, "writing layout") + } + + _, digest, err := getImageDigest(path, layoutImage) + if err != nil { + return err + } + + if idx == nil { + return nil + } + + if err := idx.Add(digest, imgutil.WithLocalImage(layoutImage)); err != nil { + return err + } + + return idx.Save() + }); err != nil { + return err + } + } + outputFile, err := os.Create(path) if err != nil { return errors.Wrap(err, "creating output file") @@ -408,7 +991,7 @@ func (b *PackageBuilder) SaveAsFile(path, imageOS string, labels map[string]stri return archive.WriteDirToTar(tw, layoutDir, "/", 0, 0, 0755, true, false, nil) } -func newLayoutImage(imageOS string) (*layoutImage, error) { +func newLayoutImage(platform v1.Platform) (*layoutImage, error) { i := empty.Image configFile, err := i.ConfigFile() @@ -416,13 +999,17 @@ func newLayoutImage(imageOS string) (*layoutImage, error) { return nil, err } - configFile.OS = imageOS + configFile.OS = platform.OS + configFile.Architecture = platform.Architecture + configFile.Variant = platform.Variant + configFile.OSVersion = platform.OSVersion + configFile.OSFeatures = platform.OSFeatures i, err = mutate.ConfigFile(i, configFile) if err != nil { return nil, err } - if imageOS == "windows" { + if platform.OS == "windows" { opener := func() (io.ReadCloser, error) { reader, err := layer.WindowsBaseLayer() return io.NopCloser(reader), err @@ -442,19 +1029,24 @@ func newLayoutImage(imageOS string) (*layoutImage, error) { return &layoutImage{Image: i}, nil } -func (b *PackageBuilder) SaveAsImage(repoName string, publish bool, imageOS string, labels map[string]string) (imgutil.Image, error) { +func (b *PackageBuilder) SaveAsImage(repoName, version string, publish bool, target dist.Target, idx imgutil.ImageIndex, labels map[string]string) (imgutil.Image, error) { if err := b.validate(); err != nil { return nil, err } - image, err := b.imageFactory.NewImage(repoName, !publish, imageOS) + platform := *target.Platform() + imageName := repoName + if idx != nil { + imageName += ":" + strings.Join(strings.Split(strings.ReplaceAll(PlatformSafeName("", target), "@", "-"), "-")[1:], "-") + } + + image, err := b.imageFactory.NewImage(imageName, !publish, platform) if err != nil { return nil, errors.Wrapf(err, "creating image") } for labelKey, labelValue := range labels { - err = image.SetLabel(labelKey, labelValue) - if err != nil { + if err = image.SetLabel(labelKey, labelValue); err != nil { return nil, errors.Wrapf(err, "adding label %s=%s", labelKey, labelValue) } } @@ -481,11 +1073,249 @@ func (b *PackageBuilder) SaveAsImage(repoName string, publish bool, imageOS stri } } + underlyingImage := image.UnderlyingImage() + ref, digest, err := getImageDigest(repoName, underlyingImage) + if err != nil { + return nil, err + } + + // handle Local Image Digest + if underlyingImage == nil { + id, err := image.Identifier() + if err != nil { + return nil, err + } + + digest = ref.Context().Digest("sha256:" + id.String()) + } + + if err := updateImagePlatform(image, target); err != nil { + return nil, err + } + + features, _ := image.Features() + osFeatures, _ := image.OSFeatures() + urls, _ := image.URLs() + annotations, _ := image.Annotations() + + var featuresFound, osFeaturesFound, urlsFound, annosFound bool + featuresFound = sliceContains(features, target.Specs.Features) + osFeaturesFound = sliceContains(osFeatures, target.Specs.OSFeatures) + urlsFound = sliceContains(urls, target.Specs.URLs) + annosFound = mapContains(annotations, target.Specs.Annotations) + + // getAddtionalImageNames(ref, target)... if err := image.Save(); err != nil { return nil, err } - return image, nil + if idx == nil { + return image, nil + } + + if err := idx.Add(digest, imgutil.WithLocalImage(image)); err != nil { + return nil, err + } + + if !featuresFound { + if err := idx.SetFeatures(digest, features); err != nil { + return nil, err + } + } + + if !osFeaturesFound { + if err := idx.SetOSFeatures(digest, osFeatures); err != nil { + return nil, err + } + } + + if !urlsFound { + if err := idx.SetURLs(digest, urls); err != nil { + return nil, err + } + } + + if !annosFound { + annos, err := target.Annotations() + if err != nil { + return nil, err + } + + if len(annotations) == 0 { + annotations = make(map[string]string) + } + + for k, v := range annos { + annotations[k] = v + } + + if err := idx.SetAnnotations(digest, annotations); err != nil { + return nil, err + } + } + + return image, idx.Save() +} + +func mapContains(src, contains map[string]string) bool { + for k, v := range contains { + if srcValue, ok := src[k]; !ok || srcValue != v { + return false + } + } + return true +} + +func sliceContains(src, contains []string) bool { + _, missing, _ := stringset.Compare(contains, src) + return len(missing) == 0 +} + +func updateLayoutImagePlatform(image *layoutImage, target dist.Target) (err error) { + var ( + config *v1.ConfigFile + mfest *v1.Manifest + ) + + if config, err = image.ConfigFile(); err != nil { + return err + } + if config == nil { + return imgutil.ErrConfigFileUndefined + } + + if mfest, err = image.Manifest(); err != nil { + return err + } + if mfest == nil { + return imgutil.ErrManifestUndefined + } + + platform := target.Platform() + if mfest.Config.Platform == nil { + mfest.Config.Platform = &v1.Platform{} + } + + if err := updatePlatformPrimitives(image, platform, config, mfest); err != nil { + return err + } + + return updatePlatformSlicesAndMaps(image, target, config, mfest) +} + +func updatePlatformPrimitives(image imgutil.EditableImage, platform *v1.Platform, config *v1.ConfigFile, mfest *v1.Manifest) error { + if platform.OS != "" && (config.OS != platform.OS || mfest.Config.Platform.OS != platform.OS) { + if err := image.SetOS(platform.OS); err != nil { + return err + } + } + + if platform.Architecture != "" && (config.Architecture != platform.Architecture || mfest.Config.Platform.Architecture != platform.Architecture) { + if err := image.SetArchitecture(platform.Architecture); err != nil { + return err + } + } + + if platform.Variant != "" && (config.Variant != platform.Variant || mfest.Config.Platform.Variant != platform.Variant) { + if err := image.SetVariant(platform.Variant); err != nil { + return err + } + } + + if platform.OSVersion != "" && (config.OSVersion != platform.OSVersion || mfest.Config.Platform.OSVersion != platform.OSVersion) { + if err := image.SetOSVersion(platform.OSVersion); err != nil { + return err + } + } + + return nil +} + +func updatePlatformSlicesAndMaps(image imgutil.EditableImage, target dist.Target, config *v1.ConfigFile, mfest *v1.Manifest) error { + platform := target.Platform() + if len(target.Specs.Features) > 0 && !slices.Equal(mfest.Config.Platform.Features, platform.Features) { + if err := image.SetFeatures(target.Specs.Features); err != nil { + return err + } + } + + if len(target.Specs.OSFeatures) > 0 && !(slices.Equal(mfest.Config.Platform.OSFeatures, platform.OSFeatures) || slices.Equal(config.OSFeatures, platform.OSFeatures)) { + if err := image.SetOSFeatures(target.Specs.OSFeatures); err != nil { + return err + } + } + + if len(target.Specs.URLs) > 0 && !slices.Equal(mfest.Config.URLs, target.Specs.URLs) { + if err := image.SetURLs(target.Specs.URLs); err != nil { + return err + } + } + + if len(target.Specs.Annotations) > 0 && !mapContains(mfest.Annotations, target.Specs.Annotations) { + if err := image.SetAnnotations(target.Specs.Annotations); err != nil { + return err + } + } + + return nil +} + +func updateImagePlatform(image imgutil.Image, target dist.Target) (err error) { + var config *v1.ConfigFile + var mfest *v1.Manifest + + platform := target.Platform() + underlyingImage := image.UnderlyingImage() + if underlyingImage == nil { + return updatePlatformPrimitives(image, platform, &v1.ConfigFile{}, &v1.Manifest{}) + } + + if config, err = underlyingImage.ConfigFile(); err != nil { + return err + } + if config == nil { + return imgutil.ErrConfigFileUndefined + } + + if mfest, err = image.UnderlyingImage().Manifest(); err != nil { + return err + } + if mfest == nil { + return imgutil.ErrManifestUndefined + } + + if mfest.Config.Platform == nil { + mfest.Config.Platform = &v1.Platform{} + } + + if err := updatePlatformPrimitives(image, platform, config, mfest); err != nil { + return err + } + + if image.Kind() != pkgImg.LOCAL { + return updatePlatformSlicesAndMaps(image, target, config, mfest) + } + + return nil +} + +func getImageDigest(repoName string, image v1.Image) (ref name.Reference, digest name.Digest, err error) { + ref, err = name.ParseReference(repoName) + if err != nil { + return ref, digest, err + } + + // for local.Image imgutil#UnderlyingImage is nil + if image == nil { + return ref, digest, nil + } + + hash, err := image.Digest() + if err != nil { + return ref, digest, err + } + + return ref, ref.Context().Digest(hash.String()), nil } func validateBuildpacks(mainBP BuildModule, depBPs []BuildModule) error { diff --git a/pkg/buildpack/builder_test.go b/pkg/buildpack/builder_test.go index 5138fcad81..89053c3b85 100644 --- a/pkg/buildpack/builder_test.go +++ b/pkg/buildpack/builder_test.go @@ -18,6 +18,7 @@ import ( "github.com/buildpacks/imgutil/layer" "github.com/buildpacks/lifecycle/api" "github.com/golang/mock/gomock" + ggcrV1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/stream" "github.com/heroku/color" v1 "github.com/opencontainers/image-spec/specs-go/v1" @@ -40,11 +41,33 @@ func TestPackageBuilder(t *testing.T) { spec.Run(t, "PackageBuilder", testPackageBuilder, spec.Parallel(), spec.Report(report.Terminal{})) } +type Identifier struct { + id string +} + +func NewIdentifier(id string) Identifier { + return Identifier{ + id: id, + } +} + +func (i Identifier) String() string { + return i.id +} + +const ( + version = "latest" + linux = "linux" + windows = "windows" + imageIdentifier = "74eb48882e835d8767f62940d453eb96ed2737de3a16573881dcea7dea769df7" +) + func testPackageBuilder(t *testing.T, when spec.G, it spec.S) { var ( mockController *gomock.Controller mockImageFactory func(expectedImageOS string) *testmocks.MockImageFactory tmpDir string + mockIndexFactory *testmocks.MockIndexFactory ) it.Before(func() { @@ -54,13 +77,15 @@ func testPackageBuilder(t *testing.T, when spec.G, it spec.S) { imageFactory := testmocks.NewMockImageFactory(mockController) if expectedImageOS != "" { - fakePackageImage := fakes.NewImage("some/package", "", nil) - imageFactory.EXPECT().NewImage("some/package", true, expectedImageOS).Return(fakePackageImage, nil).MaxTimes(1) + fakePackageImage := fakes.NewImage("some/package", "", NewIdentifier(imageIdentifier)) + imageFactory.EXPECT().NewImage("some/package", true, ggcrV1.Platform{OS: expectedImageOS}).Return(fakePackageImage, nil).MaxTimes(1) } return imageFactory } + mockIndexFactory = testmocks.NewMockIndexFactory(mockController) + var err error tmpDir, err = os.MkdirTemp("", "package_builder_tests") h.AssertNil(t, err) @@ -78,18 +103,18 @@ func testPackageBuilder(t *testing.T, when spec.G, it spec.S) { fn func(*buildpack.PackageBuilder) error }{ {name: "SaveAsImage", expectedImageOS: "linux", fn: func(builder *buildpack.PackageBuilder) error { - _, err := builder.SaveAsImage("some/package", false, "linux", map[string]string{}) + _, err := builder.SaveAsImage("some/package", version, false, dist.Target{OS: linux}, nil, map[string]string{}) return err }}, {name: "SaveAsImage", expectedImageOS: "windows", fn: func(builder *buildpack.PackageBuilder) error { - _, err := builder.SaveAsImage("some/package", false, "windows", map[string]string{}) + _, err := builder.SaveAsImage("some/package", version, false, dist.Target{OS: windows}, nil, map[string]string{}) return err }}, {name: "SaveAsFile", expectedImageOS: "linux", fn: func(builder *buildpack.PackageBuilder) error { - return builder.SaveAsFile(path.Join(tmpDir, "package.cnb"), "linux", map[string]string{}) + return builder.SaveAsFile(path.Join(tmpDir, "package.cnb"), version, dist.Target{OS: linux}, nil, map[string]string{}) }}, {name: "SaveAsFile", expectedImageOS: "windows", fn: func(builder *buildpack.PackageBuilder) error { - return builder.SaveAsFile(path.Join(tmpDir, "package.cnb"), "windows", map[string]string{}) + return builder.SaveAsFile(path.Join(tmpDir, "package.cnb"), version, dist.Target{OS: windows}, nil, map[string]string{}) }}, } { // always use copies to avoid stale refs @@ -103,7 +128,7 @@ func testPackageBuilder(t *testing.T, when spec.G, it spec.S) { when("validate buildpack", func() { when("buildpack not set", func() { it("returns error", func() { - builder := buildpack.NewBuilder(mockImageFactory(expectedImageOS)) + builder := buildpack.NewBuilder(mockImageFactory(expectedImageOS), mockIndexFactory) err := testFn(builder) h.AssertError(t, err, "buildpack or extension must be set") }) @@ -121,7 +146,7 @@ func testPackageBuilder(t *testing.T, when spec.G, it spec.S) { }, 0644) h.AssertNil(t, err) - builder := buildpack.NewBuilder(mockImageFactory(expectedImageOS)) + builder := buildpack.NewBuilder(mockImageFactory(expectedImageOS), mockIndexFactory) builder.SetBuildpack(bp1) bp2, err := ifakes.NewFakeBuildpack(dist.BuildpackDescriptor{ @@ -155,7 +180,7 @@ func testPackageBuilder(t *testing.T, when spec.G, it spec.S) { }, 0644) h.AssertNil(t, err) - builder := buildpack.NewBuilder(mockImageFactory(expectedImageOS)) + builder := buildpack.NewBuilder(mockImageFactory(expectedImageOS), mockIndexFactory) builder.SetBuildpack(mainBP) presentBP, err := ifakes.NewFakeBuildpack(dist.BuildpackDescriptor{ @@ -187,7 +212,7 @@ func testPackageBuilder(t *testing.T, when spec.G, it spec.S) { }}, }, 0644) h.AssertNil(t, err) - builder := buildpack.NewBuilder(mockImageFactory(expectedImageOS)) + builder := buildpack.NewBuilder(mockImageFactory(expectedImageOS), mockIndexFactory) builder.SetBuildpack(mainBP) presentBP, err := ifakes.NewFakeBuildpack(dist.BuildpackDescriptor{ @@ -222,7 +247,7 @@ func testPackageBuilder(t *testing.T, when spec.G, it spec.S) { }}, }, 0644) h.AssertNil(t, err) - builder := buildpack.NewBuilder(mockImageFactory(expectedImageOS)) + builder := buildpack.NewBuilder(mockImageFactory(expectedImageOS), mockIndexFactory) builder.SetBuildpack(mainBP) presentBP, err := ifakes.NewFakeBuildpack(dist.BuildpackDescriptor{ @@ -261,7 +286,7 @@ func testPackageBuilder(t *testing.T, when spec.G, it spec.S) { }, 0644) h.AssertNil(t, err) - builder := buildpack.NewBuilder(mockImageFactory(expectedImageOS)) + builder := buildpack.NewBuilder(mockImageFactory(expectedImageOS), mockIndexFactory) builder.SetBuildpack(bp) dependency, err := ifakes.NewFakeBuildpack(dist.BuildpackDescriptor{ @@ -304,7 +329,7 @@ func testPackageBuilder(t *testing.T, when spec.G, it spec.S) { }, 0644) h.AssertNil(t, err) - builder := buildpack.NewBuilder(mockImageFactory(expectedImageOS)) + builder := buildpack.NewBuilder(mockImageFactory(expectedImageOS), mockIndexFactory) builder.SetBuildpack(bp) dependency1, err := ifakes.NewFakeBuildpack(dist.BuildpackDescriptor{ @@ -361,7 +386,7 @@ func testPackageBuilder(t *testing.T, when spec.G, it spec.S) { }, 0644) h.AssertNil(t, err) - builder := buildpack.NewBuilder(mockImageFactory(expectedImageOS)) + builder := buildpack.NewBuilder(mockImageFactory(expectedImageOS), mockIndexFactory) builder.SetBuildpack(bp) dependency1, err := ifakes.NewFakeBuildpack(dist.BuildpackDescriptor{ @@ -393,7 +418,7 @@ func testPackageBuilder(t *testing.T, when spec.G, it spec.S) { h.AssertNil(t, err) builder.AddDependency(dependency2) - img, err := builder.SaveAsImage("some/package", false, expectedImageOS, map[string]string{}) + img, err := builder.SaveAsImage("some/package", version, false, dist.Target{OS: expectedImageOS}, nil, map[string]string{}) h.AssertNil(t, err) metadata := buildpack.Metadata{} @@ -424,7 +449,7 @@ func testPackageBuilder(t *testing.T, when spec.G, it spec.S) { }, 0644) h.AssertNil(t, err) - builder := buildpack.NewBuilder(mockImageFactory(expectedImageOS)) + builder := buildpack.NewBuilder(mockImageFactory(expectedImageOS), mockIndexFactory) builder.SetBuildpack(bp) dependency1, err := ifakes.NewFakeBuildpack(dist.BuildpackDescriptor{ @@ -455,7 +480,7 @@ func testPackageBuilder(t *testing.T, when spec.G, it spec.S) { h.AssertNil(t, err) builder.AddDependency(dependency2) - img, err := builder.SaveAsImage("some/package", false, expectedImageOS, map[string]string{}) + img, err := builder.SaveAsImage("some/package", version, false, dist.Target{OS: expectedImageOS}, nil, map[string]string{}) h.AssertNil(t, err) metadata := buildpack.Metadata{} @@ -483,7 +508,7 @@ func testPackageBuilder(t *testing.T, when spec.G, it spec.S) { }, 0644) h.AssertNil(t, err) - builder := buildpack.NewBuilder(mockImageFactory(expectedImageOS)) + builder := buildpack.NewBuilder(mockImageFactory(expectedImageOS), mockIndexFactory) builder.SetBuildpack(bp) dependencyOrder, err := ifakes.NewFakeBuildpack(dist.BuildpackDescriptor{ @@ -520,7 +545,7 @@ func testPackageBuilder(t *testing.T, when spec.G, it spec.S) { builder.AddDependency(dependencyNestedNested) - img, err := builder.SaveAsImage("some/package", false, expectedImageOS, map[string]string{}) + img, err := builder.SaveAsImage("some/package", version, false, dist.Target{OS: expectedImageOS}, nil, map[string]string{}) h.AssertNil(t, err) metadata := buildpack.Metadata{} @@ -562,12 +587,12 @@ func testPackageBuilder(t *testing.T, when spec.G, it spec.S) { }, 0644) h.AssertNil(t, err) - builder := buildpack.NewBuilder(mockImageFactory("linux")) + builder := buildpack.NewBuilder(mockImageFactory("linux"), mockIndexFactory) builder.SetBuildpack(buildpack1) var customLabels = map[string]string{"test.label.one": "1", "test.label.two": "2"} - packageImage, err := builder.SaveAsImage("some/package", false, "linux", customLabels) + packageImage, err := builder.SaveAsImage("some/package", version, false, dist.Target{OS: linux}, nil, customLabels) h.AssertNil(t, err) labelData, err := packageImage.Label("io.buildpacks.buildpackage.metadata") @@ -616,9 +641,9 @@ func testPackageBuilder(t *testing.T, when spec.G, it spec.S) { }, }, 0644) h.AssertNil(t, err) - builder := buildpack.NewBuilder(mockImageFactory("linux")) + builder := buildpack.NewBuilder(mockImageFactory("linux"), mockIndexFactory) builder.SetExtension(extension1) - packageImage, err := builder.SaveAsImage("some/package", false, "linux", map[string]string{}) + packageImage, err := builder.SaveAsImage("some/package", version, false, dist.Target{OS: linux}, nil, map[string]string{}) h.AssertNil(t, err) labelData, err := packageImage.Label("io.buildpacks.buildpackage.metadata") h.AssertNil(t, err) @@ -648,10 +673,10 @@ func testPackageBuilder(t *testing.T, when spec.G, it spec.S) { }, 0644) h.AssertNil(t, err) - builder := buildpack.NewBuilder(mockImageFactory("linux")) + builder := buildpack.NewBuilder(mockImageFactory("linux"), mockIndexFactory) builder.SetBuildpack(buildpack1) - packageImage, err := builder.SaveAsImage("some/package", false, "linux", map[string]string{}) + packageImage, err := builder.SaveAsImage("some/package", version, false, dist.Target{OS: linux}, nil, map[string]string{}) h.AssertNil(t, err) var bpLayers dist.ModuleLayers @@ -672,10 +697,10 @@ func testPackageBuilder(t *testing.T, when spec.G, it spec.S) { }, 0644) h.AssertNil(t, err) - builder := buildpack.NewBuilder(mockImageFactory("linux")) + builder := buildpack.NewBuilder(mockImageFactory("linux"), mockIndexFactory) builder.SetBuildpack(buildpack1) - packageImage, err := builder.SaveAsImage("some/package", false, "linux", map[string]string{}) + packageImage, err := builder.SaveAsImage("some/package", version, false, dist.Target{OS: linux}, nil, map[string]string{}) h.AssertNil(t, err) buildpackExists := func(name, version string) { @@ -719,18 +744,18 @@ func testPackageBuilder(t *testing.T, when spec.G, it spec.S) { }, 0644) h.AssertNil(t, err) - builder := buildpack.NewBuilder(mockImageFactory("windows")) + builder := buildpack.NewBuilder(mockImageFactory("windows"), mockIndexFactory) builder.SetBuildpack(buildpack1) - _, err = builder.SaveAsImage("some/package", false, "windows", map[string]string{}) + _, err = builder.SaveAsImage("some/package", version, false, dist.Target{OS: windows}, nil, map[string]string{}) h.AssertNil(t, err) }) it("should report an error when custom label cannot be set", func() { mockImageFactory = func(expectedImageOS string) *testmocks.MockImageFactory { - var imageWithLabelError = &imageWithLabelError{Image: fakes.NewImage("some/package", "", nil)} + var imageWithLabelError = &imageWithLabelError{Image: fakes.NewImage("some/package", "", NewIdentifier(imageIdentifier))} imageFactory := testmocks.NewMockImageFactory(mockController) - imageFactory.EXPECT().NewImage("some/package", true, expectedImageOS).Return(imageWithLabelError, nil).MaxTimes(1) + imageFactory.EXPECT().NewImage("some/package", true, ggcrV1.Platform{OS: expectedImageOS}).Return(imageWithLabelError, nil).MaxTimes(1) return imageFactory } @@ -758,12 +783,12 @@ func testPackageBuilder(t *testing.T, when spec.G, it spec.S) { }, 0644) h.AssertNil(t, err) - builder := buildpack.NewBuilder(mockImageFactory("linux")) + builder := buildpack.NewBuilder(mockImageFactory("linux"), mockIndexFactory) builder.SetBuildpack(buildpack1) var customLabels = map[string]string{"test.label.fail": "true"} - _, err = builder.SaveAsImage("some/package", false, "linux", customLabels) + _, err = builder.SaveAsImage("some/package", version, false, dist.Target{OS: linux}, nil, customLabels) h.AssertError(t, err, "adding label test.label.fail=true") }) @@ -881,6 +906,7 @@ func testPackageBuilder(t *testing.T, when spec.G, it spec.S) { when("no exclusions", func() { it.Before(func() { builder = buildpack.NewBuilder(mockImageFactory("linux"), + mockIndexFactory, buildpack.FlattenAll(), buildpack.WithLogger(logger), buildpack.WithLayerWriterFactory(archive.DefaultTarWriterFactory())) @@ -891,7 +917,7 @@ func testPackageBuilder(t *testing.T, when spec.G, it spec.S) { builder.AddDependencies(bp1, nil) builder.AddDependencies(compositeBP2, []buildpack.BuildModule{bp21, bp22, compositeBP3, bp31}) - packageImage, err := builder.SaveAsImage("some/package", false, "linux", map[string]string{}) + packageImage, err := builder.SaveAsImage("some/package", version, false, dist.Target{OS: linux}, nil, map[string]string{}) h.AssertNil(t, err) fakePackageImage := packageImage.(*fakes.Image) @@ -904,6 +930,7 @@ func testPackageBuilder(t *testing.T, when spec.G, it spec.S) { excluded := []string{bp31.Descriptor().Info().FullName()} builder = buildpack.NewBuilder(mockImageFactory("linux"), + mockIndexFactory, buildpack.DoNotFlatten(excluded), buildpack.WithLogger(logger), buildpack.WithLayerWriterFactory(archive.DefaultTarWriterFactory())) @@ -914,7 +941,7 @@ func testPackageBuilder(t *testing.T, when spec.G, it spec.S) { builder.AddDependencies(bp1, nil) builder.AddDependencies(compositeBP2, []buildpack.BuildModule{bp21, bp22, compositeBP3, bp31}) - packageImage, err := builder.SaveAsImage("some/package", false, "linux", map[string]string{}) + packageImage, err := builder.SaveAsImage("some/package", version, false, dist.Target{OS: linux}, nil, map[string]string{}) h.AssertNil(t, err) fakePackageImage := packageImage.(*fakes.Image) @@ -935,13 +962,13 @@ func testPackageBuilder(t *testing.T, when spec.G, it spec.S) { }, 0644) h.AssertNil(t, err) - builder := buildpack.NewBuilder(mockImageFactory("")) + builder := buildpack.NewBuilder(mockImageFactory(""), mockIndexFactory) builder.SetBuildpack(buildpack1) var customLabels = map[string]string{"test.label.one": "1", "test.label.two": "2"} outputFile := filepath.Join(tmpDir, fmt.Sprintf("package-%s.cnb", h.RandString(10))) - h.AssertNil(t, builder.SaveAsFile(outputFile, "linux", customLabels)) + h.AssertNil(t, builder.SaveAsFile(outputFile, version, dist.Target{OS: linux}, nil, customLabels)) withContents := func(fn func(data []byte)) h.TarEntryAssertion { return func(t *testing.T, header *tar.Header, data []byte) { @@ -997,11 +1024,11 @@ func testPackageBuilder(t *testing.T, when spec.G, it spec.S) { }, 0644) h.AssertNil(t, err) - builder := buildpack.NewBuilder(mockImageFactory("")) + builder := buildpack.NewBuilder(mockImageFactory(""), mockIndexFactory) builder.SetBuildpack(buildpack1) outputFile := filepath.Join(tmpDir, fmt.Sprintf("package-%s.cnb", h.RandString(10))) - h.AssertNil(t, builder.SaveAsFile(outputFile, "linux", map[string]string{})) + h.AssertNil(t, builder.SaveAsFile(outputFile, version, dist.Target{OS: linux}, nil, map[string]string{})) h.AssertOnTarEntry(t, outputFile, "/blobs", h.IsDirectory(), @@ -1047,11 +1074,11 @@ func testPackageBuilder(t *testing.T, when spec.G, it spec.S) { }, 0644) h.AssertNil(t, err) - builder := buildpack.NewBuilder(mockImageFactory("")) + builder := buildpack.NewBuilder(mockImageFactory(""), mockIndexFactory) builder.SetBuildpack(buildpack1) outputFile := filepath.Join(tmpDir, fmt.Sprintf("package-%s.cnb", h.RandString(10))) - h.AssertNil(t, builder.SaveAsFile(outputFile, "windows", map[string]string{})) + h.AssertNil(t, builder.SaveAsFile(outputFile, version, dist.Target{OS: windows}, nil, map[string]string{})) // Windows baselayer content is constant expectedBaseLayerReader, err := layer.WindowsBaseLayer() @@ -1080,6 +1107,84 @@ func testPackageBuilder(t *testing.T, when spec.G, it spec.S) { h.IsGzipped(), ) }) + + it("should mutate image", func() { + buildpack1, err := ifakes.NewFakeBuildpack(dist.BuildpackDescriptor{ + WithAPI: api.MustParse("0.2"), + WithInfo: dist.ModuleInfo{ID: "bp.1.id", Version: "bp.1.version"}, + WithStacks: []dist.Stack{{ID: "stack.id.1"}, {ID: "stack.id.2"}}, + WithOrder: nil, + }, 0644) + h.AssertNil(t, err) + + builder := buildpack.NewBuilder(mockImageFactory(""), mockIndexFactory) + builder.SetBuildpack(buildpack1) + + outputFile := filepath.Join(tmpDir, fmt.Sprintf("package-%s.cnb", h.RandString(10))) + h.AssertNil(t, builder.SaveAsFile(outputFile, version, dist.Target{ + OS: linux, + Arch: "arm", + ArchVariant: "v6", + Distributions: []dist.Distribution{ + { + Name: "ubuntu", + Versions: []string{"22.04"}, + }, + }, + Specs: dist.TargetSpecs{ + Features: []string{"feature1", "feature2"}, + OSFeatures: []string{"osFeature1", "osFeature2"}, + URLs: []string{"url1", "url2"}, + Annotations: map[string]string{"key1": "value1", "key2": "value2"}, + Labels: make(map[string]string), + }, + }, nil, make(map[string]string))) + + withContents := func(fn func(data []byte)) h.TarEntryAssertion { + return func(t *testing.T, header *tar.Header, data []byte) { + fn(data) + } + } + + h.AssertOnTarEntry(t, outputFile, "/index.json", + h.HasOwnerAndGroup(0, 0), + h.HasFileMode(0755), + withContents(func(data []byte) { + index := v1.Index{} + err := json.Unmarshal(data, &index) + h.AssertNil(t, err) + h.AssertEq(t, len(index.Manifests), 1) + + // manifest: application/vnd.docker.distribution.manifest.v2+json + h.AssertOnTarEntry(t, outputFile, + "/blobs/sha256/"+index.Manifests[0].Digest.Hex(), + h.HasOwnerAndGroup(0, 0), + h.IsJSON(), + + withContents(func(data []byte) { + manifest := v1.Manifest{} + err := json.Unmarshal(data, &manifest) + h.AssertNil(t, err) + + // config: application/vnd.docker.container.image.v1+json + h.AssertOnTarEntry(t, outputFile, + "/blobs/sha256/"+manifest.Config.Digest.Hex(), + h.HasOwnerAndGroup(0, 0), + h.IsJSON(), + // image os + h.ContentContains(`"os":"linux"`), + // image arch + h.ContentContains(`"arch":"arm"`), + // image variant + h.ContentContains(`"variant":"v6"`), + // image version + h.ContentContains(`"version":"bp.1.version"`), + // image osFeatures + h.ContentContains(`"os.features":["osFeature1", "osFeature2"]`), + ) + })) + })) + }) }) } diff --git a/pkg/buildpack/downloader_test.go b/pkg/buildpack/downloader_test.go index 2aefdb571d..49820b0a37 100644 --- a/pkg/buildpack/downloader_test.go +++ b/pkg/buildpack/downloader_test.go @@ -10,8 +10,9 @@ import ( "github.com/buildpacks/imgutil/fakes" "github.com/buildpacks/lifecycle/api" - "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/system" "github.com/golang/mock/gomock" + v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/heroku/color" "github.com/pkg/errors" "github.com/sclevine/spec" @@ -48,6 +49,7 @@ func testBuildpackDownloader(t *testing.T, when spec.G, it spec.S) { logger logging.Logger out bytes.Buffer tmpDir string + imageIdentifier = "74eb48882e835d8767f62940d453eb96ed2737de3a16573881dcea7dea769df7" ) var createBuildpack = func(descriptor dist.BuildpackDescriptor) string { @@ -59,8 +61,8 @@ func testBuildpackDownloader(t *testing.T, when spec.G, it spec.S) { } var createPackage = func(imageName string) *fakes.Image { - packageImage := fakes.NewImage(imageName, "", nil) - mockImageFactory.EXPECT().NewImage(packageImage.Name(), false, "linux").Return(packageImage, nil) + packageImage := fakes.NewImage(imageName, "", NewIdentifier(imageIdentifier)) + mockImageFactory.EXPECT().NewImage(packageImage.Name(), false, v1.Platform{OS: "linux"}).Return(packageImage, nil) pack, err := client.NewClient( client.WithLogger(logger), @@ -81,6 +83,9 @@ func testBuildpackDownloader(t *testing.T, when spec.G, it spec.S) { WithStacks: []dist.Stack{{ID: "some.stack.id"}}, })}, }, + IndexOptions: pubbldpkg.IndexOptions{ + Targets: []dist.Target{{OS: "linux"}}, + }, Publish: true, })) @@ -100,7 +105,7 @@ func testBuildpackDownloader(t *testing.T, when spec.G, it spec.S) { buildpackDownloader = buildpack.NewDownloader(logger, mockImageFetcher, mockDownloader, mockRegistryResolver) - mockDockerClient.EXPECT().Info(context.TODO()).Return(types.Info{OSType: "linux"}, nil).AnyTimes() + mockDockerClient.EXPECT().Info(context.TODO()).Return(system.Info{OSType: "linux"}, nil).AnyTimes() mockRegistryResolver.EXPECT(). Resolve("some-registry", "urn:cnb:registry:example/foo@1.1.0"). diff --git a/pkg/buildpack/parse_name.go b/pkg/buildpack/parse_name.go index bc170c2788..871674d4d1 100644 --- a/pkg/buildpack/parse_name.go +++ b/pkg/buildpack/parse_name.go @@ -3,6 +3,14 @@ package buildpack import ( "fmt" "strings" + + "github.com/buildpacks/pack/pkg/dist" +) + +const ( + platformDelim = "/" + platformSafeDelim = "-" + distroDelim = "@" ) // ParseIDLocator parses a buildpack locator in the following formats into its ID and version. @@ -57,3 +65,34 @@ func parseBuilderLocator(locator string) (path string) { strings.TrimPrefix(locator, deprecatedFromBuilderPrefix+":"), fromBuilderPrefix+":") } + +// Returns uri + "/" + [-os][-arch][-variant][-distro@version] +func PlatformSafeName(uri string, target dist.Target) string { + var distro = dist.Distribution{} + if len(target.Distributions) != 0 { + distro = target.Distributions[0] + } + + var version = "" + if len(distro.Versions) != 0 { + version = distro.Versions[0] + } + platformDir := PlatformRootDirectory(target, distro.Name, version) + return uri + platformSafeDelim + strings.ReplaceAll(platformDir, "/", platformSafeDelim) +} + +// Returns os/[arch]/[variant]/[distroName@variant] +func PlatformRootDirectory(target dist.Target, distroName, version string) string { + distroStr := strings.Join(getNonNilStringSlice([]string{distroName, version}), distroDelim) + return strings.Join(getNonNilStringSlice([]string{target.OS, target.Arch, target.ArchVariant, distroStr}), platformDelim) +} + +func getNonNilStringSlice(slice []string) (nonNil []string) { + for _, s := range slice { + if s != "" { + nonNil = append(nonNil, s) + } + } + + return nonNil +} diff --git a/pkg/cache/image_cache.go b/pkg/cache/image_cache.go index da1765eeee..5585ddc136 100644 --- a/pkg/cache/image_cache.go +++ b/pkg/cache/image_cache.go @@ -4,6 +4,7 @@ import ( "context" "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/image" "github.com/docker/docker/client" "github.com/google/go-containerregistry/pkg/name" ) @@ -14,7 +15,7 @@ type ImageCache struct { } type DockerClient interface { - ImageRemove(ctx context.Context, image string, options types.ImageRemoveOptions) ([]types.ImageDeleteResponseItem, error) + ImageRemove(ctx context.Context, image string, options types.ImageRemoveOptions) ([]image.DeleteResponse, error) VolumeRemove(ctx context.Context, volumeID string, force bool) error } diff --git a/pkg/client/add_manifest.go b/pkg/client/add_manifest.go new file mode 100644 index 0000000000..3e3b27514a --- /dev/null +++ b/pkg/client/add_manifest.go @@ -0,0 +1,73 @@ +package client + +import ( + "context" + "fmt" + + "github.com/buildpacks/imgutil" + "github.com/google/go-containerregistry/pkg/name" +) + +type ManifestAddOptions struct { + OS, OSVersion, OSArch, OSVariant string + OSFeatures, Features []string + Annotations map[string]string + All bool +} + +// AddManifest implements commands.PackClient. +func (c *Client) AddManifest(ctx context.Context, ii string, image string, opts ManifestAddOptions) (err error) { + idx, err := c.indexFactory.LoadIndex(ii) + if err != nil { + return err + } + + var ops = make([]imgutil.IndexAddOption, 0) + if opts.All { + ops = append(ops, imgutil.WithAll(opts.All)) + } + + if opts.OS != "" { + ops = append(ops, imgutil.WithOS(opts.OS)) + } + + if opts.OSArch != "" { + ops = append(ops, imgutil.WithArchitecture(opts.OSArch)) + } + + if opts.OSVariant != "" { + ops = append(ops, imgutil.WithVariant(opts.OSVariant)) + } + + if opts.OSVersion != "" { + ops = append(ops, imgutil.WithOSVersion(opts.OSVersion)) + } + + if len(opts.Features) != 0 { + ops = append(ops, imgutil.WithFeatures(opts.Features)) + } + + if len(opts.OSFeatures) != 0 { + ops = append(ops, imgutil.WithOSFeatures(opts.OSFeatures)) + } + + if len(opts.Annotations) != 0 { + ops = append(ops, imgutil.WithAnnotations(opts.Annotations)) + } + + ref, err := name.ParseReference(image, name.Insecure, name.WeakValidation) + if err != nil { + return err + } + + if err = idx.Add(ref, ops...); err != nil { + return err + } + + if err = idx.Save(); err != nil { + return err + } + + fmt.Printf("successfully added to index: '%s'\n", image) + return nil +} diff --git a/pkg/client/add_manifest_test.go b/pkg/client/add_manifest_test.go new file mode 100644 index 0000000000..1dba278536 --- /dev/null +++ b/pkg/client/add_manifest_test.go @@ -0,0 +1,254 @@ +package client + +import ( + "bytes" + "context" + "errors" + "os" + "testing" + + "github.com/buildpacks/imgutil" + "github.com/buildpacks/imgutil/fakes" + "github.com/golang/mock/gomock" + "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/name" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/types" + "github.com/heroku/color" + "github.com/sclevine/spec" + "github.com/sclevine/spec/report" + + "github.com/buildpacks/pack/pkg/logging" + "github.com/buildpacks/pack/pkg/testmocks" + h "github.com/buildpacks/pack/testhelpers" +) + +func TestAddManifest(t *testing.T) { + color.Disable(true) + defer color.Disable(false) + spec.Run(t, "build", testAddManifest, spec.Report(report.Terminal{})) +} + +func testAddManifest(t *testing.T, when spec.G, it spec.S) { + var ( + mockController *gomock.Controller + mockIndexFactory *testmocks.MockIndexFactory + out bytes.Buffer + logger logging.Logger + subject *Client + err error + tmpDir string + ) + + when("#Add", func() { + it.Before(func() { + logger = logging.NewLogWithWriters(&out, &out, logging.WithVerbose()) + mockController = gomock.NewController(t) + mockIndexFactory = testmocks.NewMockIndexFactory(mockController) + + subject, err = NewClient( + WithLogger(logger), + WithIndexFactory(mockIndexFactory), + WithExperimental(true), + WithKeychain(authn.DefaultKeychain), + ) + h.AssertSameInstance(t, mockIndexFactory, subject.indexFactory) + h.AssertNil(t, err) + }) + it.After(func() { + mockController.Finish() + h.AssertNil(t, os.RemoveAll(tmpDir)) + }) + it("should return an error if index doesn't exists locally", func() { + prepareIndexWithoutLocallyExists(*mockIndexFactory) + err = subject.AddManifest( + context.TODO(), + "pack/index", + "pack/image", + ManifestAddOptions{}, + ) + + h.AssertEq(t, err.Error(), "index not found locally") + }) + it("should add the given image", func() { + digest, err := name.NewDigest("pack/image@sha256:15c46ced65c6abed6a27472a7904b04273e9a8091a5627badd6ff016ab073171") + h.AssertNil(t, err) + + idx := prepareLoadIndex(t, *mockIndexFactory) + err = subject.AddManifest( + context.TODO(), + "pack/index", + digest.Name(), + ManifestAddOptions{}, + ) + h.AssertNil(t, err) + + _, err = idx.OS(digest) + h.AssertNil(t, err) + }) + it("should add index with OS and Arch specific", func() { + digest, err := name.NewDigest("pack/image@sha256:15c46ced65c6abed6a27472a7904b04273e9a8091a5627badd6ff016ab073171") + h.AssertNil(t, err) + + idx := prepareLoadIndex( + t, + *mockIndexFactory, + ) + + err = subject.AddManifest( + context.TODO(), + "pack/index", + digest.Name(), + ManifestAddOptions{ + OS: "some-os", + OSArch: "some-arch", + }, + ) + h.AssertNil(t, err) + + os, err := idx.OS(digest) + h.AssertNil(t, err) + h.AssertEq(t, os, "some-os") + + arch, err := idx.Architecture(digest) + h.AssertNil(t, err) + h.AssertEq(t, arch, "some-arch") + }) + it("should add with variant", func() { + digest, err := name.NewDigest("pack/image@sha256:15c46ced65c6abed6a27472a7904b04273e9a8091a5627badd6ff016ab073171") + h.AssertNil(t, err) + + idx := prepareLoadIndex( + t, + *mockIndexFactory, + ) + + err = subject.AddManifest( + context.TODO(), + "pack/index", + digest.Name(), + ManifestAddOptions{ + OSVariant: "some-variant", + }, + ) + h.AssertNil(t, err) + + variant, err := idx.Variant(digest) + h.AssertNil(t, err) + h.AssertEq(t, variant, "some-variant") + }) + it("should add with osVersion", func() { + digest, err := name.NewDigest("pack/image@sha256:15c46ced65c6abed6a27472a7904b04273e9a8091a5627badd6ff016ab073171") + h.AssertNil(t, err) + + idx := prepareLoadIndex( + t, + *mockIndexFactory, + ) + + err = subject.AddManifest( + context.TODO(), + "pack/index", + digest.Name(), + ManifestAddOptions{ + OSVersion: "some-os-version", + }, + ) + h.AssertNil(t, err) + + osVersion, err := idx.OSVersion(digest) + h.AssertNil(t, err) + h.AssertEq(t, osVersion, "some-os-version") + }) + it("should add with features", func() { + digest, err := name.NewDigest("pack/image@sha256:15c46ced65c6abed6a27472a7904b04273e9a8091a5627badd6ff016ab073171") + h.AssertNil(t, err) + + idx := prepareLoadIndex( + t, + *mockIndexFactory, + ) + + err = subject.AddManifest( + context.TODO(), + "pack/index", + digest.Name(), + ManifestAddOptions{ + Features: []string{"some-features"}, + }, + ) + h.AssertNil(t, err) + + features, err := idx.Features(digest) + h.AssertNil(t, err) + h.AssertEq(t, features, []string{"some-features"}) + }) + it("should add with osFeatures", func() { + digest, err := name.NewDigest("pack/image@sha256:15c46ced65c6abed6a27472a7904b04273e9a8091a5627badd6ff016ab073171") + h.AssertNil(t, err) + + idx := prepareLoadIndex( + t, + *mockIndexFactory, + ) + + err = subject.AddManifest( + context.TODO(), + "pack/index", + digest.Name(), + ManifestAddOptions{ + Features: []string{"some-os-features"}, + }, + ) + h.AssertNil(t, err) + + osFeatures, err := idx.Features(digest) + h.AssertNil(t, err) + h.AssertEq(t, osFeatures, []string{"some-os-features"}) + }) + it("should add with annotations", func() { + digestStr := "pack/image@sha256:15c46ced65c6abed6a27472a7904b04273e9a8091a5627badd6ff016ab073171" + digest, err := name.NewDigest(digestStr) + h.AssertNil(t, err) + + idx := prepareLoadIndex( + t, + *mockIndexFactory, + ) + + err = subject.AddManifest( + context.TODO(), + "pack/index", + digestStr, + ManifestAddOptions{ + Annotations: map[string]string{"some-key": "some-value"}, + }, + ) + h.AssertNil(t, err) + + annos, err := idx.Annotations(digest) + h.AssertNil(t, err) + h.AssertEq(t, annos, map[string]string{"some-key": "some-value"}) + }) + }) +} + +func prepareIndexWithoutLocallyExists(mockIndexFactory testmocks.MockIndexFactory) { + mockIndexFactory. + EXPECT(). + LoadIndex(gomock.Any(), gomock.Any()). + Return(nil, errors.New("index not found locally")) +} + +func prepareLoadIndex(t *testing.T, mockIndexFactory testmocks.MockIndexFactory) imgutil.ImageIndex { + idx, err := fakes.NewIndex(types.OCIImageIndex, 1024, 1, 1, v1.Descriptor{}) + h.AssertNil(t, err) + + mockIndexFactory. + EXPECT(). + LoadIndex(gomock.Any(), gomock.Any()). + Return(idx, nil). + AnyTimes() + + return idx +} diff --git a/pkg/client/annotate_manifest.go b/pkg/client/annotate_manifest.go new file mode 100644 index 0000000000..0fa77181a3 --- /dev/null +++ b/pkg/client/annotate_manifest.go @@ -0,0 +1,80 @@ +package client + +import ( + "context" + "fmt" + + ggcrName "github.com/google/go-containerregistry/pkg/name" +) + +type ManifestAnnotateOptions struct { + OS, OSVersion, OSArch, OSVariant string + OSFeatures, Features, URLs []string + Annotations map[string]string +} + +// AnnotateManifest implements commands.PackClient. +func (c *Client) AnnotateManifest(ctx context.Context, name string, image string, opts ManifestAnnotateOptions) error { + idx, err := c.indexFactory.LoadIndex(name) + if err != nil { + return err + } + + digest, err := ggcrName.NewDigest(image, ggcrName.Insecure, ggcrName.WeakValidation) + if err != nil { + return err + } + + if opts.OS != "" { + if err := idx.SetOS(digest, opts.OS); err != nil { + return err + } + } + if opts.OSVersion != "" { + if err := idx.SetOSVersion(digest, opts.OSVersion); err != nil { + return err + } + } + if len(opts.OSFeatures) != 0 { + if err := idx.SetOSFeatures(digest, opts.OSFeatures); err != nil { + return err + } + } + if opts.OSArch != "" { + if err := idx.SetArchitecture(digest, opts.OSArch); err != nil { + return err + } + } + if opts.OSVariant != "" { + if err := idx.SetVariant(digest, opts.OSVariant); err != nil { + return err + } + } + if len(opts.Features) != 0 { + if err := idx.SetFeatures(digest, opts.Features); err != nil { + return err + } + } + if len(opts.OSFeatures) != 0 { + if err := idx.SetOSFeatures(digest, opts.OSFeatures); err != nil { + return err + } + } + if len(opts.URLs) != 0 { + if err := idx.SetURLs(digest, opts.URLs); err != nil { + return err + } + } + if len(opts.Annotations) != 0 { + if err := idx.SetAnnotations(digest, opts.Annotations); err != nil { + return err + } + } + + if err = idx.Save(); err != nil { + return err + } + + fmt.Printf("successfully annotated image '%s' in index '%s'\n", image, name) + return nil +} diff --git a/pkg/client/annotate_manifest_test.go b/pkg/client/annotate_manifest_test.go new file mode 100644 index 0000000000..1b0bb07a1b --- /dev/null +++ b/pkg/client/annotate_manifest_test.go @@ -0,0 +1,457 @@ +package client + +import ( + "bytes" + "context" + "os" + "testing" + + "github.com/buildpacks/imgutil" + "github.com/buildpacks/imgutil/fakes" + "github.com/golang/mock/gomock" + "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/name" + "github.com/heroku/color" + "github.com/sclevine/spec" + "github.com/sclevine/spec/report" + + "github.com/buildpacks/pack/pkg/logging" + "github.com/buildpacks/pack/pkg/testmocks" + h "github.com/buildpacks/pack/testhelpers" +) + +const digestStr = "sha256:d4707523ce6e12afdbe9a3be5ad69027150a834870ca0933baf7516dd1fe0f56" + +func TestAnnotateManifest(t *testing.T) { + color.Disable(true) + defer color.Disable(false) + spec.Run(t, "build", testAnnotateManifest, spec.Report(report.Terminal{})) +} + +func testAnnotateManifest(t *testing.T, when spec.G, it spec.S) { + var ( + mockController *gomock.Controller + mockIndexFactory *testmocks.MockIndexFactory + out bytes.Buffer + logger logging.Logger + subject *Client + err error + tmpDir string + ) + + when("#Add", func() { + it.Before(func() { + logger = logging.NewLogWithWriters(&out, &out, logging.WithVerbose()) + mockController = gomock.NewController(t) + mockIndexFactory = testmocks.NewMockIndexFactory(mockController) + + subject, err = NewClient( + WithLogger(logger), + WithIndexFactory(mockIndexFactory), + WithExperimental(true), + WithKeychain(authn.DefaultKeychain), + ) + h.AssertSameInstance(t, mockIndexFactory, subject.indexFactory) + h.AssertNil(t, err) + }) + it.After(func() { + mockController.Finish() + h.AssertNil(t, os.RemoveAll(tmpDir)) + }) + when("successful when", func() { + it("should return an error if index doesn't exists locally", func() { + prepareIndexWithoutLocallyExists(*mockIndexFactory) + err = subject.AnnotateManifest( + context.TODO(), + "pack/index", + "pack/image", + ManifestAnnotateOptions{}, + ) + + h.AssertEq(t, err.Error(), "index not found locally") + }) + it("should set OS for given image", func() { + idx := prepareLoadIndex(t, *mockIndexFactory) + imgIdx, ok := idx.(*fakes.Index) + h.AssertEq(t, ok, true) + + mfest, err := imgIdx.IndexManifest() + h.AssertNil(t, err) + + digest, err := name.NewDigest("some/repo@" + mfest.Manifests[0].Digest.String()) + h.AssertNil(t, err) + + err = subject.AnnotateManifest( + context.TODO(), + "some/repo", + digest.Name(), + ManifestAnnotateOptions{ + OS: "some-os", + }, + ) + h.AssertNil(t, err) + + os, err := idx.OS(digest) + h.AssertNil(t, err) + h.AssertEq(t, os, "some-os") + }) + it("should set Arch for given image", func() { + idx := prepareLoadIndex(t, *mockIndexFactory) + imgIdx, ok := idx.(*fakes.Index) + h.AssertEq(t, ok, true) + + mfest, err := imgIdx.IndexManifest() + h.AssertNil(t, err) + + digest, err := name.NewDigest("some/repo@" + mfest.Manifests[0].Digest.String()) + h.AssertNil(t, err) + + err = subject.AnnotateManifest( + context.TODO(), + "some/repo", + digest.Name(), + ManifestAnnotateOptions{ + OSArch: "some-arch", + }, + ) + h.AssertNil(t, err) + + arch, err := idx.Architecture(digest) + h.AssertNil(t, err) + h.AssertEq(t, arch, "some-arch") + }) + it("should set Variant for given image", func() { + idx := prepareLoadIndex(t, *mockIndexFactory) + imgIdx, ok := idx.(*fakes.Index) + h.AssertEq(t, ok, true) + + mfest, err := imgIdx.IndexManifest() + h.AssertNil(t, err) + + digest, err := name.NewDigest("some/repo@" + mfest.Manifests[0].Digest.String()) + h.AssertNil(t, err) + + err = subject.AnnotateManifest( + context.TODO(), + "some/repo", + digest.Name(), + ManifestAnnotateOptions{ + OSVariant: "some-variant", + }, + ) + h.AssertNil(t, err) + + variant, err := idx.Variant(digest) + h.AssertNil(t, err) + h.AssertEq(t, variant, "some-variant") + }) + it("should set OSVersion for given image", func() { + idx := prepareLoadIndex(t, *mockIndexFactory) + imgIdx, ok := idx.(*fakes.Index) + h.AssertEq(t, ok, true) + + mfest, err := imgIdx.IndexManifest() + h.AssertNil(t, err) + + digest, err := name.NewDigest("some/repo@" + mfest.Manifests[0].Digest.String()) + h.AssertNil(t, err) + + err = subject.AnnotateManifest( + context.TODO(), + "some/repo", + digest.Name(), + ManifestAnnotateOptions{ + OSVersion: "some-osVersion", + }, + ) + h.AssertNil(t, err) + + osVersion, err := idx.OSVersion(digest) + h.AssertNil(t, err) + h.AssertEq(t, osVersion, "some-osVersion") + }) + it("should set Features for given image", func() { + idx := prepareLoadIndex(t, *mockIndexFactory) + imgIdx, ok := idx.(*fakes.Index) + h.AssertEq(t, ok, true) + + mfest, err := imgIdx.IndexManifest() + h.AssertNil(t, err) + + digest, err := name.NewDigest("some/repo@" + mfest.Manifests[0].Digest.String()) + h.AssertNil(t, err) + + err = subject.AnnotateManifest( + context.TODO(), + "some/repo", + digest.Name(), + ManifestAnnotateOptions{ + Features: []string{"some-features"}, + }, + ) + h.AssertNil(t, err) + + features, err := idx.Features(digest) + h.AssertNil(t, err) + h.AssertEq(t, features, []string{"some-features"}) + }) + it("should set OSFeatures for given image", func() { + idx := prepareLoadIndex(t, *mockIndexFactory) + imgIdx, ok := idx.(*fakes.Index) + h.AssertEq(t, ok, true) + + mfest, err := imgIdx.IndexManifest() + h.AssertNil(t, err) + + digest, err := name.NewDigest("some/repo@" + mfest.Manifests[0].Digest.String()) + h.AssertNil(t, err) + + err = subject.AnnotateManifest( + context.TODO(), + "some/repo", + digest.Name(), + ManifestAnnotateOptions{ + OSFeatures: []string{"some-osFeatures"}, + }, + ) + h.AssertNil(t, err) + + osFeatures, err := idx.OSFeatures(digest) + h.AssertNil(t, err) + h.AssertEq(t, osFeatures, []string{"some-osFeatures", "some-osFeatures"}) + }) + it("should set URLs for given image", func() { + idx := prepareLoadIndex(t, *mockIndexFactory) + imgIdx, ok := idx.(*fakes.Index) + h.AssertEq(t, ok, true) + + mfest, err := imgIdx.IndexManifest() + h.AssertNil(t, err) + + digest, err := name.NewDigest("some/repo@" + mfest.Manifests[0].Digest.String()) + h.AssertNil(t, err) + + err = subject.AnnotateManifest( + context.TODO(), + "some/repo", + digest.Name(), + ManifestAnnotateOptions{ + URLs: []string{"some-urls"}, + }, + ) + h.AssertNil(t, err) + + urls, err := idx.URLs(digest) + h.AssertNil(t, err) + h.AssertEq(t, urls, []string{"some-urls"}) + }) + it("should set Annotations for given image", func() { + idx := prepareLoadIndex(t, *mockIndexFactory) + imgIdx, ok := idx.(*fakes.Index) + h.AssertEq(t, ok, true) + + mfest, err := imgIdx.IndexManifest() + h.AssertNil(t, err) + + digest, err := name.NewDigest("some/repo@" + mfest.Manifests[0].Digest.String()) + h.AssertNil(t, err) + + err = subject.AnnotateManifest( + context.TODO(), + "some/repo", + digest.Name(), + ManifestAnnotateOptions{ + Annotations: map[string]string{"some-key": "some-value"}, + }, + ) + h.AssertNil(t, err) + + annos, err := idx.Annotations(digest) + h.AssertNil(t, err) + h.AssertEq(t, annos, map[string]string{"some-key": "some-value"}) + }) + it("should save annotated index", func() { + var ( + fakeOS = "some-os" + fakeArch = "some-arch" + fakeVariant = "some-variant" + fakeVersion = "some-osVersion" + fakeFeatures = []string{"some-features"} + fakeOSFeatures = []string{"some-OSFeatures"} + fakeURLs = []string{"some-urls"} + fakeAnnotations = map[string]string{"some-key": "some-value"} + ) + idx := prepareLoadIndex(t, *mockIndexFactory) + imgIdx, ok := idx.(*fakes.Index) + h.AssertEq(t, ok, true) + + mfest, err := imgIdx.IndexManifest() + h.AssertNil(t, err) + + digest, err := name.NewDigest("some/repo@" + mfest.Manifests[0].Digest.String()) + h.AssertNil(t, err) + + err = subject.AnnotateManifest( + context.TODO(), + "some/repo", + digest.Name(), + ManifestAnnotateOptions{ + OS: fakeOS, + OSArch: fakeArch, + OSVariant: fakeVariant, + OSVersion: fakeVersion, + Features: fakeFeatures, + OSFeatures: fakeOSFeatures, + URLs: fakeURLs, + Annotations: fakeAnnotations, + }, + ) + h.AssertNil(t, err) + + err = idx.Save() + h.AssertNil(t, err) + + os, err := idx.OS(digest) + h.AssertNil(t, err) + h.AssertEq(t, os, fakeOS) + + arch, err := idx.Architecture(digest) + h.AssertNil(t, err) + h.AssertEq(t, arch, fakeArch) + + variant, err := idx.Variant(digest) + h.AssertNil(t, err) + h.AssertEq(t, variant, fakeVariant) + + osVersion, err := idx.OSVersion(digest) + h.AssertNil(t, err) + h.AssertEq(t, osVersion, fakeVersion) + + features, err := idx.Features(digest) + h.AssertNil(t, err) + h.AssertEq(t, features, fakeFeatures) + + osFeatures, err := idx.OSFeatures(digest) + h.AssertNil(t, err) + h.AssertEq(t, osFeatures, []string{"some-OSFeatures", "some-OSFeatures"}) + + urls, err := idx.URLs(digest) + h.AssertNil(t, err) + h.AssertEq(t, urls, fakeURLs) + + annos, err := idx.Annotations(digest) + h.AssertNil(t, err) + h.AssertEq(t, annos, fakeAnnotations) + }) + }) + when("return an error when", func() { + it("has no Index locally by given Name", func() { + prepareIndexWithoutLocallyExists(*mockIndexFactory) + err = subject.AnnotateManifest( + context.TODO(), + "some/repo", + "", + ManifestAnnotateOptions{}, + ) + h.AssertEq(t, err.Error(), "index not found locally") + }) + it("has no image with given digest for OS", func() { + prepareLoadIndex(t, *mockIndexFactory) + + err = subject.AnnotateManifest( + context.TODO(), + "some/repo", + "busybox@"+digestStr, + ManifestAnnotateOptions{ + OS: "some-os", + }, + ) + h.AssertEq(t, err.Error(), imgutil.ErrNoImageOrIndexFoundWithGivenDigest(digestStr).Error()) + }) + it("has no image with given digest for Arch", func() { + prepareLoadIndex(t, *mockIndexFactory) + err = subject.AnnotateManifest( + context.TODO(), + "some/repo", + "busybox@"+digestStr, + ManifestAnnotateOptions{ + OSArch: "some-arch", + }, + ) + h.AssertEq(t, err.Error(), imgutil.ErrNoImageOrIndexFoundWithGivenDigest(digestStr).Error()) + }) + it("has no image with given digest for Variant", func() { + prepareLoadIndex(t, *mockIndexFactory) + err = subject.AnnotateManifest( + context.TODO(), + "some/repo", + "busybox@"+digestStr, + ManifestAnnotateOptions{ + OSVariant: "some-variant", + }, + ) + h.AssertEq(t, err.Error(), imgutil.ErrNoImageOrIndexFoundWithGivenDigest(digestStr).Error()) + }) + it("has no image with given digest for osVersion", func() { + prepareLoadIndex(t, *mockIndexFactory) + err = subject.AnnotateManifest( + context.TODO(), + "some/repo", + "busybox@"+digestStr, + ManifestAnnotateOptions{ + OSVersion: "some-osVersion", + }, + ) + h.AssertEq(t, err.Error(), imgutil.ErrNoImageOrIndexFoundWithGivenDigest(digestStr).Error()) + }) + it("has no image with given digest for Features", func() { + prepareLoadIndex(t, *mockIndexFactory) + err = subject.AnnotateManifest( + context.TODO(), + "some/repo", + "busybox@"+digestStr, + ManifestAnnotateOptions{ + Features: []string{"some-features"}, + }, + ) + h.AssertEq(t, err.Error(), imgutil.ErrNoImageOrIndexFoundWithGivenDigest(digestStr).Error()) + }) + it("has no image with given digest for OSFeatures", func() { + prepareLoadIndex(t, *mockIndexFactory) + err = subject.AnnotateManifest( + context.TODO(), + "some/repo", + "busybox@"+digestStr, + ManifestAnnotateOptions{ + OSFeatures: []string{"some-osFeatures"}, + }, + ) + h.AssertEq(t, err.Error(), imgutil.ErrNoImageOrIndexFoundWithGivenDigest(digestStr).Error()) + }) + it("has no image with given digest for URLs", func() { + prepareLoadIndex(t, *mockIndexFactory) + err = subject.AnnotateManifest( + context.TODO(), + "some/repo", + "busybox@"+digestStr, + ManifestAnnotateOptions{ + URLs: []string{"some-urls"}, + }, + ) + h.AssertEq(t, err.Error(), imgutil.ErrNoImageOrIndexFoundWithGivenDigest(digestStr).Error()) + }) + it("has no image with given digest for Annotations", func() { + prepareLoadIndex(t, *mockIndexFactory) + err = subject.AnnotateManifest( + context.TODO(), + "some/repo", + "busybox@"+digestStr, + ManifestAnnotateOptions{ + Annotations: map[string]string{"some-key": "some-value"}, + }, + ) + h.AssertEq(t, err.Error(), imgutil.ErrNoImageOrIndexFoundWithGivenDigest(digestStr).Error()) + }) + }) + }) +} diff --git a/pkg/client/build.go b/pkg/client/build.go index a4ebef86c0..b3cdf97e36 100644 --- a/pkg/client/build.go +++ b/pkg/client/build.go @@ -1295,7 +1295,7 @@ func prependBuildpackToOrder(order dist.Order, bpInfo dist.ModuleInfo) (newOrder return newOrder } -func (c *Client) processExtensions(ctx context.Context, builderImage imgutil.Image, builderExs []dist.ModuleInfo, builderOrder dist.Order, stackID string, opts BuildOptions) (fetchedExs []buildpack.BuildModule, orderExtensions dist.Order, err error) { +func (c *Client) processExtensions(ctx context.Context, builderImage imgutil.Image, builderExs []dist.ModuleInfo, _ dist.Order, _ string, opts BuildOptions) (fetchedExs []buildpack.BuildModule, orderExtensions dist.Order, err error) { relativeBaseDir := opts.RelativeBaseDir declaredExs := opts.Extensions diff --git a/pkg/client/build_test.go b/pkg/client/build_test.go index 27cd02d236..db1824f319 100644 --- a/pkg/client/build_test.go +++ b/pkg/client/build_test.go @@ -26,11 +26,9 @@ import ( "github.com/google/go-containerregistry/pkg/name" "github.com/heroku/color" "github.com/onsi/gomega/ghttp" - "github.com/pkg/errors" "github.com/sclevine/spec" "github.com/sclevine/spec/report" - "github.com/buildpacks/pack/internal/build" "github.com/buildpacks/pack/internal/builder" cfg "github.com/buildpacks/pack/internal/config" ifakes "github.com/buildpacks/pack/internal/fakes" @@ -3366,12 +3364,3 @@ func setAPIs(t *testing.T, image *fakes.Image, buildpackAPIs []string, platformA h.AssertNil(t, err) h.AssertNil(t, image.SetLabel(builderMDLabelName, string(builderMDLabelBytes))) } - -type executeFailsLifecycle struct { //nolint - Opts build.LifecycleOptions -} - -func (f *executeFailsLifecycle) Execute(_ context.Context, opts build.LifecycleOptions) error { //nolint - f.Opts = opts - return errors.New("") -} diff --git a/pkg/client/client.go b/pkg/client/client.go index d034851fc6..0194fb8352 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -16,14 +16,19 @@ package client import ( "context" + "fmt" "os" "path/filepath" "github.com/buildpacks/imgutil" + "github.com/buildpacks/imgutil/index" + "github.com/buildpacks/imgutil/layout" "github.com/buildpacks/imgutil/local" "github.com/buildpacks/imgutil/remote" dockerClient "github.com/docker/docker/client" "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/name" + v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/pkg/errors" "github.com/buildpacks/pack" @@ -36,6 +41,8 @@ import ( "github.com/buildpacks/pack/pkg/logging" ) +const xdgRuntimePath = "XDG_RUNTIME_DIR" + //go:generate mockgen -package testmocks -destination ../testmocks/mock_docker_client.go github.com/docker/docker/client CommonAPIClient //go:generate mockgen -package testmocks -destination ../testmocks/mock_image_fetcher.go github.com/buildpacks/pack/pkg/client ImageFetcher @@ -69,7 +76,21 @@ type BlobDownloader interface { type ImageFactory interface { // NewImage initializes an image object with required settings so that it // can be written either locally or to a registry. - NewImage(repoName string, local bool, imageOS string) (imgutil.Image, error) + NewImage(repoName string, local bool, platform v1.Platform) (imgutil.Image, error) +} + +//go:generate mockgen -package testmocks -destination ../testmocks/mock_index_factory.go github.com/buildpacks/pack/pkg/client IndexFactory + +// IndexFactory is an interface representing the ability to create a ImageIndex/ManifestList. +type IndexFactory interface { + // create ManifestList locally + CreateIndex(repoName string, opts ...index.Option) (imgutil.ImageIndex, error) + // load ManifestList from local storage with the given name + LoadIndex(reponame string, opts ...index.Option) (imgutil.ImageIndex, error) + // Fetch ManifestList from Registry with the given name + FetchIndex(name string, opts ...index.Option) (imgutil.ImageIndex, error) + // FindIndex will find Index locally then on remote + FindIndex(name string, opts ...index.Option) (imgutil.ImageIndex, error) } //go:generate mockgen -package testmocks -destination ../testmocks/mock_buildpack_downloader.go github.com/buildpacks/pack/pkg/client BuildpackDownloader @@ -97,14 +118,16 @@ type Client struct { keychain authn.Keychain imageFactory ImageFactory imageFetcher ImageFetcher + indexFactory IndexFactory accessChecker AccessChecker downloader BlobDownloader lifecycleExecutor LifecycleExecutor buildpackDownloader BuildpackDownloader - experimental bool - registryMirrors map[string]string - version string + experimental bool + registryMirrors map[string]string + version string + cachedIndexManifests map[name.Reference]*v1.IndexManifest } // Option is a type of function that mutate settings on the client. @@ -125,6 +148,13 @@ func WithImageFactory(f ImageFactory) Option { } } +// WithIndexFactory supply your own index factory +func WithIndexFactory(f IndexFactory) Option { + return func(c *Client) { + c.indexFactory = f + } +} + // WithFetcher supply your own Fetcher. // A Fetcher retrieves both local and remote images to make them available. func WithFetcher(f ImageFetcher) Option { @@ -236,8 +266,14 @@ func NewClient(opts ...Option) (*Client, error) { if client.imageFactory == nil { client.imageFactory = &imageFactory{ - dockerClient: client.docker, - keychain: client.keychain, + keychain: client.keychain, + } + } + + if client.indexFactory == nil { + client.indexFactory = &indexFactory{ + keychain: client.keychain, + xdgRuntimePath: os.Getenv("XDG_RUNTIME_DIR"), } } @@ -256,6 +292,10 @@ func NewClient(opts ...Option) (*Client, error) { ) } + if len(client.cachedIndexManifests) == 0 { + client.cachedIndexManifests = make(map[name.Reference]*v1.IndexManifest) + } + client.lifecycleExecutor = build.NewLifecycleExecutor(client.logger, client.docker) return client, nil @@ -284,12 +324,75 @@ type imageFactory struct { keychain authn.Keychain } -func (f *imageFactory) NewImage(repoName string, daemon bool, imageOS string) (imgutil.Image, error) { - platform := imgutil.Platform{OS: imageOS} - +func (f *imageFactory) NewImage(repoName string, daemon bool, platform v1.Platform) (imgutil.Image, error) { if daemon { return local.NewImage(repoName, f.dockerClient, local.WithDefaultPlatform(platform)) } - return remote.NewImage(repoName, f.keychain, remote.WithDefaultPlatform(platform)) } + +type indexFactory struct { + keychain authn.Keychain + xdgRuntimePath string +} + +func (f *indexFactory) LoadIndex(repoName string, opts ...index.Option) (img imgutil.ImageIndex, err error) { + if opts, err = withOptions(opts, f.keychain); err != nil { + return nil, err + } + + if img, err = local.NewIndex(repoName, opts...); err == nil { + return img, err + } + + if img, err = layout.NewIndex(repoName, opts...); err == nil { + return img, err + } + + return nil, errors.Wrap(err, errors.Errorf("Image: '%s' not found", repoName).Error()) +} + +func (f *indexFactory) FetchIndex(name string, opts ...index.Option) (idx imgutil.ImageIndex, err error) { + if opts, err = withOptions(opts, f.keychain); err != nil { + return nil, err + } + + if idx, err = remote.NewIndex(name, opts...); err != nil { + return idx, fmt.Errorf("ImageIndex in not available at registry") + } + + return idx, err +} + +func (f *indexFactory) FindIndex(repoName string, opts ...index.Option) (idx imgutil.ImageIndex, err error) { + if opts, err = withOptions(opts, f.keychain); err != nil { + return nil, err + } + + if idx, err = f.LoadIndex(repoName, opts...); err == nil { + return idx, err + } + + return f.FetchIndex(repoName, opts...) +} + +func (f *indexFactory) CreateIndex(repoName string, opts ...index.Option) (idx imgutil.ImageIndex, err error) { + if opts, err = withOptions(opts, f.keychain); err != nil { + return nil, err + } + + return index.NewIndex(repoName, opts...) +} + +func withOptions(ops []index.Option, keychain authn.Keychain) ([]index.Option, error) { + ops = append(ops, index.WithKeychain(keychain)) + if xdgPath, ok := os.LookupEnv(xdgRuntimePath); ok { + return append(ops, index.WithXDGRuntimePath(xdgPath)), nil + } + + home, err := iconfig.PackHome() + if err != nil { + return ops, err + } + return append(ops, index.WithXDGRuntimePath(filepath.Join(home, "manifests"))), nil +} diff --git a/pkg/client/create_builder.go b/pkg/client/create_builder.go index ee1062bc4f..593927fd82 100644 --- a/pkg/client/create_builder.go +++ b/pkg/client/create_builder.go @@ -8,6 +8,7 @@ import ( "github.com/Masterminds/semver" "github.com/buildpacks/imgutil" + "github.com/google/go-containerregistry/pkg/name" "github.com/pkg/errors" "golang.org/x/text/cases" "golang.org/x/text/language" @@ -36,7 +37,7 @@ type CreateBuilderOptions struct { Labels map[string]string // Configuration that defines the functionality a builder provides. - Config pubbldr.Config + Config pubbldr.MultiArchConfig // Skip building image locally, directly publish to a registry. // Requires BuilderName to be a valid registry location. @@ -50,6 +51,9 @@ type CreateBuilderOptions struct { // List of modules to be flattened Flatten buildpack.FlattenModuleInfos + + // Optional: ImageIndex used for creating IndexManifest + ImageIndex imgutil.ImageIndex } // CreateBuilder creates and saves a builder image to a registry with the provided options. @@ -81,11 +85,96 @@ func (c *Client) CreateBuilder(ctx context.Context, opts CreateBuilderOptions) e bldr.SetRunImage(opts.Config.Run) bldr.SetBuildConfigEnv(opts.BuildConfigEnv) - return bldr.Save(c.logger, builder.CreatorMetadata{Version: c.version}) + if err := bldr.Save(c.logger, builder.CreatorMetadata{Version: c.version}); err != nil { + return err + } + + if opts.ImageIndex == nil { + return nil + } + + ref, err := name.ParseReference(opts.BuilderName, name.Insecure, name.WeakValidation) + if err != nil { + return err + } + + underlyingImage := bldr.Image().UnderlyingImage() + if underlyingImage != nil { + digest, err := underlyingImage.Digest() + if err != nil { + return err + } + + if err := opts.ImageIndex.Add(ref.Context().Digest(digest.String()), imgutil.WithLocalImage(bldr.Image())); err != nil { + return err + } + } else { + id, err := bldr.Image().Identifier() + if err != nil { + return err + } + + digest := ref.Context().Digest("sha256:" + id.String()) + if err := opts.ImageIndex.Add(ref.Context().Digest(digest.String()), imgutil.WithLocalImage(bldr.Image())); err != nil { + return err + } + } + + return opts.ImageIndex.Save() +} + +func (c *Client) CreateMultiArchBuilder(ctx context.Context, opts CreateBuilderOptions) error { + if !c.experimental { + return errors.Errorf("creating %s is currently %s", style.Symbol("multi arch builder"), style.Symbol(("experimental"))) + } + + if err := c.validateConfig(ctx, opts); err != nil { + return err + } + + configs, err := opts.Config.BuilderConfigs(c.GetIndexManifestFn()) + if err != nil { + return err + } + + if err := createImageIndex(c, opts.BuilderName); err != nil { + return err + } + + idx, err := loadImageIndex(c, opts.BuilderName) + if err != nil { + return err + } + + // var errs errgroup.Group + ops := opts + ops.ImageIndex = idx + for _, config := range configs { + ops.Config.Config = config + // errs.Go(func() error { + if err := c.CreateBuilder(ctx, ops); err != nil { + return err + } + // }) + } + + // if err := errs.Wait(); err != nil { + // return err + // } + + if !opts.Publish { + return nil + } + + if err := idx.Save(); err != nil { + return err + } + + return idx.Push(imgutil.WithInsecure(true), imgutil.WithTags("latest")) } func (c *Client) validateConfig(ctx context.Context, opts CreateBuilderOptions) error { - if err := pubbldr.ValidateConfig(opts.Config); err != nil { + if err := pubbldr.ValidateConfig(opts.Config.Config); err != nil { return errors.Wrap(err, "invalid builder config") } diff --git a/pkg/client/create_builder_test.go b/pkg/client/create_builder_test.go index 34f5bc7aa8..3b406fe865 100644 --- a/pkg/client/create_builder_test.go +++ b/pkg/client/create_builder_test.go @@ -9,9 +9,10 @@ import ( "strings" "testing" + "github.com/buildpacks/imgutil" "github.com/buildpacks/imgutil/fakes" "github.com/buildpacks/lifecycle/api" - "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/system" "github.com/golang/mock/gomock" "github.com/heroku/color" "github.com/pkg/errors" @@ -48,6 +49,7 @@ func testCreateBuilder(t *testing.T, when spec.G, it spec.S) { mockDownloader *testmocks.MockBlobDownloader mockBuildpackDownloader *testmocks.MockBuildpackDownloader mockImageFactory *testmocks.MockImageFactory + mockIndexFactory *testmocks.MockIndexFactory mockImageFetcher *testmocks.MockImageFetcher mockDockerClient *testmocks.MockCommonAPIClient fakeBuildImage *fakes.Image @@ -74,7 +76,7 @@ func testCreateBuilder(t *testing.T, when spec.G, it spec.S) { return buildpack } - var shouldCallBuildpackDownloaderWith = func(uri string, buildpackDownloadOptions buildpack.DownloadOptions) { + var shouldCallBuildpackDownloaderWith = func(uri string, _ buildpack.DownloadOptions) { buildpack := createBuildpack(dist.BuildpackDescriptor{ WithAPI: api.MustParse("0.3"), WithInfo: dist.ModuleInfo{ID: "example/foo", Version: "1.1.0"}, @@ -88,6 +90,7 @@ func testCreateBuilder(t *testing.T, when spec.G, it spec.S) { mockController = gomock.NewController(t) mockDownloader = testmocks.NewMockBlobDownloader(mockController) mockImageFetcher = testmocks.NewMockImageFetcher(mockController) + mockIndexFactory = testmocks.NewMockIndexFactory(mockController) mockImageFactory = testmocks.NewMockImageFactory(mockController) mockDockerClient = testmocks.NewMockCommonAPIClient(mockController) mockBuildpackDownloader = testmocks.NewMockBuildpackDownloader(mockController) @@ -129,56 +132,58 @@ func testCreateBuilder(t *testing.T, when spec.G, it spec.S) { ) h.AssertNil(t, err) - mockDockerClient.EXPECT().Info(context.TODO()).Return(types.Info{OSType: "linux"}, nil).AnyTimes() + mockDockerClient.EXPECT().Info(context.TODO()).Return(system.Info{OSType: "linux"}, nil).AnyTimes() opts = client.CreateBuilderOptions{ RelativeBaseDir: "/", BuilderName: "some/builder", - Config: pubbldr.Config{ - Description: "Some description", - Buildpacks: []pubbldr.ModuleConfig{ - { - ModuleInfo: dist.ModuleInfo{ID: "bp.one", Version: "1.2.3", Homepage: "http://one.buildpack"}, - ImageOrURI: dist.ImageOrURI{ - BuildpackURI: dist.BuildpackURI{ - URI: "https://example.fake/bp-one.tgz", + Config: pubbldr.MultiArchConfig{ + Config: pubbldr.Config{ + Description: "Some description", + Buildpacks: []pubbldr.ModuleConfig{ + { + ModuleInfo: dist.ModuleInfo{ID: "bp.one", Version: "1.2.3", Homepage: "http://one.buildpack"}, + ImageOrURI: dist.ImageOrURI{ + BuildpackURI: dist.BuildpackURI{ + URI: "https://example.fake/bp-one.tgz", + }, }, }, }, - }, - Extensions: []pubbldr.ModuleConfig{ - { - ModuleInfo: dist.ModuleInfo{ID: "ext.one", Version: "1.2.3", Homepage: "http://one.extension"}, - ImageOrURI: dist.ImageOrURI{ - BuildpackURI: dist.BuildpackURI{ - URI: "https://example.fake/ext-one.tgz", + Extensions: []pubbldr.ModuleConfig{ + { + ModuleInfo: dist.ModuleInfo{ID: "ext.one", Version: "1.2.3", Homepage: "http://one.extension"}, + ImageOrURI: dist.ImageOrURI{ + BuildpackURI: dist.BuildpackURI{ + URI: "https://example.fake/ext-one.tgz", + }, }, }, }, + Order: []dist.OrderEntry{{ + Group: []dist.ModuleRef{ + {ModuleInfo: dist.ModuleInfo{ID: "bp.one", Version: "1.2.3"}, Optional: false}, + }}, + }, + OrderExtensions: []dist.OrderEntry{{ + Group: []dist.ModuleRef{ + {ModuleInfo: dist.ModuleInfo{ID: "ext.one", Version: "1.2.3"}, Optional: true}, + }}, + }, + Stack: pubbldr.StackConfig{ + ID: "some.stack.id", + }, + Run: pubbldr.RunConfig{ + Images: []pubbldr.RunImageConfig{{ + Image: "some/run-image", + Mirrors: []string{"localhost:5000/some/run-image"}, + }}, + }, + Build: pubbldr.BuildConfig{ + Image: "some/build-image", + }, + Lifecycle: pubbldr.LifecycleConfig{URI: "file:///some-lifecycle"}, }, - Order: []dist.OrderEntry{{ - Group: []dist.ModuleRef{ - {ModuleInfo: dist.ModuleInfo{ID: "bp.one", Version: "1.2.3"}, Optional: false}, - }}, - }, - OrderExtensions: []dist.OrderEntry{{ - Group: []dist.ModuleRef{ - {ModuleInfo: dist.ModuleInfo{ID: "ext.one", Version: "1.2.3"}, Optional: true}, - }}, - }, - Stack: pubbldr.StackConfig{ - ID: "some.stack.id", - }, - Run: pubbldr.RunConfig{ - Images: []pubbldr.RunImageConfig{{ - Image: "some/run-image", - Mirrors: []string{"localhost:5000/some/run-image"}, - }}, - }, - Build: pubbldr.BuildConfig{ - Image: "some/build-image", - }, - Lifecycle: pubbldr.LifecycleConfig{URI: "file:///some-lifecycle"}, }, Publish: false, PullPolicy: image.PullAlways, @@ -512,6 +517,153 @@ func testCreateBuilder(t *testing.T, when spec.G, it spec.S) { }) }) + when("creating the multi arch base builder", func() { + var err error + it.Before(func() { + subject, err = client.NewClient( + client.WithLogger(logger), + client.WithDownloader(mockDownloader), + client.WithImageFactory(mockImageFactory), + client.WithIndexFactory(mockIndexFactory), + client.WithFetcher(mockImageFetcher), + client.WithDockerClient(mockDockerClient), + client.WithBuildpackDownloader(mockBuildpackDownloader), + client.WithExperimental(true), + ) + h.AssertNil(t, err) + + opts.Publish = true + opts.Config.WithTargets = []dist.Target{ + { + OS: "linux", + Arch: "arm", + ArchVariant: "v6", + Distributions: []dist.Distribution{ + { + Name: "ubuntu", + Versions: []string{"22.04"}, + }, + { + Name: "debian", + Versions: []string{"10.0"}, + }, + }, + }, + { + OS: "linux", + Arch: "amd64", + Distributions: []dist.Distribution{ + { + Name: "ubuntu", + Versions: []string{"22.04"}, + }, + { + Name: "debian", + Versions: []string{"10.0"}, + }, + }, + Specs: dist.TargetSpecs{ + Features: []string{"feature1", "feature2"}, + OSFeatures: []string{"osFeature1", "osFeature2"}, + URLs: []string{"url1", "url2"}, + Annotations: map[string]string{"key1": "value1", "key2": "value2"}, + OSVersion: "latest", + }, + }, + } + }) + when("build image not found", func() { + it("should fail", func() { + prepareFetcherWithRunImages() + mockIndexFactory.EXPECT().FetchIndex(gomock.Any(), gomock.Any()).Return(nil, errors.New("the given reference('index.docker.io/some/build-image:latest') either doesn't exist or not referencing IndexManifest")) + + err := subject.CreateMultiArchBuilder(context.TODO(), opts) + h.AssertError(t, err, "the given reference('index.docker.io/some/build-image:latest') either doesn't exist or not referencing IndexManifest") + }) + }) + + when("build image isn't a valid image", func() { + it("should fail", func() { + // fakeImage := fakeBadImageStruct{} + + prepareFetcherWithRunImages() + mockIndexFactory.EXPECT().LoadIndex(gomock.Any(), gomock.Any(), gomock.Any()).Return(gomock.Any().(imgutil.Image), nil) + + err := subject.CreateMultiArchBuilder(context.TODO(), opts) + h.AssertError(t, err, "failed to create builder: invalid build-image") + }) + }) + + when("windows containers", func() { + when("experimental enabled", func() { + it("succeeds", func() { + opts.Config.Extensions = nil // TODO: downloading extensions doesn't work yet; to be implemented in https://github.com/buildpacks/pack/issues/1489 + opts.Config.OrderExtensions = nil // TODO: downloading extensions doesn't work yet; to be implemented in https://github.com/buildpacks/pack/issues/1489 + packClientWithExperimental, err := client.NewClient( + client.WithLogger(logger), + client.WithDownloader(mockDownloader), + client.WithImageFactory(mockImageFactory), + client.WithFetcher(mockImageFetcher), + client.WithExperimental(true), + ) + h.AssertNil(t, err) + + prepareFetcherWithRunImages() + + h.AssertNil(t, fakeBuildImage.SetOS("windows")) + mockImageFetcher.EXPECT().Fetch(gomock.Any(), "some/build-image", image.FetchOptions{Daemon: true, PullPolicy: image.PullAlways}).Return(fakeBuildImage, nil) + + err = packClientWithExperimental.CreateMultiArchBuilder(context.TODO(), opts) + h.AssertNil(t, err) + }) + }) + + when("experimental disabled", func() { + it("fails", func() { + prepareFetcherWithRunImages() + + h.AssertNil(t, fakeBuildImage.SetOS("windows")) + mockImageFetcher.EXPECT().Fetch(gomock.Any(), "some/build-image", image.FetchOptions{Daemon: true, PullPolicy: image.PullAlways}).Return(fakeBuildImage, nil) + + err := subject.CreateMultiArchBuilder(context.TODO(), opts) + h.AssertError(t, err, "failed to create builder: Windows containers support is currently experimental.") + }) + }) + }) + + when("error downloading lifecycle", func() { + it("should fail", func() { + prepareFetcherWithBuildImage() + prepareFetcherWithRunImages() + opts.Config.Lifecycle.URI = "fake" + + uri, err := paths.FilePathToURI(opts.Config.Lifecycle.URI, opts.RelativeBaseDir) + h.AssertNil(t, err) + + mockDownloader.EXPECT().Download(gomock.Any(), uri).Return(nil, errors.New("error here")).AnyTimes() + + err = subject.CreateMultiArchBuilder(context.TODO(), opts) + h.AssertError(t, err, "downloading lifecycle") + }) + }) + + when("lifecycle isn't a valid lifecycle", func() { + it("should fail", func() { + prepareFetcherWithBuildImage() + prepareFetcherWithRunImages() + opts.Config.Lifecycle.URI = "fake" + + uri, err := paths.FilePathToURI(opts.Config.Lifecycle.URI, opts.RelativeBaseDir) + h.AssertNil(t, err) + + mockDownloader.EXPECT().Download(gomock.Any(), uri).Return(blob.NewBlob(filepath.Join("testdata", "empty-file")), nil).AnyTimes() + + err = subject.CreateMultiArchBuilder(context.TODO(), opts) + h.AssertError(t, err, "invalid lifecycle") + }) + }) + }) + when("only lifecycle version is provided", func() { it("should download from predetermined uri", func() { prepareFetcherWithBuildImage() @@ -1043,39 +1195,41 @@ func testCreateBuilder(t *testing.T, when spec.G, it spec.S) { opts = client.CreateBuilderOptions{ RelativeBaseDir: "/", BuilderName: "some/builder", - Config: pubbldr.Config{ - Description: "Some description", - Buildpacks: []pubbldr.ModuleConfig{ - { - ModuleInfo: dist.ModuleInfo{ID: "flatten/bp-1", Version: "1", Homepage: "http://buildpack-1"}, - ImageOrURI: dist.ImageOrURI{ - BuildpackURI: dist.BuildpackURI{ - URI: "https://example.fake/flatten-bp-1.tgz", + Config: pubbldr.MultiArchConfig{ + Config: pubbldr.Config{ + Description: "Some description", + Buildpacks: []pubbldr.ModuleConfig{ + { + ModuleInfo: dist.ModuleInfo{ID: "flatten/bp-1", Version: "1", Homepage: "http://buildpack-1"}, + ImageOrURI: dist.ImageOrURI{ + BuildpackURI: dist.BuildpackURI{ + URI: "https://example.fake/flatten-bp-1.tgz", + }, }, }, }, + Order: []dist.OrderEntry{{ + Group: []dist.ModuleRef{ + {ModuleInfo: dist.ModuleInfo{ID: "flatten/bp-2", Version: "2"}, Optional: false}, + {ModuleInfo: dist.ModuleInfo{ID: "flatten/bp-4", Version: "4"}, Optional: false}, + {ModuleInfo: dist.ModuleInfo{ID: "flatten/bp-6", Version: "6"}, Optional: false}, + {ModuleInfo: dist.ModuleInfo{ID: "flatten/bp-7", Version: "7"}, Optional: false}, + }}, + }, + Stack: pubbldr.StackConfig{ + ID: "some.stack.id", + }, + Run: pubbldr.RunConfig{ + Images: []pubbldr.RunImageConfig{{ + Image: "some/run-image", + Mirrors: []string{"localhost:5000/some/run-image"}, + }}, + }, + Build: pubbldr.BuildConfig{ + Image: "some/build-image", + }, + Lifecycle: pubbldr.LifecycleConfig{URI: "file:///some-lifecycle"}, }, - Order: []dist.OrderEntry{{ - Group: []dist.ModuleRef{ - {ModuleInfo: dist.ModuleInfo{ID: "flatten/bp-2", Version: "2"}, Optional: false}, - {ModuleInfo: dist.ModuleInfo{ID: "flatten/bp-4", Version: "4"}, Optional: false}, - {ModuleInfo: dist.ModuleInfo{ID: "flatten/bp-6", Version: "6"}, Optional: false}, - {ModuleInfo: dist.ModuleInfo{ID: "flatten/bp-7", Version: "7"}, Optional: false}, - }}, - }, - Stack: pubbldr.StackConfig{ - ID: "some.stack.id", - }, - Run: pubbldr.RunConfig{ - Images: []pubbldr.RunImageConfig{{ - Image: "some/run-image", - Mirrors: []string{"localhost:5000/some/run-image"}, - }}, - }, - Build: pubbldr.BuildConfig{ - Image: "some/build-image", - }, - Lifecycle: pubbldr.LifecycleConfig{URI: "file:///some-lifecycle"}, }, Publish: false, PullPolicy: image.PullAlways, diff --git a/pkg/client/create_manifest.go b/pkg/client/create_manifest.go new file mode 100644 index 0000000000..4fa01fc362 --- /dev/null +++ b/pkg/client/create_manifest.go @@ -0,0 +1,86 @@ +package client + +import ( + "context" + "fmt" + + "github.com/buildpacks/imgutil" + "github.com/buildpacks/imgutil/index" + ggcrName "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/v1/types" + "golang.org/x/sync/errgroup" +) + +type CreateManifestOptions struct { + Format, Registry string + Insecure, Publish, All bool +} + +// CreateManifest implements commands.PackClient. +func (c *Client) CreateManifest(ctx context.Context, name string, images []string, opts CreateManifestOptions) (err error) { + ops := parseOptsToIndexOptions(opts) + _, err = c.indexFactory.LoadIndex(name, ops...) + if err == nil { + return fmt.Errorf("exits in your local storage, use 'pack manifest remove' if you want to delete it") + } + + if _, err = c.indexFactory.CreateIndex(name, ops...); err != nil { + return err + } + + index, err := c.indexFactory.LoadIndex(name, ops...) + if err != nil { + return err + } + + var errGroup, _ = errgroup.WithContext(ctx) + for _, img := range images { + img := img + errGroup.Go(func() error { + return addImage(index, img, opts) + }) + } + + if err = errGroup.Wait(); err != nil { + return err + } + + if err = index.Save(); err != nil { + return err + } + + fmt.Printf("successfully created index: '%s'\n", name) + if !opts.Publish { + return nil + } + + if err = index.Push(imgutil.WithInsecure(opts.Insecure)); err != nil { + return err + } + + fmt.Printf("successfully pushed '%s' to registry \n", name) + return nil +} + +func parseOptsToIndexOptions(opts CreateManifestOptions) (idxOpts []index.Option) { + var format types.MediaType + switch opts.Format { + case "oci": + format = types.OCIImageIndex + default: + format = types.DockerManifestList + } + return []index.Option{ + index.WithFormat(format), + index.WithInsecure(opts.Insecure), + } +} + +func addImage(index imgutil.ImageIndex, img string, opts CreateManifestOptions) error { + ref, err := ggcrName.ParseReference(img) + if err != nil { + return err + } + + return index.Add(ref, imgutil.WithAll(opts.All)) +} diff --git a/pkg/client/create_manifest_test.go b/pkg/client/create_manifest_test.go new file mode 100644 index 0000000000..51c5cc7d55 --- /dev/null +++ b/pkg/client/create_manifest_test.go @@ -0,0 +1,173 @@ +package client + +import ( + "bytes" + "context" + "errors" + "os" + "testing" + + "github.com/golang/mock/gomock" + "github.com/google/go-containerregistry/pkg/authn" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/types" + "github.com/heroku/color" + "github.com/sclevine/spec" + "github.com/sclevine/spec/report" + + "github.com/buildpacks/imgutil" + "github.com/buildpacks/imgutil/fakes" + + "github.com/buildpacks/pack/pkg/logging" + "github.com/buildpacks/pack/pkg/testmocks" + h "github.com/buildpacks/pack/testhelpers" +) + +func TestCreateManifest(t *testing.T) { + color.Disable(true) + defer color.Disable(false) + spec.Run(t, "build", testCreateManifest, spec.Report(report.Terminal{})) +} + +func testCreateManifest(t *testing.T, when spec.G, it spec.S) { + var ( + mockController *gomock.Controller + mockIndexFactory *testmocks.MockIndexFactory + out bytes.Buffer + logger logging.Logger + subject *Client + err error + tmpDir string + ) + when("#CreateManifest", func() { + it.Before(func() { + logger = logging.NewLogWithWriters(&out, &out, logging.WithVerbose()) + mockController = gomock.NewController(t) + mockIndexFactory = testmocks.NewMockIndexFactory(mockController) + + subject, err = NewClient( + WithLogger(logger), + WithIndexFactory(mockIndexFactory), + WithExperimental(true), + WithKeychain(authn.DefaultKeychain), + ) + h.AssertSameInstance(t, mockIndexFactory, subject.indexFactory) + h.AssertNil(t, err) + }) + it.After(func() { + mockController.Finish() + h.AssertNil(t, os.RemoveAll(tmpDir)) + }) + it("create manifest", func() { + prepareMockImageFactoryForValidCreateIndex(t, mockIndexFactory) + err := subject.CreateManifest( + context.TODO(), + "pack/imgutil", + []string{"busybox:1.36-musl"}, + CreateManifestOptions{ + Insecure: true, + }, + ) + h.AssertNil(t, err) + }) + it("create manifests ignoring all option", func() { + prepareMockImageFactoryForValidCreateIndex(t, mockIndexFactory) + err := subject.CreateManifest( + context.TODO(), + "pack/imgutil", + []string{"busybox:1.36-musl"}, + CreateManifestOptions{ + Insecure: true, + All: true, + }, + ) + h.AssertNil(t, err) + }) + it("create manifests with all nested images", func() { + prepareMockImageFactoryForValidCreateIndexWithAll(t, mockIndexFactory) + err := subject.CreateManifest( + context.TODO(), + "pack/imgutil", + []string{"busybox:1.36-musl"}, + CreateManifestOptions{ + Insecure: true, + All: true, + }, + ) + h.AssertNil(t, err) + }) + it("return an error when index exists already", func() { + prepareMockImageFactoryForInvalidCreateIndexExistsLoadIndex(t, mockIndexFactory) + err := subject.CreateManifest( + context.TODO(), + "pack/imgutil", + []string{"busybox:1.36-musl"}, + CreateManifestOptions{ + Insecure: true, + }, + ) + h.AssertEq(t, err.Error(), "exits in your local storage, use 'pack manifest remove' if you want to delete it") + }) + }) +} + +func prepareMockImageFactoryForValidCreateIndex(t *testing.T, mockIndexFactory *testmocks.MockIndexFactory) { + idx, err := fakes.NewIndex(types.OCIImageIndex, 1024, 1, 1, v1.Descriptor{}) + h.AssertNil(t, err) + + mockIndexFactory.EXPECT(). + CreateIndex(gomock.Any(), gomock.Any()). + AnyTimes(). + Return(idx, err) + mockIndexFactory.EXPECT(). + LoadIndex(gomock.Any(), gomock.Any()). + AnyTimes(). + After( + mockIndexFactory.EXPECT(). + LoadIndex(gomock.Any(), gomock.Any()). + Times(1). + Return( + imgutil.ImageIndex(nil), + errors.New("no image exists"), + ), + ). + Return(idx, err) +} + +func prepareMockImageFactoryForValidCreateIndexWithAll(t *testing.T, mockIndexFactory *testmocks.MockIndexFactory) { + idx, err := fakes.NewIndex(types.OCIImageIndex, 1024, 1, 1, v1.Descriptor{}) + h.AssertNil(t, err) + + mockIndexFactory.EXPECT(). + CreateIndex(gomock.Any(), gomock.Any()). + AnyTimes(). + Return(idx, err) + mockIndexFactory.EXPECT(). + LoadIndex(gomock.Any(), gomock.Any()). + AnyTimes(). + After( + mockIndexFactory.EXPECT(). + LoadIndex(gomock.Any(), gomock.Any()). + Times(1). + Return( + imgutil.ImageIndex(nil), + errors.New("no image exists"), + ), + ). + Return(idx, err) +} + +func prepareMockImageFactoryForInvalidCreateIndexExistsLoadIndex(t *testing.T, mockIndexFactory *testmocks.MockIndexFactory) { + idx, err := fakes.NewIndex(types.OCIImageIndex, 1024, 1, 1, v1.Descriptor{}) + h.AssertNil(t, err) + + mockIndexFactory.EXPECT(). + CreateIndex(gomock.Any(), gomock.Any()). + AnyTimes(). + Return(idx, err) + + mockIndexFactory.EXPECT(). + LoadIndex(gomock.Any(), gomock.Any()). + AnyTimes(). + Return(idx, err) +} diff --git a/pkg/client/docker.go b/pkg/client/docker.go index dfdf2c6139..bb81dc487a 100644 --- a/pkg/client/docker.go +++ b/pkg/client/docker.go @@ -8,6 +8,7 @@ import ( containertypes "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/image" networktypes "github.com/docker/docker/api/types/network" + "github.com/docker/docker/api/types/system" specs "github.com/opencontainers/image-spec/specs-go/v1" ) @@ -18,17 +19,17 @@ type DockerClient interface { ImageTag(ctx context.Context, image, ref string) error ImageLoad(ctx context.Context, input io.Reader, quiet bool) (types.ImageLoadResponse, error) ImageSave(ctx context.Context, images []string) (io.ReadCloser, error) - ImageRemove(ctx context.Context, image string, options types.ImageRemoveOptions) ([]types.ImageDeleteResponseItem, error) + ImageRemove(ctx context.Context, image string, options types.ImageRemoveOptions) ([]image.DeleteResponse, error) ImagePull(ctx context.Context, ref string, options types.ImagePullOptions) (io.ReadCloser, error) - Info(ctx context.Context) (types.Info, error) + Info(ctx context.Context) (system.Info, error) ServerVersion(ctx context.Context) (types.Version, error) VolumeRemove(ctx context.Context, volumeID string, force bool) error ContainerCreate(ctx context.Context, config *containertypes.Config, hostConfig *containertypes.HostConfig, networkingConfig *networktypes.NetworkingConfig, platform *specs.Platform, containerName string) (containertypes.CreateResponse, error) CopyFromContainer(ctx context.Context, container, srcPath string) (io.ReadCloser, types.ContainerPathStat, error) ContainerInspect(ctx context.Context, container string) (types.ContainerJSON, error) - ContainerRemove(ctx context.Context, container string, options types.ContainerRemoveOptions) error + ContainerRemove(ctx context.Context, container string, options containertypes.RemoveOptions) error CopyToContainer(ctx context.Context, container, path string, content io.Reader, options types.CopyToContainerOptions) error ContainerWait(ctx context.Context, container string, condition containertypes.WaitCondition) (<-chan containertypes.WaitResponse, <-chan error) - ContainerAttach(ctx context.Context, container string, options types.ContainerAttachOptions) (types.HijackedResponse, error) - ContainerStart(ctx context.Context, container string, options types.ContainerStartOptions) error + ContainerAttach(ctx context.Context, container string, options containertypes.AttachOptions) (types.HijackedResponse, error) + ContainerStart(ctx context.Context, container string, options containertypes.StartOptions) error } diff --git a/pkg/client/exists_manifest.go b/pkg/client/exists_manifest.go new file mode 100644 index 0000000000..1fc09f5fb0 --- /dev/null +++ b/pkg/client/exists_manifest.go @@ -0,0 +1,17 @@ +package client + +import ( + "context" + "fmt" + + "github.com/pkg/errors" +) + +func (c *Client) ExistsManifest(ctx context.Context, image string) error { + if _, err := c.indexFactory.LoadIndex(image); err != nil { + return errors.Errorf("image '%s' is not found", image) + } + + fmt.Printf("index '%s' exists \n", image) + return nil +} diff --git a/pkg/client/inspect_image.go b/pkg/client/inspect_image.go index e60f444a8f..84fa044301 100644 --- a/pkg/client/inspect_image.go +++ b/pkg/client/inspect_image.go @@ -223,7 +223,7 @@ func getRebasableLabel(labeled dist.Labeled) (bool, error) { return false, err } - if !isPresent && err == nil { + if !isPresent { rebasableOutput = true } diff --git a/pkg/client/inspect_manifest.go b/pkg/client/inspect_manifest.go new file mode 100644 index 0000000000..73f401385d --- /dev/null +++ b/pkg/client/inspect_manifest.go @@ -0,0 +1,20 @@ +package client + +import ( + "context" + "fmt" +) + +// InspectManifest implements commands.PackClient. +func (c *Client) InspectManifest(ctx context.Context, name string) (err error) { + idx, err := c.indexFactory.FindIndex(name) + if err != nil { + return err + } + + if mfest, err := idx.Inspect(); err == nil { + fmt.Println(mfest) + } + + return err +} diff --git a/pkg/client/inspect_manifest_test.go b/pkg/client/inspect_manifest_test.go new file mode 100644 index 0000000000..55a2417918 --- /dev/null +++ b/pkg/client/inspect_manifest_test.go @@ -0,0 +1,104 @@ +package client + +import ( + "bytes" + "context" + "errors" + "os" + "testing" + + "github.com/buildpacks/imgutil" + "github.com/buildpacks/imgutil/fakes" + "github.com/golang/mock/gomock" + "github.com/google/go-containerregistry/pkg/authn" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/types" + "github.com/heroku/color" + "github.com/sclevine/spec" + "github.com/sclevine/spec/report" + + "github.com/buildpacks/pack/pkg/logging" + "github.com/buildpacks/pack/pkg/testmocks" + h "github.com/buildpacks/pack/testhelpers" +) + +func TestInspectManifest(t *testing.T) { + color.Disable(true) + defer color.Disable(false) + spec.Run(t, "build", testInspectManifest, spec.Report(report.Terminal{})) +} + +func testInspectManifest(t *testing.T, when spec.G, it spec.S) { + var ( + mockController *gomock.Controller + mockIndexFactory *testmocks.MockIndexFactory + stdout bytes.Buffer + stderr bytes.Buffer + logger logging.Logger + subject *Client + err error + tmpDir string + ) + + when("#Add", func() { + it.Before(func() { + logger = logging.NewLogWithWriters(&stdout, &stderr, logging.WithVerbose()) + mockController = gomock.NewController(t) + mockIndexFactory = testmocks.NewMockIndexFactory(mockController) + + subject, err = NewClient( + WithLogger(logger), + WithIndexFactory(mockIndexFactory), + WithExperimental(true), + WithKeychain(authn.DefaultKeychain), + ) + h.AssertSameInstance(t, mockIndexFactory, subject.indexFactory) + h.AssertSameInstance(t, subject.logger, logger) + h.AssertNil(t, err) + }) + it.After(func() { + mockController.Finish() + h.AssertNil(t, os.RemoveAll(tmpDir)) + }) + it("should return an error when index not found", func() { + prepareFindIndexWithError(*mockIndexFactory) + + err := subject.InspectManifest( + context.TODO(), + "some/name", + ) + h.AssertEq(t, err.Error(), "index not found") + }) + it("should return formatted IndexManifest", func() { + prepareFindIndex(t, *mockIndexFactory) + + err := subject.InspectManifest( + context.TODO(), + "some/name", + ) + h.AssertNil(t, err) + h.AssertEq(t, stderr.String(), "") + }) + }) +} + +func prepareFindIndexWithError(mockIndexFactory testmocks.MockIndexFactory) { + mockIndexFactory. + EXPECT(). + FindIndex(gomock.Any(), gomock.Any()). + AnyTimes(). + Return(nil, errors.New("index not found")) +} + +func prepareFindIndex(t *testing.T, mockIndexFactory testmocks.MockIndexFactory) imgutil.ImageIndex { + idx, err := fakes.NewIndex(types.OCIImageIndex, 1024, 1, 1, v1.Descriptor{}) + h.AssertNil(t, err) + + mockIndexFactory. + EXPECT(). + FindIndex(gomock.Any(), gomock.Any()). + AnyTimes(). + Return(idx, nil) + + return idx +} diff --git a/pkg/client/new_buildpack_test.go b/pkg/client/new_buildpack_test.go index c2eb0eb6ed..d2ebe2b355 100644 --- a/pkg/client/new_buildpack_test.go +++ b/pkg/client/new_buildpack_test.go @@ -119,7 +119,7 @@ func testNewBuildpack(t *testing.T, when spec.G, it spec.S) { }) } -func assertBuildpackToml(t *testing.T, path string, id string) { +func assertBuildpackToml(t *testing.T, path string, _ string) { buildpackTOML := filepath.Join(path, "buildpack.toml") _, err := os.Stat(buildpackTOML) h.AssertFalse(t, os.IsNotExist(err)) diff --git a/pkg/client/package_buildpack.go b/pkg/client/package_buildpack.go index 49ca63882a..989c0d72b4 100644 --- a/pkg/client/package_buildpack.go +++ b/pkg/client/package_buildpack.go @@ -2,8 +2,16 @@ package client import ( "context" + "fmt" + "path/filepath" + "github.com/google/go-containerregistry/pkg/name" + v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/pkg/errors" + "golang.org/x/sync/errgroup" + + "github.com/buildpacks/imgutil" + "github.com/buildpacks/imgutil/index" pubbldpkg "github.com/buildpacks/pack/buildpackage" "github.com/buildpacks/pack/internal/layer" @@ -11,6 +19,8 @@ import ( "github.com/buildpacks/pack/internal/style" "github.com/buildpacks/pack/pkg/blob" "github.com/buildpacks/pack/pkg/buildpack" + "github.com/buildpacks/pack/pkg/dist" + "github.com/buildpacks/pack/pkg/image" ) @@ -59,6 +69,12 @@ type PackageBuildpackOptions struct { // Map of labels to add to the Buildpack Labels map[string]string + + // Image Version + Version string + + // Index Options instruct how IndexManifest should be created + IndexOptions pubbldpkg.IndexOptions } // PackageBuildpack packages buildpack(s) into either an image or file. @@ -71,8 +87,7 @@ func (c *Client) PackageBuildpack(ctx context.Context, opts PackageBuildpackOpti return NewExperimentError("Windows buildpackage support is currently experimental.") } - err := c.validateOSPlatform(ctx, opts.Config.Platform.OS, opts.Publish, opts.Format) - if err != nil { + if err := c.validateOSPlatform(ctx, opts.Config.Platform.OS, opts.Publish, opts.Format); err != nil { return err } @@ -86,7 +101,7 @@ func (c *Client) PackageBuildpack(ctx context.Context, opts PackageBuildpackOpti packageBuilderOpts = append(packageBuilderOpts, buildpack.DoNotFlatten(opts.FlattenExclude), buildpack.WithLayerWriterFactory(writerFactory), buildpack.WithLogger(c.logger)) } - packageBuilder := buildpack.NewBuilder(c.imageFactory, packageBuilderOpts...) + packageBuilder := buildpack.NewBuilder(c.imageFactory, c.indexFactory, packageBuilderOpts...) bpURI := opts.Config.Buildpack.URI if bpURI == "" { @@ -122,11 +137,22 @@ func (c *Client) PackageBuildpack(ctx context.Context, opts PackageBuildpackOpti packageBuilder.AddDependencies(mainBP, deps) } + if opts.IndexOptions.ImageIndex != nil && opts.Format == FormatFile { + return packageBuilder.SaveAsMultiArchFile(opts.Name, opts.Version, opts.IndexOptions.Targets, opts.IndexOptions.ImageIndex, opts.Labels) + } + switch opts.Format { case FormatFile: - return packageBuilder.SaveAsFile(opts.Name, opts.Config.Platform.OS, opts.Labels) + return packageBuilder.SaveAsFile(opts.Name, opts.Version, opts.IndexOptions.Targets[0], opts.IndexOptions.ImageIndex, opts.Labels) case FormatImage: - _, err = packageBuilder.SaveAsImage(opts.Name, opts.Publish, opts.Config.Platform.OS, opts.Labels) + if _, err = packageBuilder.SaveAsImage(opts.Name, opts.Version, opts.Publish, opts.IndexOptions.Targets[0], opts.IndexOptions.ImageIndex, opts.Labels); err == nil { + version := opts.Version + if version == "" { + version = "latest" + } + fmt.Println("Successfully saved image: " + opts.Name + ":" + version) + return nil + } return errors.Wrapf(err, "saving image") default: return errors.Errorf("unknown format: %s", style.Symbol(opts.Format)) @@ -165,3 +191,186 @@ func (c *Client) validateOSPlatform(ctx context.Context, os string, publish bool return nil } + +// PackageBuildpack packages multiple buildpack(s) into image index with each buildpack into either an image or file. +func (c *Client) PackageMultiArchBuildpack(ctx context.Context, opts PackageBuildpackOptions) error { + if !c.experimental { + return errors.Errorf("packaging %s is currently %s", style.Symbol("multi arch buildpacks"), style.Symbol(("experimental"))) + } + + if opts.IndexOptions.BPConfigs == nil || len(*opts.IndexOptions.BPConfigs) < 2 { + return errors.Errorf("%s must not be nil", style.Symbol("IndexOptions")) + } + + if opts.IndexOptions.PkgConfig == nil { + return errors.Errorf("package configaration is undefined") + } + + bpCfg, err := pubbldpkg.NewConfigReader().ReadBuildpackDescriptor(opts.RelativeBaseDir) + if err != nil { + return fmt.Errorf("cannot read %s file: %s", style.Symbol("buildpack.toml"), style.Symbol(opts.RelativeBaseDir)) + } + + var repoName string + if info := bpCfg.WithInfo; info.Version == "" { + repoName = info.ID + } else { + repoName = info.ID + ":" + info.Version + } + + if err := createImageIndex(c, repoName); err != nil { + return err + } + + pkgConfig, bpConfigs := *opts.IndexOptions.PkgConfig, *opts.IndexOptions.BPConfigs + var ( + errs errgroup.Group + ) + + idx, err := loadImageIndex(c, repoName) + if err != nil { + return err + } + + IndexManifestFn := c.GetIndexManifestFn() + packageMultiArchBuildpackFn := func(bpConfig pubbldpkg.MultiArchBuildpackConfig) error { + if err := bpConfig.CopyBuildpackToml(IndexManifestFn); err != nil { + return err + } + defer bpConfig.CleanBuildpackToml() + + if targets := bpConfig.Targets(); bpConfig.BuildpackType() != pubbldpkg.Composite { + target, distro, version := dist.Target{}, dist.Distribution{}, "" + if len(targets) != 0 { + target = targets[0] + } + + if len(target.Distributions) != 0 { + distro = target.Distributions[0] + } + + if len(distro.Versions) != 0 { + version = distro.Versions[0] + } + + if err := pkgConfig.CopyPackageToml(filepath.Dir(bpConfig.Path()), target, distro.Name, version, IndexManifestFn); err != nil { + return err + } + defer pkgConfig.CleanPackageToml(filepath.Dir(bpConfig.Path()), target, distro.Name, version) + } + + if !opts.Flatten && bpConfig.Flatten { + opts.IndexOptions.Logger.Warn("Flattening a buildpack package could break the distribution specification. Please use it with caution.") + } + + return c.PackageBuildpack(ctx, PackageBuildpackOptions{ + RelativeBaseDir: "", + Name: opts.Name, + Format: opts.Format, + Config: pkgConfig.Config, + Publish: opts.Publish, + PullPolicy: opts.PullPolicy, + Registry: opts.Registry, + Flatten: bpConfig.Flatten, + FlattenExclude: bpConfig.FlattenExclude, + Labels: bpConfig.Labels, + Version: opts.Version, + IndexOptions: pubbldpkg.IndexOptions{ + ImageIndex: idx, + Logger: opts.IndexOptions.Logger, + Targets: bpConfig.Targets(), + }, + }) + } + + for _, bpConfig := range bpConfigs { + c := bpConfig + errs.Go(func() error { + return packageMultiArchBuildpackFn(c) + }) + } + + if err := errs.Wait(); err != nil { + return err + } + + if err := idx.Save(); err != nil { + return err + } + + if !opts.Publish { + return nil + } + + return idx.Push(imgutil.WithInsecure(true), imgutil.WithTags("latest")) +} + +func (c *Client) GetIndexManifestFn() pubbldpkg.GetIndexManifestFn { + if len(c.cachedIndexManifests) == 0 { + c.cachedIndexManifests = make(map[name.Reference]*v1.IndexManifest) + } + + IndexHandlerFn := func(ref name.Reference) (*v1.IndexManifest, error) { + mfest := c.cachedIndexManifests[ref] + if mfest != nil { + return mfest, nil + } + + fetchOpts, err := withOptions([]index.Option{ + index.WithInsecure(true), + }, c.keychain) + if err != nil { + return nil, err + } + + idx, err := c.indexFactory.FetchIndex(ref.Name(), fetchOpts...) + if err != nil { + return nil, errors.Errorf("the given reference(%s) either doesn't exist or not referencing IndexManifest", style.Symbol(ref.Name())) + } + + ii, ok := idx.(*imgutil.ManifestHandler) + if !ok { + return nil, errors.Errorf("unknown handler: %s", style.Symbol("ManifestHandler")) + } + + if mfest, err = ii.IndexManifest(); err != nil { + return mfest, err + } + + c.cachedIndexManifests[ref] = mfest + return mfest, nil + } + + return IndexHandlerFn +} + +func createImageIndex(c *Client, repoName string) (err error) { + opts, err := withOptions([]index.Option{ + index.WithInsecure(true), + }, c.keychain) + if err != nil { + return err + } + + // Delete ImageIndex if already exists + if idx, err := c.indexFactory.LoadIndex(repoName, opts...); err == nil { + if err = idx.Delete(); err != nil { + return err + } + } + + // Create a new ImageIndex. the newly created index has `Image(hash v1.Hash)` `ImageIndex(hash v1.Hash)` that always returns error. + _, err = c.indexFactory.CreateIndex(repoName, opts...) + return err +} + +func loadImageIndex(c *Client, repoName string) (imgutil.ImageIndex, error) { + opts, err := withOptions([]index.Option{ + index.WithInsecure(true), + }, c.keychain) + if err != nil { + return nil, err + } + + return c.indexFactory.LoadIndex(repoName, opts...) +} diff --git a/pkg/client/package_buildpack_test.go b/pkg/client/package_buildpack_test.go index dfeecc98da..f9e9a3e87a 100644 --- a/pkg/client/package_buildpack_test.go +++ b/pkg/client/package_buildpack_test.go @@ -8,10 +8,11 @@ import ( "path/filepath" "testing" + "github.com/BurntSushi/toml" "github.com/buildpacks/imgutil" "github.com/buildpacks/imgutil/fakes" "github.com/buildpacks/lifecycle/api" - "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/system" "github.com/golang/mock/gomock" "github.com/heroku/color" "github.com/sclevine/spec" @@ -19,6 +20,9 @@ import ( "github.com/buildpacks/pack/pkg/archive" + v1 "github.com/google/go-containerregistry/pkg/v1" + ggcrTypes "github.com/google/go-containerregistry/pkg/v1/types" + pubbldpkg "github.com/buildpacks/pack/buildpackage" cfg "github.com/buildpacks/pack/internal/config" ifakes "github.com/buildpacks/pack/internal/fakes" @@ -39,6 +43,20 @@ func TestPackageBuildpack(t *testing.T) { spec.Run(t, "PackageBuildpack", testPackageBuildpack, spec.Parallel(), spec.Report(report.Terminal{})) } +type Identifier struct { + id string +} + +func NewIdentifier(id string) Identifier { + return Identifier{ + id: id, + } +} + +func (i Identifier) String() string { + return i.id +} + func testPackageBuildpack(t *testing.T, when spec.G, it spec.S) { var ( subject *client.Client @@ -46,8 +64,10 @@ func testPackageBuildpack(t *testing.T, when spec.G, it spec.S) { mockDownloader *testmocks.MockBlobDownloader mockImageFactory *testmocks.MockImageFactory mockImageFetcher *testmocks.MockImageFetcher + mockIndexFactory *testmocks.MockIndexFactory mockDockerClient *testmocks.MockCommonAPIClient out bytes.Buffer + imageIdentifier = "74eb48882e835d8767f62940d453eb96ed2737de3a16573881dcea7dea769df7" ) it.Before(func() { @@ -55,6 +75,7 @@ func testPackageBuildpack(t *testing.T, when spec.G, it spec.S) { mockDownloader = testmocks.NewMockBlobDownloader(mockController) mockImageFactory = testmocks.NewMockImageFactory(mockController) mockImageFetcher = testmocks.NewMockImageFetcher(mockController) + mockIndexFactory = testmocks.NewMockIndexFactory(mockController) mockDockerClient = testmocks.NewMockCommonAPIClient(mockController) var err error @@ -62,8 +83,10 @@ func testPackageBuildpack(t *testing.T, when spec.G, it spec.S) { client.WithLogger(logging.NewLogWithWriters(&out, &out)), client.WithDownloader(mockDownloader), client.WithImageFactory(mockImageFactory), + client.WithIndexFactory(mockIndexFactory), client.WithFetcher(mockImageFetcher), client.WithDockerClient(mockDockerClient), + client.WithExperimental(true), ) h.AssertNil(t, err) }) @@ -80,6 +103,14 @@ func testPackageBuildpack(t *testing.T, when spec.G, it spec.S) { return url } + createMultiArchBuildpack := func(descriptor dist.BuildpackDescriptor) { + bp, err := ifakes.NewFakeBuildpackBlob(&descriptor, 0644) + h.AssertNil(t, err) + // url := fmt.Sprintf("https://example.com/bp.%s.tgz", h.RandString(12)) + mockDownloader.EXPECT().Download(gomock.Any(), gomock.Any()).Return(bp, nil).AnyTimes() + // return url + } + when("buildpack has issues", func() { when("buildpack has no URI", func() { it("should fail", func() { @@ -89,6 +120,9 @@ func testPackageBuildpack(t *testing.T, when spec.G, it spec.S) { Platform: dist.Platform{OS: "linux"}, Buildpack: dist.BuildpackURI{URI: ""}, }, + IndexOptions: pubbldpkg.IndexOptions{ + Targets: []dist.Target{{OS: "linux"}}, + }, Publish: true, }) h.AssertError(t, err, "buildpack URI must be provided") @@ -106,6 +140,9 @@ func testPackageBuildpack(t *testing.T, when spec.G, it spec.S) { Platform: dist.Platform{OS: "linux"}, Buildpack: dist.BuildpackURI{URI: bpURL}, }, + IndexOptions: pubbldpkg.IndexOptions{ + Targets: []dist.Target{{OS: "linux"}}, + }, Publish: true, }) h.AssertError(t, err, "downloading buildpack") @@ -124,6 +161,9 @@ func testPackageBuildpack(t *testing.T, when spec.G, it spec.S) { Platform: dist.Platform{OS: "linux"}, Buildpack: dist.BuildpackURI{URI: bpURL}, }, + IndexOptions: pubbldpkg.IndexOptions{ + Targets: []dist.Target{{OS: "linux"}}, + }, Publish: true, }) h.AssertError(t, err, "creating buildpack") @@ -137,7 +177,7 @@ func testPackageBuildpack(t *testing.T, when spec.G, it spec.S) { dependencyPath := "http://example.com/flawed.file" mockDownloader.EXPECT().Download(gomock.Any(), dependencyPath).Return(blob.NewBlob("no-file.txt"), nil).AnyTimes() - mockDockerClient.EXPECT().Info(context.TODO()).Return(types.Info{OSType: "linux"}, nil).AnyTimes() + mockDockerClient.EXPECT().Info(context.TODO()).Return(system.Info{OSType: "linux"}, nil).AnyTimes() packageDescriptor := dist.BuildpackDescriptor{ WithAPI: api.MustParse("0.2"), @@ -157,6 +197,9 @@ func testPackageBuildpack(t *testing.T, when spec.G, it spec.S) { Buildpack: dist.BuildpackURI{URI: createBuildpack(packageDescriptor)}, Dependencies: []dist.ImageOrURI{{BuildpackURI: dist.BuildpackURI{URI: dependencyPath}}}, }, + IndexOptions: pubbldpkg.IndexOptions{ + Targets: []dist.Target{{OS: "linux"}}, + }, Publish: false, PullPolicy: image.PullAlways, }) @@ -171,7 +214,7 @@ func testPackageBuildpack(t *testing.T, when spec.G, it spec.S) { it("creates package image based on daemon OS", func() { for _, daemonOS := range []string{"linux", "windows"} { localMockDockerClient := testmocks.NewMockCommonAPIClient(mockController) - localMockDockerClient.EXPECT().Info(context.TODO()).Return(types.Info{OSType: daemonOS}, nil).AnyTimes() + localMockDockerClient.EXPECT().Info(context.TODO()).Return(system.Info{OSType: daemonOS}, nil).AnyTimes() packClientWithExperimental, err := client.NewClient( client.WithDockerClient(localMockDockerClient), @@ -181,8 +224,8 @@ func testPackageBuildpack(t *testing.T, when spec.G, it spec.S) { ) h.AssertNil(t, err) - fakeImage := fakes.NewImage("basic/package-"+h.RandString(12), "", nil) - mockImageFactory.EXPECT().NewImage(fakeImage.Name(), true, daemonOS).Return(fakeImage, nil) + fakeImage := fakes.NewImage("basic/package-"+h.RandString(12), "", NewIdentifier(imageIdentifier)) + mockImageFactory.EXPECT().NewImage(fakeImage.Name(), true, v1.Platform{OS: daemonOS}).Return(fakeImage, nil) fakeBlob := blob.NewBlob(filepath.Join("testdata", "empty-file")) bpURL := fmt.Sprintf("https://example.com/bp.%s.tgz", h.RandString(12)) @@ -199,6 +242,9 @@ func testPackageBuildpack(t *testing.T, when spec.G, it spec.S) { WithStacks: []dist.Stack{{ID: "some.stack.id"}}, })}, }, + IndexOptions: pubbldpkg.IndexOptions{ + Targets: []dist.Target{{OS: daemonOS}}, + }, PullPolicy: image.PullNever, })) } @@ -225,7 +271,7 @@ func testPackageBuildpack(t *testing.T, when spec.G, it spec.S) { it("fails for mismatched platform and daemon os", func() { windowsMockDockerClient := testmocks.NewMockCommonAPIClient(mockController) - windowsMockDockerClient.EXPECT().Info(context.TODO()).Return(types.Info{OSType: "windows"}, nil).AnyTimes() + windowsMockDockerClient.EXPECT().Info(context.TODO()).Return(system.Info{OSType: "windows"}, nil).AnyTimes() packClientWithoutExperimental, err := client.NewClient( client.WithDockerClient(windowsMockDockerClient), @@ -249,10 +295,10 @@ func testPackageBuildpack(t *testing.T, when spec.G, it spec.S) { var nestedPackage *fakes.Image it.Before(func() { - nestedPackage = fakes.NewImage("nested/package-"+h.RandString(12), "", nil) - mockImageFactory.EXPECT().NewImage(nestedPackage.Name(), false, "linux").Return(nestedPackage, nil) + nestedPackage = fakes.NewImage("nested/package-"+h.RandString(12), "", NewIdentifier(imageIdentifier)) + mockImageFactory.EXPECT().NewImage(nestedPackage.Name(), false, v1.Platform{OS: "linux"}).Return(nestedPackage, nil) - mockDockerClient.EXPECT().Info(context.TODO()).Return(types.Info{OSType: "linux"}, nil).AnyTimes() + mockDockerClient.EXPECT().Info(context.TODO()).Return(system.Info{OSType: "linux"}, nil).AnyTimes() h.AssertNil(t, subject.PackageBuildpack(context.TODO(), client.PackageBuildpackOptions{ Name: nestedPackage.Name(), @@ -264,6 +310,9 @@ func testPackageBuildpack(t *testing.T, when spec.G, it spec.S) { WithStacks: []dist.Stack{{ID: "some.stack.id"}}, })}, }, + IndexOptions: pubbldpkg.IndexOptions{ + Targets: []dist.Target{{OS: "linux"}}, + }, Publish: true, PullPolicy: image.PullAlways, })) @@ -278,14 +327,14 @@ func testPackageBuildpack(t *testing.T, when spec.G, it spec.S) { } shouldCreateLocalPackage := func() imgutil.Image { - img := fakes.NewImage("some/package-"+h.RandString(12), "", nil) - mockImageFactory.EXPECT().NewImage(img.Name(), true, "linux").Return(img, nil) + img := fakes.NewImage("some/package-"+h.RandString(12), "", NewIdentifier(imageIdentifier)) + mockImageFactory.EXPECT().NewImage(img.Name(), true, v1.Platform{OS: "linux"}).Return(img, nil) return img } shouldCreateRemotePackage := func() *fakes.Image { - img := fakes.NewImage("some/package-"+h.RandString(12), "", nil) - mockImageFactory.EXPECT().NewImage(img.Name(), false, "linux").Return(img, nil) + img := fakes.NewImage("some/package-"+h.RandString(12), "", NewIdentifier(imageIdentifier)) + mockImageFactory.EXPECT().NewImage(img.Name(), false, v1.Platform{OS: "linux"}).Return(img, nil) return img } @@ -310,6 +359,9 @@ func testPackageBuildpack(t *testing.T, when spec.G, it spec.S) { })}, Dependencies: []dist.ImageOrURI{{ImageRef: dist.ImageRef{ImageName: nestedPackage.Name()}}}, }, + IndexOptions: pubbldpkg.IndexOptions{ + Targets: []dist.Target{{OS: "linux"}}, + }, Publish: false, PullPolicy: image.PullAlways, })) @@ -337,6 +389,9 @@ func testPackageBuildpack(t *testing.T, when spec.G, it spec.S) { })}, Dependencies: []dist.ImageOrURI{{ImageRef: dist.ImageRef{ImageName: nestedPackage.Name()}}}, }, + IndexOptions: pubbldpkg.IndexOptions{ + Targets: []dist.Target{{OS: "linux"}}, + }, Publish: true, PullPolicy: image.PullAlways, })) @@ -364,6 +419,9 @@ func testPackageBuildpack(t *testing.T, when spec.G, it spec.S) { })}, Dependencies: []dist.ImageOrURI{{ImageRef: dist.ImageRef{ImageName: nestedPackage.Name()}}}, }, + IndexOptions: pubbldpkg.IndexOptions{ + Targets: []dist.Target{{OS: "linux"}}, + }, Publish: true, PullPolicy: image.PullNever, })) @@ -385,6 +443,9 @@ func testPackageBuildpack(t *testing.T, when spec.G, it spec.S) { })}, Dependencies: []dist.ImageOrURI{{ImageRef: dist.ImageRef{ImageName: nestedPackage.Name()}}}, }, + IndexOptions: pubbldpkg.IndexOptions{ + Targets: []dist.Target{{OS: "linux"}}, + }, Publish: false, PullPolicy: image.PullNever, }), "not found") @@ -394,10 +455,10 @@ func testPackageBuildpack(t *testing.T, when spec.G, it spec.S) { when("nested package is not a valid package", func() { it("should error", func() { - notPackageImage := fakes.NewImage("not/package", "", nil) + notPackageImage := fakes.NewImage("not/package", "", NewIdentifier(imageIdentifier)) mockImageFetcher.EXPECT().Fetch(gomock.Any(), notPackageImage.Name(), image.FetchOptions{Daemon: true, PullPolicy: image.PullAlways}).Return(notPackageImage, nil) - mockDockerClient.EXPECT().Info(context.TODO()).Return(types.Info{OSType: "linux"}, nil).AnyTimes() + mockDockerClient.EXPECT().Info(context.TODO()).Return(system.Info{OSType: "linux"}, nil).AnyTimes() h.AssertError(t, subject.PackageBuildpack(context.TODO(), client.PackageBuildpackOptions{ Name: "some/package", @@ -410,6 +471,9 @@ func testPackageBuildpack(t *testing.T, when spec.G, it spec.S) { })}, Dependencies: []dist.ImageOrURI{{ImageRef: dist.ImageRef{ImageName: notPackageImage.Name()}}}, }, + IndexOptions: pubbldpkg.IndexOptions{ + Targets: []dist.Target{{OS: "linux"}}, + }, Publish: false, PullPolicy: image.PullAlways, }), "extracting buildpacks from 'not/package': could not find label 'io.buildpacks.buildpackage.metadata'") @@ -452,12 +516,12 @@ func testPackageBuildpack(t *testing.T, when spec.G, it spec.S) { ) h.AssertNil(t, err) - mockDockerClient.EXPECT().Info(context.TODO()).Return(types.Info{OSType: "linux"}, nil).AnyTimes() + mockDockerClient.EXPECT().Info(context.TODO()).Return(system.Info{OSType: "linux"}, nil).AnyTimes() name := "basic/package-" + h.RandString(12) - fakeImage := fakes.NewImage(name, "", nil) + fakeImage := fakes.NewImage(name, "", NewIdentifier(imageIdentifier)) fakeLayerImage = &h.FakeAddedLayerImage{Image: fakeImage} - mockImageFactory.EXPECT().NewImage(fakeLayerImage.Name(), true, "linux").Return(fakeLayerImage, nil) + mockImageFactory.EXPECT().NewImage(fakeLayerImage.Name(), true, v1.Platform{OS: "linux"}).Return(fakeLayerImage, nil) mockImageFetcher.EXPECT().Fetch(gomock.Any(), name, gomock.Any()).Return(fakeLayerImage, nil).AnyTimes() blob1 := blob.NewBlob(filepath.Join("testdata", "buildpack-flatten", "buildpack-1")) @@ -497,6 +561,9 @@ func testPackageBuildpack(t *testing.T, when spec.G, it spec.S) { {BuildpackURI: dist.BuildpackURI{URI: "https://example.fake/flatten-bp-3.tgz"}}, }, }, + IndexOptions: pubbldpkg.IndexOptions{ + Targets: []dist.Target{{OS: "linux"}}, + }, PullPolicy: image.PullNever, Flatten: true, } @@ -524,7 +591,7 @@ func testPackageBuildpack(t *testing.T, when spec.G, it spec.S) { for _, imageOS := range []string{"linux", "windows"} { localMockDockerClient := testmocks.NewMockCommonAPIClient(mockController) - localMockDockerClient.EXPECT().Info(context.TODO()).Return(types.Info{OSType: imageOS}, nil).AnyTimes() + localMockDockerClient.EXPECT().Info(context.TODO()).Return(system.Info{OSType: imageOS}, nil).AnyTimes() packClientWithExperimental, err := client.NewClient( client.WithDockerClient(localMockDockerClient), @@ -550,6 +617,9 @@ func testPackageBuildpack(t *testing.T, when spec.G, it spec.S) { WithStacks: []dist.Stack{{ID: "some.stack.id"}}, })}, }, + IndexOptions: pubbldpkg.IndexOptions{ + Targets: []dist.Target{{OS: imageOS}}, + }, PullPolicy: image.PullNever, })) } @@ -593,8 +663,8 @@ func testPackageBuildpack(t *testing.T, when spec.G, it spec.S) { when("dependencies are packaged buildpack image", func() { it.Before(func() { - nestedPackage = fakes.NewImage("nested/package-"+h.RandString(12), "", nil) - mockImageFactory.EXPECT().NewImage(nestedPackage.Name(), false, "linux").Return(nestedPackage, nil) + nestedPackage = fakes.NewImage("nested/package-"+h.RandString(12), "", NewIdentifier(imageIdentifier)) + mockImageFactory.EXPECT().NewImage(nestedPackage.Name(), false, v1.Platform{OS: "linux"}).Return(nestedPackage, nil) h.AssertNil(t, subject.PackageBuildpack(context.TODO(), client.PackageBuildpackOptions{ Name: nestedPackage.Name(), @@ -602,6 +672,9 @@ func testPackageBuildpack(t *testing.T, when spec.G, it spec.S) { Platform: dist.Platform{OS: "linux"}, Buildpack: dist.BuildpackURI{URI: createBuildpack(childDescriptor)}, }, + IndexOptions: pubbldpkg.IndexOptions{ + Targets: []dist.Target{{OS: "linux"}}, + }, Publish: true, PullPolicy: image.PullAlways, })) @@ -619,6 +692,9 @@ func testPackageBuildpack(t *testing.T, when spec.G, it spec.S) { Buildpack: dist.BuildpackURI{URI: createBuildpack(packageDescriptor)}, Dependencies: []dist.ImageOrURI{{ImageRef: dist.ImageRef{ImageName: nestedPackage.Name()}}}, }, + IndexOptions: pubbldpkg.IndexOptions{ + Targets: []dist.Target{{OS: "linux"}}, + }, Publish: false, PullPolicy: image.PullAlways, Format: client.FormatFile, @@ -639,6 +715,9 @@ func testPackageBuildpack(t *testing.T, when spec.G, it spec.S) { Buildpack: dist.BuildpackURI{URI: createBuildpack(packageDescriptor)}, Dependencies: []dist.ImageOrURI{{BuildpackURI: dist.BuildpackURI{URI: createBuildpack(childDescriptor)}}}, }, + IndexOptions: pubbldpkg.IndexOptions{ + Targets: []dist.Target{{OS: "linux"}}, + }, Publish: false, PullPolicy: image.PullAlways, Format: client.FormatFile, @@ -661,6 +740,9 @@ func testPackageBuildpack(t *testing.T, when spec.G, it spec.S) { Buildpack: dist.BuildpackURI{URI: createBuildpack(packageDescriptor)}, Dependencies: []dist.ImageOrURI{{BuildpackURI: dist.BuildpackURI{URI: bpURL}}}, }, + IndexOptions: pubbldpkg.IndexOptions{ + Targets: []dist.Target{{OS: "linux"}}, + }, Publish: false, PullPolicy: image.PullAlways, Format: client.FormatFile, @@ -684,6 +766,9 @@ func testPackageBuildpack(t *testing.T, when spec.G, it spec.S) { Buildpack: dist.BuildpackURI{URI: createBuildpack(packageDescriptor)}, Dependencies: []dist.ImageOrURI{{BuildpackURI: dist.BuildpackURI{URI: bpURL}}}, }, + IndexOptions: pubbldpkg.IndexOptions{ + Targets: []dist.Target{{OS: "linux"}}, + }, Publish: false, PullPolicy: image.PullAlways, Format: client.FormatFile, @@ -708,8 +793,8 @@ func testPackageBuildpack(t *testing.T, when spec.G, it spec.S) { Optional: false, }}}) - nestedPackage = fakes.NewImage("nested/package-"+h.RandString(12), "", nil) - mockImageFactory.EXPECT().NewImage(nestedPackage.Name(), false, "linux").Return(nestedPackage, nil) + nestedPackage = fakes.NewImage("nested/package-"+h.RandString(12), "", NewIdentifier(imageIdentifier)) + mockImageFactory.EXPECT().NewImage(nestedPackage.Name(), false, v1.Platform{OS: "linux"}).Return(nestedPackage, nil) h.AssertNil(t, subject.PackageBuildpack(context.TODO(), client.PackageBuildpackOptions{ Name: nestedPackage.Name(), @@ -717,6 +802,9 @@ func testPackageBuildpack(t *testing.T, when spec.G, it spec.S) { Platform: dist.Platform{OS: "linux"}, Buildpack: dist.BuildpackURI{URI: createBuildpack(childDescriptor)}, }, + IndexOptions: pubbldpkg.IndexOptions{ + Targets: []dist.Target{{OS: "linux"}}, + }, Publish: true, PullPolicy: image.PullAlways, })) @@ -735,6 +823,9 @@ func testPackageBuildpack(t *testing.T, when spec.G, it spec.S) { Dependencies: []dist.ImageOrURI{{ImageRef: dist.ImageRef{ImageName: nestedPackage.Name()}}, {BuildpackURI: dist.BuildpackURI{URI: createBuildpack(secondChildDescriptor)}}}, }, + IndexOptions: pubbldpkg.IndexOptions{ + Targets: []dist.Target{{OS: "linux"}}, + }, Publish: false, PullPolicy: image.PullAlways, Format: client.FormatFile, @@ -759,6 +850,9 @@ func testPackageBuildpack(t *testing.T, when spec.G, it spec.S) { Platform: dist.Platform{OS: "linux"}, Buildpack: dist.BuildpackURI{URI: createBuildpack(childDescriptor)}, }, + IndexOptions: pubbldpkg.IndexOptions{ + Targets: []dist.Target{{OS: "linux"}}, + }, PullPolicy: image.PullAlways, Format: client.FormatFile, })) @@ -776,6 +870,9 @@ func testPackageBuildpack(t *testing.T, when spec.G, it spec.S) { Buildpack: dist.BuildpackURI{URI: createBuildpack(packageDescriptor)}, Dependencies: []dist.ImageOrURI{{BuildpackURI: dist.BuildpackURI{URI: dependencyPackagePath}}}, }, + IndexOptions: pubbldpkg.IndexOptions{ + Targets: []dist.Target{{OS: "linux"}}, + }, Publish: false, PullPolicy: image.PullAlways, Format: client.FormatFile, @@ -822,7 +919,7 @@ func testPackageBuildpack(t *testing.T, when spec.G, it spec.S) { registryFixture = h.CreateRegistryFixture(t, tmpDir, filepath.Join("testdata", "registry")) h.AssertNotNil(t, registryFixture) - packageImage := fakes.NewImage("example.com/some/package@sha256:74eb48882e835d8767f62940d453eb96ed2737de3a16573881dcea7dea769df7", "", nil) + packageImage := fakes.NewImage("example.com/some/package@sha256:74eb48882e835d8767f62940d453eb96ed2737de3a16573881dcea7dea769df7", "", NewIdentifier(imageIdentifier)) err = packageImage.AddLayerWithDiffID("testdata/empty-file", "sha256:xxx") h.AssertNil(t, err) err = packageImage.SetLabel("io.buildpacks.buildpackage.metadata", `{"id":"example/foo", "version":"1.1.0", "stacks":[{"id":"some.stack.id"}]}`) @@ -861,6 +958,9 @@ func testPackageBuildpack(t *testing.T, when spec.G, it spec.S) { Buildpack: dist.BuildpackURI{URI: createBuildpack(packageDescriptor)}, Dependencies: []dist.ImageOrURI{{BuildpackURI: dist.BuildpackURI{URI: "urn:cnb:registry:example/foo@1.1.0"}}}, }, + IndexOptions: pubbldpkg.IndexOptions{ + Targets: []dist.Target{{OS: "linux"}}, + }, Publish: false, PullPolicy: image.PullAlways, Format: client.FormatFile, @@ -872,10 +972,109 @@ func testPackageBuildpack(t *testing.T, when spec.G, it spec.S) { }) }) }) + when("#PackageMultiArchBuildpack", func() { + it("should return an error when BPConfig is not provided", func() { + h.AssertEq(t, subject.PackageMultiArchBuildpack(context.TODO(), client.PackageBuildpackOptions{}).Error(), "'IndexOptions' must not be nil") + }) + it("should return an error when BPConfig is nil", func() { + h.AssertEq(t, subject.PackageMultiArchBuildpack(context.TODO(), client.PackageBuildpackOptions{}).Error(), "'IndexOptions' must not be nil") + }) + it("should return an error when PKGConfig is not provided", func() { + h.AssertEq(t, subject.PackageMultiArchBuildpack(context.TODO(), client.PackageBuildpackOptions{ + IndexOptions: pubbldpkg.IndexOptions{ + BPConfigs: &[]pubbldpkg.MultiArchBuildpackConfig{ + { + BuildpackDescriptor: dist.BuildpackDescriptor{}, + }, + { + BuildpackDescriptor: dist.BuildpackDescriptor{}, + }, + }, + }, + }).Error(), "package configaration is undefined") + }) + when("ImageIndex", func() { + it("should create a new ImageIndex", func() { + tmpDir, err := os.MkdirTemp("", "test_dir") + h.AssertNil(t, err) + + f, err := os.CreateTemp(tmpDir, "buildpack.toml") + h.AssertNil(t, err) + + bpDescConfig := dist.BuildpackDescriptor{ + WithInfo: dist.ModuleInfo{ + ID: "some/bp", + Version: "latest", + }, + WithStacks: []dist.Stack{ + { + ID: "build-image", + Mixins: []string{}, + }, + { + ID: "run", + Mixins: []string{}, + }, + }, + WithTargets: []dist.Target{ + { + OS: "linux", + Arch: "arm", + ArchVariant: "v6", + Distributions: []dist.Distribution{ + { + Name: "distro1", + Versions: []string{"version1", "version2"}, + }, + }, + }, + { + OS: "windows", + Arch: "amd64", + Distributions: []dist.Distribution{ + { + Name: "distro1", + Versions: []string{"version1", "version2"}, + }, + }, + }, + }, + } + + toml.NewEncoder(f).Encode(bpDescConfig) + _, err = os.Stat(f.Name()) + h.AssertNil(t, err) + + expectCreateIndex(t, mockIndexFactory) + expectLoadIndex(t, mockIndexFactory) + mockDockerClient.EXPECT().Info(context.TODO()).Return(system.Info{OSType: "linux"}, nil).AnyTimes() + createMultiArchBuildpack(bpDescConfig) + fakeImage := fakes.NewImage("basic/package-"+h.RandString(12), "", NewIdentifier(imageIdentifier)) + mockImageFactory.EXPECT().NewImage(gomock.Any(), true, gomock.Any()).Return(fakeImage, nil).AnyTimes() + multiArchBPConfig := pubbldpkg.NewMultiArchBuildpack(bpDescConfig, tmpDir, true, true, nil) + cfgs, err := multiArchBPConfig.MultiArchConfigs() + h.AssertNil(t, err) + + err = subject.PackageMultiArchBuildpack(context.TODO(), client.PackageBuildpackOptions{ + IndexOptions: pubbldpkg.IndexOptions{ + BPConfigs: &cfgs, + PkgConfig: pubbldpkg.NewMultiArchPackage(pubbldpkg.DefaultConfig(), tmpDir), + Logger: logging.NewLogWithWriters(&out, &out), + }, + Name: bpDescConfig.WithInfo.ID, + Config: pubbldpkg.DefaultConfig(), + Version: bpDescConfig.WithInfo.Version, + RelativeBaseDir: f.Name(), + }) + h.AssertNil(t, err) + h.AssertNil(t, subject.ExistsManifest(context.TODO(), "some/bp:latest")) + }) + }) + }) when("unknown format is provided", func() { it("should error", func() { - mockDockerClient.EXPECT().Info(context.TODO()).Return(types.Info{OSType: "linux"}, nil).AnyTimes() + mockDockerClient.EXPECT().Info(context.TODO()).Return(system.Info{OSType: "linux"}, nil).AnyTimes() err := subject.PackageBuildpack(context.TODO(), client.PackageBuildpackOptions{ Name: "some-buildpack", @@ -888,6 +1087,9 @@ func testPackageBuildpack(t *testing.T, when spec.G, it spec.S) { WithStacks: []dist.Stack{{ID: "some.stack.id"}}, })}, }, + IndexOptions: pubbldpkg.IndexOptions{ + Targets: []dist.Target{{OS: "linux"}}, + }, Publish: false, PullPolicy: image.PullAlways, }) @@ -902,3 +1104,25 @@ func assertPackageBPFileHasBuildpacks(t *testing.T, path string, descriptors []d h.AssertNil(t, err) h.AssertBuildpacksHaveDescriptors(t, append([]buildpack.BuildModule{mainBP}, depBPs...), descriptors) } + +func expectLoadIndex(t *testing.T, mock *testmocks.MockIndexFactory) { + idx, err := fakes.NewIndex(ggcrTypes.OCIImageIndex, 1024, 1, 1, v1.Descriptor{}) + h.AssertNil(t, err) + + mock. + EXPECT(). + LoadIndex(gomock.Any(), gomock.Any()). + AnyTimes(). + Return(idx, nil) +} + +func expectCreateIndex(t *testing.T, mock *testmocks.MockIndexFactory) { + idx, err := fakes.NewIndex(ggcrTypes.OCIImageIndex, 1024, 1, 1, v1.Descriptor{}) + h.AssertNil(t, err) + + mock. + EXPECT(). + CreateIndex(gomock.Any(), gomock.Any()). + AnyTimes(). + Return(idx, nil) +} diff --git a/pkg/client/package_extension.go b/pkg/client/package_extension.go index 0e2c0e7317..54bbcd79e7 100644 --- a/pkg/client/package_extension.go +++ b/pkg/client/package_extension.go @@ -2,12 +2,19 @@ package client import ( "context" + "fmt" + "path/filepath" "github.com/pkg/errors" + "golang.org/x/sync/errgroup" + "github.com/buildpacks/imgutil" + + pubbldpkg "github.com/buildpacks/pack/buildpackage" "github.com/buildpacks/pack/internal/layer" "github.com/buildpacks/pack/internal/style" "github.com/buildpacks/pack/pkg/buildpack" + "github.com/buildpacks/pack/pkg/dist" ) // PackageExtension packages extension(s) into either an image or file. @@ -20,8 +27,7 @@ func (c *Client) PackageExtension(ctx context.Context, opts PackageBuildpackOpti return NewExperimentError("Windows extensionpackage support is currently experimental.") } - err := c.validateOSPlatform(ctx, opts.Config.Platform.OS, opts.Publish, opts.Format) - if err != nil { + if err := c.validateOSPlatform(ctx, opts.Config.Platform.OS, opts.Publish, opts.Format); err != nil { return err } @@ -30,7 +36,7 @@ func (c *Client) PackageExtension(ctx context.Context, opts PackageBuildpackOpti return errors.Wrap(err, "creating layer writer factory") } - packageBuilder := buildpack.NewBuilder(c.imageFactory) + packageBuilder := buildpack.NewBuilder(c.imageFactory, c.indexFactory) exURI := opts.Config.Extension.URI if exURI == "" { @@ -49,13 +55,115 @@ func (c *Client) PackageExtension(ctx context.Context, opts PackageBuildpackOpti packageBuilder.SetExtension(ex) + if opts.Format == FormatFile && opts.IndexOptions.ImageIndex != nil { + return packageBuilder.SaveAsMultiArchFile(opts.Name, opts.Version, opts.IndexOptions.Targets, opts.IndexOptions.ImageIndex, make(map[string]string)) + } switch opts.Format { case FormatFile: - return packageBuilder.SaveAsFile(opts.Name, opts.Config.Platform.OS, map[string]string{}) + return packageBuilder.SaveAsFile(opts.Name, opts.Version, opts.IndexOptions.Targets[0], opts.IndexOptions.ImageIndex, map[string]string{}) case FormatImage: - _, err = packageBuilder.SaveAsImage(opts.Name, opts.Publish, opts.Config.Platform.OS, map[string]string{}) + _, err = packageBuilder.SaveAsImage(opts.Name, opts.Version, opts.Publish, opts.IndexOptions.Targets[0], opts.IndexOptions.ImageIndex, map[string]string{}) return errors.Wrapf(err, "saving image") default: return errors.Errorf("unknown format: %s", style.Symbol(opts.Format)) } } + +func (c *Client) PackageMultiArchExtension(ctx context.Context, opts PackageBuildpackOptions) error { + if !c.experimental { + return errors.Errorf("packaging %s is currently %s", style.Symbol("multi arch extensions"), style.Symbol(("experimental"))) + } + + if opts.IndexOptions.ExtConfigs == nil || len(*opts.IndexOptions.ExtConfigs) < 2 { + return errors.Errorf("%s must not be nil", style.Symbol("IndexOptions")) + } + + if opts.IndexOptions.PkgConfig == nil { + return errors.Errorf("package configaration is undefined") + } + + extCfg, err := pubbldpkg.NewConfigReader().ReadExtensionDescriptor(opts.RelativeBaseDir) + if err != nil { + return fmt.Errorf("cannot read %s file: %s", style.Symbol("extension.toml"), style.Symbol(opts.RelativeBaseDir)) + } + + var repoName string + if info := extCfg.WithInfo; info.Version == "" { + repoName = info.ID + } else { + repoName = info.ID + ":" + info.Version + } + + if err := createImageIndex(c, repoName); err != nil { + return err + } + + pkgConfig, extConfigs := *opts.IndexOptions.PkgConfig, *opts.IndexOptions.ExtConfigs + + var errs errgroup.Group + idx, err := loadImageIndex(c, repoName) + if err != nil { + return err + } + + IndexManifestFn := c.GetIndexManifestFn() + packageMultiArchExtensionFn := func(extConfig pubbldpkg.MultiArchExtensionConfig) error { + if err := extConfig.CopyExtensionToml(IndexManifestFn); err != nil { + return err + } + defer extConfig.CleanExtensionToml() + + targets, target, distro, version := extConfig.Targets(), dist.Target{}, dist.Distribution{}, "" + if len(targets) != 0 { + target = targets[0] + } + + if len(target.Distributions) != 0 { + distro = target.Distributions[0] + } + + if len(distro.Versions) != 0 { + version = distro.Versions[0] + } + if err := pkgConfig.CopyPackageToml(filepath.Dir(extConfig.Path()), target, distro.Name, version, IndexManifestFn); err != nil { + return err + } + defer pkgConfig.CleanPackageToml(filepath.Dir(extConfig.Path()), target, distro.Name, version) + + return c.PackageExtension(ctx, PackageBuildpackOptions{ + RelativeBaseDir: "", + Name: opts.Name, + Format: opts.Format, + Config: pkgConfig.Config, + Publish: opts.Publish, + PullPolicy: opts.PullPolicy, + Registry: opts.Registry, + Version: opts.Version, + IndexOptions: pubbldpkg.IndexOptions{ + ImageIndex: idx, + Logger: opts.IndexOptions.Logger, + Targets: extConfig.Targets(), + }, + }) + } + + for _, extConfig := range extConfigs { + c := extConfig + errs.Go(func() error { + return packageMultiArchExtensionFn(c) + }) + } + if err := errs.Wait(); err != nil { + return err + } + + if err := idx.Save(); err != nil { + return err + } + + if !opts.Publish { + return nil + } + + return idx.Push(imgutil.WithInsecure(true) /* imgutil.WithTags("latest") */) +} diff --git a/pkg/client/package_extension_test.go b/pkg/client/package_extension_test.go index 86caa702c2..fa6154448c 100644 --- a/pkg/client/package_extension_test.go +++ b/pkg/client/package_extension_test.go @@ -10,8 +10,9 @@ import ( "github.com/buildpacks/imgutil/fakes" "github.com/buildpacks/lifecycle/api" - "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/system" "github.com/golang/mock/gomock" + v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/heroku/color" "github.com/sclevine/spec" "github.com/sclevine/spec/report" @@ -42,6 +43,7 @@ func testPackageExtension(t *testing.T, when spec.G, it spec.S) { mockImageFetcher *testmocks.MockImageFetcher mockDockerClient *testmocks.MockCommonAPIClient out bytes.Buffer + imageIdentifier = "74eb48882e835d8767f62940d453eb96ed2737de3a16573881dcea7dea769df7" ) it.Before(func() { @@ -83,6 +85,9 @@ func testPackageExtension(t *testing.T, when spec.G, it spec.S) { Platform: dist.Platform{OS: "linux"}, Extension: dist.BuildpackURI{URI: ""}, }, + IndexOptions: pubbldpkg.IndexOptions{ + Targets: []dist.Target{{OS: "linux"}}, + }, Publish: true, }) h.AssertError(t, err, "extension URI must be provided") @@ -100,6 +105,9 @@ func testPackageExtension(t *testing.T, when spec.G, it spec.S) { Platform: dist.Platform{OS: "linux"}, Extension: dist.BuildpackURI{URI: exURL}, }, + IndexOptions: pubbldpkg.IndexOptions{ + Targets: []dist.Target{{OS: "linux"}}, + }, Publish: true, }) h.AssertError(t, err, "downloading buildpack") @@ -118,6 +126,9 @@ func testPackageExtension(t *testing.T, when spec.G, it spec.S) { Platform: dist.Platform{OS: "linux"}, Extension: dist.BuildpackURI{URI: exURL}, }, + IndexOptions: pubbldpkg.IndexOptions{ + Targets: []dist.Target{{OS: "linux"}}, + }, Publish: true, }) h.AssertError(t, err, "creating extension") @@ -130,7 +141,7 @@ func testPackageExtension(t *testing.T, when spec.G, it spec.S) { it("creates package image based on daemon OS", func() { for _, daemonOS := range []string{"linux", "windows"} { localMockDockerClient := testmocks.NewMockCommonAPIClient(mockController) - localMockDockerClient.EXPECT().Info(context.TODO()).Return(types.Info{OSType: daemonOS}, nil).AnyTimes() + localMockDockerClient.EXPECT().Info(context.TODO()).Return(system.Info{OSType: daemonOS}, nil).AnyTimes() packClientWithExperimental, err := client.NewClient( client.WithDockerClient(localMockDockerClient), @@ -140,8 +151,8 @@ func testPackageExtension(t *testing.T, when spec.G, it spec.S) { ) h.AssertNil(t, err) - fakeImage := fakes.NewImage("basic/package-"+h.RandString(12), "", nil) - mockImageFactory.EXPECT().NewImage(fakeImage.Name(), true, daemonOS).Return(fakeImage, nil) + fakeImage := fakes.NewImage("basic/package-"+h.RandString(12), "", NewIdentifier(imageIdentifier)) + mockImageFactory.EXPECT().NewImage(fakeImage.Name(), true, v1.Platform{OS: daemonOS}).Return(fakeImage, nil) fakeBlob := blob.NewBlob(filepath.Join("testdata", "empty-file")) exURL := fmt.Sprintf("https://example.com/ex.%s.tgz", h.RandString(12)) @@ -157,6 +168,9 @@ func testPackageExtension(t *testing.T, when spec.G, it spec.S) { WithInfo: dist.ModuleInfo{ID: "ex.basic", Version: "2.3.4"}, })}, }, + IndexOptions: pubbldpkg.IndexOptions{ + Targets: []dist.Target{{OS: daemonOS}}, + }, PullPolicy: image.PullNever, })) } @@ -177,13 +191,16 @@ func testPackageExtension(t *testing.T, when spec.G, it spec.S) { OS: "windows", }, }, + IndexOptions: pubbldpkg.IndexOptions{ + Targets: []dist.Target{{OS: "linux"}}, + }, }) h.AssertError(t, err, "Windows extensionpackage support is currently experimental.") }) it("fails for mismatched platform and daemon os", func() { windowsMockDockerClient := testmocks.NewMockCommonAPIClient(mockController) - windowsMockDockerClient.EXPECT().Info(context.TODO()).Return(types.Info{OSType: "windows"}, nil).AnyTimes() + windowsMockDockerClient.EXPECT().Info(context.TODO()).Return(system.Info{OSType: "windows"}, nil).AnyTimes() packClientWithoutExperimental, err := client.NewClient( client.WithDockerClient(windowsMockDockerClient), @@ -197,6 +214,9 @@ func testPackageExtension(t *testing.T, when spec.G, it spec.S) { OS: "linux", }, }, + IndexOptions: pubbldpkg.IndexOptions{ + Targets: []dist.Target{{OS: "linux"}}, + }, }) h.AssertError(t, err, "invalid 'platform.os' specified: DOCKER_OS is 'windows'") @@ -213,7 +233,7 @@ func testPackageExtension(t *testing.T, when spec.G, it spec.S) { for _, imageOS := range []string{"linux", "windows"} { localMockDockerClient := testmocks.NewMockCommonAPIClient(mockController) - localMockDockerClient.EXPECT().Info(context.TODO()).Return(types.Info{OSType: imageOS}, nil).AnyTimes() + localMockDockerClient.EXPECT().Info(context.TODO()).Return(system.Info{OSType: imageOS}, nil).AnyTimes() packClientWithExperimental, err := client.NewClient( client.WithDockerClient(localMockDockerClient), @@ -238,6 +258,9 @@ func testPackageExtension(t *testing.T, when spec.G, it spec.S) { WithInfo: dist.ModuleInfo{ID: "ex.basic", Version: "2.3.4"}, })}, }, + IndexOptions: pubbldpkg.IndexOptions{ + Targets: []dist.Target{{OS: imageOS}}, + }, PullPolicy: image.PullNever, })) } @@ -247,7 +270,7 @@ func testPackageExtension(t *testing.T, when spec.G, it spec.S) { when("unknown format is provided", func() { it("should error", func() { - mockDockerClient.EXPECT().Info(context.TODO()).Return(types.Info{OSType: "linux"}, nil).AnyTimes() + mockDockerClient.EXPECT().Info(context.TODO()).Return(system.Info{OSType: "linux"}, nil).AnyTimes() err := subject.PackageExtension(context.TODO(), client.PackageBuildpackOptions{ Name: "some-extension", @@ -259,6 +282,9 @@ func testPackageExtension(t *testing.T, when spec.G, it spec.S) { WithInfo: dist.ModuleInfo{ID: "ex.1", Version: "1.2.3"}, })}, }, + IndexOptions: pubbldpkg.IndexOptions{ + Targets: []dist.Target{{OS: "linux"}}, + }, Publish: false, PullPolicy: image.PullAlways, }) diff --git a/pkg/client/push_manifest.go b/pkg/client/push_manifest.go new file mode 100644 index 0000000000..a6bcff8ab7 --- /dev/null +++ b/pkg/client/push_manifest.go @@ -0,0 +1,45 @@ +package client + +import ( + "context" + "fmt" + + "github.com/buildpacks/imgutil" + "github.com/google/go-containerregistry/pkg/v1/types" +) + +type PushManifestOptions struct { + Format string + Insecure, Purge bool +} + +// PushManifest implements commands.PackClient. +func (c *Client) PushManifest(ctx context.Context, index string, opts PushManifestOptions) (err error) { + idx, err := c.indexFactory.LoadIndex(index) + if err != nil { + return + } + + if err = idx.Push(parseFalgsForImgUtil(opts)...); err != nil { + return err + } + + if !opts.Purge { + fmt.Printf("successfully pushed index: '%s'\n", index) + return nil + } + + return idx.Delete() +} + +func parseFalgsForImgUtil(opts PushManifestOptions) (idxOptions []imgutil.IndexPushOption) { + idxOptions = append(idxOptions, imgutil.WithInsecure(opts.Insecure)) + switch opts.Format { + case "oci": + return append(idxOptions, imgutil.WithFormat(types.OCIImageIndex)) + case "v2s2": + return append(idxOptions, imgutil.WithFormat(types.DockerManifestList)) + default: + return idxOptions + } +} diff --git a/pkg/client/push_manifest_test.go b/pkg/client/push_manifest_test.go new file mode 100644 index 0000000000..09e5414daf --- /dev/null +++ b/pkg/client/push_manifest_test.go @@ -0,0 +1,77 @@ +package client + +import ( + "bytes" + "context" + "os" + "testing" + + "github.com/golang/mock/gomock" + "github.com/google/go-containerregistry/pkg/authn" + "github.com/heroku/color" + "github.com/sclevine/spec" + "github.com/sclevine/spec/report" + + "github.com/buildpacks/imgutil" + + "github.com/buildpacks/pack/pkg/logging" + "github.com/buildpacks/pack/pkg/testmocks" + h "github.com/buildpacks/pack/testhelpers" +) + +func TestPushManifest(t *testing.T) { + color.Disable(true) + defer color.Disable(false) + spec.Run(t, "build", testPushManifest, spec.Report(report.Terminal{})) +} + +func testPushManifest(t *testing.T, when spec.G, it spec.S) { + var ( + mockController *gomock.Controller + mockIndexFactory *testmocks.MockIndexFactory + out bytes.Buffer + logger logging.Logger + subject *Client + err error + tmpDir string + ) + when("#Push", func() { + it.Before(func() { + logger = logging.NewLogWithWriters(&out, &out, logging.WithVerbose()) + mockController = gomock.NewController(t) + mockIndexFactory = testmocks.NewMockIndexFactory(mockController) + + subject, err = NewClient( + WithLogger(logger), + WithIndexFactory(mockIndexFactory), + WithExperimental(true), + WithKeychain(authn.DefaultKeychain), + ) + h.AssertSameInstance(t, mockIndexFactory, subject.indexFactory) + h.AssertNil(t, err) + }) + it.After(func() { + mockController.Finish() + h.AssertNil(t, os.RemoveAll(tmpDir)) + }) + it("should not have local image index", func() { + prepareLoadIndexWithError(*mockIndexFactory) + + err := subject.PushManifest(context.TODO(), "some-index", PushManifestOptions{}) + h.AssertEq(t, err.Error(), imgutil.ErrNoImageOrIndexFoundWithGivenDigest("").Error()) + }) + it("should push index to registry", func() { + prepareLoadIndex(t, *mockIndexFactory) + + err := subject.PushManifest(context.TODO(), "some-index", PushManifestOptions{}) + h.AssertNil(t, err) + }) + }) +} + +func prepareLoadIndexWithError(mockIndexFactory testmocks.MockIndexFactory) { + mockIndexFactory. + EXPECT(). + LoadIndex(gomock.Any(), gomock.Any()). + Return(nil, imgutil.ErrNoImageOrIndexFoundWithGivenDigest("")) +} diff --git a/pkg/client/remove_manifest.go b/pkg/client/remove_manifest.go new file mode 100644 index 0000000000..5df0de8908 --- /dev/null +++ b/pkg/client/remove_manifest.go @@ -0,0 +1,26 @@ +package client + +import ( + "context" + "fmt" +) + +// DeleteManifest implements commands.PackClient. +func (c *Client) DeleteManifest(ctx context.Context, names []string) (errs []error) { + for _, name := range names { + imgIndex, err := c.indexFactory.LoadIndex(name) + if err != nil { + errs = append(errs, err) + } + + if err := imgIndex.Delete(); err != nil { + errs = append(errs, err) + } + } + + if len(errs) == 0 { + fmt.Printf("successfully deleted indexes \n") + } + + return errs +} diff --git a/pkg/client/remove_manifest_test.go b/pkg/client/remove_manifest_test.go new file mode 100644 index 0000000000..b919032fef --- /dev/null +++ b/pkg/client/remove_manifest_test.go @@ -0,0 +1,72 @@ +package client + +import ( + "bytes" + "context" + "os" + "testing" + + "github.com/golang/mock/gomock" + "github.com/google/go-containerregistry/pkg/authn" + "github.com/heroku/color" + "github.com/sclevine/spec" + "github.com/sclevine/spec/report" + + "github.com/buildpacks/pack/pkg/logging" + "github.com/buildpacks/pack/pkg/testmocks" + h "github.com/buildpacks/pack/testhelpers" +) + +func TestDeleteManifest(t *testing.T) { + color.Disable(true) + defer color.Disable(false) + spec.Run(t, "build", testDeleteManifest, spec.Report(report.Terminal{})) +} + +func testDeleteManifest(t *testing.T, when spec.G, it spec.S) { + var ( + mockController *gomock.Controller + mockIndexFactory *testmocks.MockIndexFactory + out bytes.Buffer + logger logging.Logger + subject *Client + err error + tmpDir string + ) + + when("#Add", func() { + it.Before(func() { + logger = logging.NewLogWithWriters(&out, &out, logging.WithVerbose()) + mockController = gomock.NewController(t) + mockIndexFactory = testmocks.NewMockIndexFactory(mockController) + + subject, err = NewClient( + WithLogger(logger), + WithIndexFactory(mockIndexFactory), + WithExperimental(true), + WithKeychain(authn.DefaultKeychain), + ) + h.AssertSameInstance(t, mockIndexFactory, subject.indexFactory) + h.AssertNil(t, err) + }) + it.After(func() { + mockController.Finish() + h.AssertNil(t, os.RemoveAll(tmpDir)) + }) + it("should delete local index", func() { + prepareLoadIndex(t, *mockIndexFactory) + + errs := subject.DeleteManifest(context.TODO(), []string{"some-index"}) + h.AssertEq(t, len(errs), 0) + }) + it("should return an error when index is already deleted", func() { + prepareLoadIndex(t, *mockIndexFactory) + + errs := subject.DeleteManifest(context.TODO(), []string{"some-index"}) + h.AssertEq(t, len(errs), 0) + + errs = subject.DeleteManifest(context.TODO(), []string{"some-index"}) + h.AssertNotEq(t, len(errs), 0) + }) + }) +} diff --git a/pkg/client/rm_manifest.go b/pkg/client/rm_manifest.go new file mode 100644 index 0000000000..9aa95e4e5c --- /dev/null +++ b/pkg/client/rm_manifest.go @@ -0,0 +1,36 @@ +package client + +import ( + "context" + "fmt" + + gccrName "github.com/google/go-containerregistry/pkg/name" +) + +// RemoveManifest implements commands.PackClient. +func (c *Client) RemoveManifest(ctx context.Context, name string, images []string) (errs []error) { + imgIndex, err := c.indexFactory.LoadIndex(name) + if err != nil { + return append(errs, err) + } + + for _, image := range images { + ref, err := gccrName.ParseReference(image, gccrName.WeakValidation, gccrName.Insecure) + if err != nil { + errs = append(errs, fmt.Errorf(`invalid instance "%s": %v`, image, err)) + } + if err = imgIndex.Remove(ref); err != nil { + errs = append(errs, err) + } + + if err = imgIndex.Save(); err != nil { + errs = append(errs, err) + } + } + + if len(errs) == 0 { + fmt.Printf("Successfully removed images from index: '%s' \n", name) + } + + return errs +} diff --git a/pkg/client/rm_manifest_test.go b/pkg/client/rm_manifest_test.go new file mode 100644 index 0000000000..9c092daf94 --- /dev/null +++ b/pkg/client/rm_manifest_test.go @@ -0,0 +1,88 @@ +package client + +import ( + "bytes" + "context" + "os" + "testing" + + "github.com/golang/mock/gomock" + "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/name" + "github.com/heroku/color" + "github.com/sclevine/spec" + "github.com/sclevine/spec/report" + + "github.com/buildpacks/imgutil/fakes" + + "github.com/buildpacks/pack/pkg/logging" + "github.com/buildpacks/pack/pkg/testmocks" + h "github.com/buildpacks/pack/testhelpers" +) + +func TestRemoveManifest(t *testing.T) { + color.Disable(true) + defer color.Disable(false) + spec.Run(t, "build", testRemoveManifest, spec.Report(report.Terminal{})) +} + +func testRemoveManifest(t *testing.T, when spec.G, it spec.S) { + var ( + mockController *gomock.Controller + mockIndexFactory *testmocks.MockIndexFactory + out bytes.Buffer + logger logging.Logger + subject *Client + err error + tmpDir string + ) + + when("#Add", func() { + it.Before(func() { + logger = logging.NewLogWithWriters(&out, &out, logging.WithVerbose()) + mockController = gomock.NewController(t) + mockIndexFactory = testmocks.NewMockIndexFactory(mockController) + + subject, err = NewClient( + WithLogger(logger), + WithIndexFactory(mockIndexFactory), + WithExperimental(true), + WithKeychain(authn.DefaultKeychain), + ) + h.AssertSameInstance(t, mockIndexFactory, subject.indexFactory) + h.AssertNil(t, err) + }) + it.After(func() { + mockController.Finish() + h.AssertNil(t, os.RemoveAll(tmpDir)) + }) + it("should remove local index", func() { + idx := prepareLoadIndex(t, *mockIndexFactory) + imgIdx, ok := idx.(*fakes.Index) + h.AssertEq(t, ok, true) + + mfest, err := imgIdx.IndexManifest() + h.AssertNil(t, err) + + digest, err := name.NewDigest("some/repo@" + mfest.Manifests[0].Digest.String()) + h.AssertNil(t, err) + + errs := subject.RemoveManifest(context.TODO(), "some-index", []string{digest.Name()}) + h.AssertEq(t, len(errs), 0) + }) + it("should remove image", func() { + idx := prepareLoadIndex(t, *mockIndexFactory) + imgIdx, ok := idx.(*fakes.Index) + h.AssertEq(t, ok, true) + + mfest, err := imgIdx.IndexManifest() + h.AssertNil(t, err) + + digest, err := name.NewDigest("some/repo@" + mfest.Manifests[0].Digest.String()) + h.AssertNil(t, err) + + errs := subject.RemoveManifest(context.TODO(), "some-index", []string{digest.Name()}) + h.AssertEq(t, len(errs), 0) + }) + }) +} diff --git a/pkg/dist/buildmodule.go b/pkg/dist/buildmodule.go index 5126d988e0..5231605b74 100644 --- a/pkg/dist/buildmodule.go +++ b/pkg/dist/buildmodule.go @@ -1,6 +1,7 @@ package dist import ( + v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/pkg/errors" "github.com/buildpacks/pack/internal/style" @@ -57,9 +58,70 @@ type Target struct { Arch string `json:"arch" toml:"arch"` ArchVariant string `json:"variant,omitempty" toml:"variant,omitempty"` Distributions []Distribution `json:"distributions,omitempty" toml:"distributions,omitempty"` + Specs TargetSpecs `json:"specs,omitempty" toml:"specs,omitempty"` } type Distribution struct { Name string `json:"name,omitempty" toml:"name,omitempty"` Versions []string `json:"versions,omitempty" toml:"versions,omitempty"` } + +type TargetSpecs struct { + Features []string `json:"features,omitempty" toml:"features,omitempty"` + OSFeatures []string `json:"os.features,omitempty" toml:"os.features,omitempty"` + URLs []string `json:"urls,omitempty" toml:"urls,omitempty"` + Annotations map[string]string `json:"annotations,omitempty" toml:"annotations,omitempty"` + Flatten bool `json:"flatten,omitempty" toml:"flatten,omitempty"` + FlattenExclude []string `json:"flatten.exclude,omitempty" toml:"flatten.exclude,omitempty"` + Labels map[string]string `json:"labels,omitempty" toml:"labels,omitempty"` + OSVersion string `json:"os.version,omitempty" toml:"os.version,omitempty"` + Path string `json:"path,omitempty" toml:"path,omitempty"` +} + +func (t Target) Range(op func(target Target, distroName, distroVersion string) error) error { + emptyString := "" + if len(t.Distributions) == 0 { + return op(t, emptyString, emptyString) + } + for _, distro := range t.Distributions { + if len(distro.Versions) == 0 { + if err := op(t, distro.Name, emptyString); err != nil { + return err + } + continue + } + for _, version := range distro.Versions { + if err := op(t, distro.Name, version); err != nil { + return err + } + } + } + return nil +} + +func (t *Target) Platform() *v1.Platform { + return &v1.Platform{ + OS: t.OS, + Architecture: t.Arch, + Variant: t.ArchVariant, + OSVersion: t.Specs.OSVersion, + Features: t.Specs.Features, + OSFeatures: t.Specs.OSFeatures, + } +} + +func (t *Target) Annotations() (map[string]string, error) { + if len(t.Distributions) == 0 { + return nil, errors.New("unable to get annotations: distroless target provided.") + } + + distro := t.Distributions[0] + return map[string]string{ + "io.buildpacks.base.distro.name": distro.Name, + "io.buildpacks.base.distro.version": distro.Versions[0], + }, nil +} + +func (t *Target) URLs() []string { + return t.Specs.URLs +} diff --git a/pkg/dist/buildpack_descriptor_test.go b/pkg/dist/buildpack_descriptor_test.go index 1f755edb5d..82b694e64d 100644 --- a/pkg/dist/buildpack_descriptor_test.go +++ b/pkg/dist/buildpack_descriptor_test.go @@ -207,7 +207,7 @@ func testBuildpackDescriptor(t *testing.T, when spec.G, it spec.S) { h.AssertNil(t, bp.EnsureStackSupport("some.stack.id", []string{}, true)) h.AssertError(t, bp.EnsureTargetSupport("some-other-os", "fake-arch", "fake-distro", "0.0"), - `unable to satisfy target os/arch constraints; build image: {"os":"some-other-os","arch":"fake-arch","distribution":{"name":"fake-distro","version":"0.0"}}, buildpack 'some.buildpack.id@some.buildpack.version': [{"os":"fake-os","arch":"fake-arch"}]`) + `unable to satisfy target os/arch constraints; build image: {"os":"some-other-os","arch":"fake-arch","distribution":{"name":"fake-distro","version":"0.0"}}, buildpack 'some.buildpack.id@some.buildpack.version': [{"os":"fake-os","arch":"fake-arch","specs":{}}]`) }) it("succeeds with distribution", func() { @@ -260,7 +260,7 @@ func testBuildpackDescriptor(t *testing.T, when spec.G, it spec.S) { h.AssertNil(t, bp.EnsureStackSupport("some.stack.id", []string{}, true)) h.AssertError(t, bp.EnsureTargetSupport("some-other-os", "fake-arch", "fake-distro", "0.0"), - `unable to satisfy target os/arch constraints; build image: {"os":"some-other-os","arch":"fake-arch","distribution":{"name":"fake-distro","version":"0.0"}}, buildpack 'some.buildpack.id@some.buildpack.version': [{"os":"fake-os","arch":"fake-arch","distributions":[{"name":"fake-distro","versions":["0.1"]},{"name":"another-distro","versions":["0.22"]}]}]`) + `unable to satisfy target os/arch constraints; build image: {"os":"some-other-os","arch":"fake-arch","distribution":{"name":"fake-distro","version":"0.0"}}, buildpack 'some.buildpack.id@some.buildpack.version': [{"os":"fake-os","arch":"fake-arch","distributions":[{"name":"fake-distro","versions":["0.1"]},{"name":"another-distro","versions":["0.22"]}],"specs":{}}]`) }) it("succeeds with missing arch", func() { diff --git a/pkg/dist/extension_descriptor.go b/pkg/dist/extension_descriptor.go index b968ebf447..38b3e99a6a 100644 --- a/pkg/dist/extension_descriptor.go +++ b/pkg/dist/extension_descriptor.go @@ -7,8 +7,9 @@ import ( ) type ExtensionDescriptor struct { - WithAPI *api.Version `toml:"api"` - WithInfo ModuleInfo `toml:"extension"` + WithAPI *api.Version `toml:"api"` + WithInfo ModuleInfo `toml:"extension"` + WithTargets []Target `toml:"targets,omitempty"` } func (e *ExtensionDescriptor) EnsureStackSupport(_ string, _ []string, _ bool) error { @@ -44,5 +45,5 @@ func (e *ExtensionDescriptor) Stacks() []Stack { } func (e *ExtensionDescriptor) Targets() []Target { - return nil + return e.WithTargets } diff --git a/pkg/errors/errors.go b/pkg/errors/errors.go new file mode 100644 index 0000000000..669d3e8ab7 --- /dev/null +++ b/pkg/errors/errors.go @@ -0,0 +1,9 @@ +package errors + +import "errors" + +var ( + ErrDuplicateName = errors.New("Image/ImageIndex with the given name exists") + ErrIndexUnknown = errors.New("cannot find Image Index with the given name") + ErrNotAddManifestList = errors.New("error while adding ImageIndex to the list") +) diff --git a/pkg/image/fetcher.go b/pkg/image/fetcher.go index 60548fcc1d..d5506f1013 100644 --- a/pkg/image/fetcher.go +++ b/pkg/image/fetcher.go @@ -26,6 +26,13 @@ import ( "github.com/buildpacks/pack/pkg/logging" ) +const ( + LOCAL = "local" + LOCALLAYOUT = "locallayout" + REMOTE = "remote" + LAYOUT = "layout" +) + // FetcherOption is a type of function that mutate settings on the client. // Values in these functions are set through currying. type FetcherOption func(c *Fetcher) diff --git a/pkg/image/fetcher_test.go b/pkg/image/fetcher_test.go index 3b119e15f3..8a90b4dcdd 100644 --- a/pkg/image/fetcher_test.go +++ b/pkg/image/fetcher_test.go @@ -9,12 +9,11 @@ import ( "runtime" "testing" - "github.com/buildpacks/imgutil" - "github.com/buildpacks/imgutil/local" "github.com/buildpacks/imgutil/remote" "github.com/docker/docker/client" "github.com/google/go-containerregistry/pkg/authn" + v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/heroku/color" "github.com/sclevine/spec" "github.com/sclevine/spec/report" @@ -224,7 +223,7 @@ func testFetcher(t *testing.T, when spec.G, it spec.S) { when("remote platform does not match", func() { it.Before(func() { - img, err := remote.NewImage(repoName, authn.DefaultKeychain, remote.WithDefaultPlatform(imgutil.Platform{OS: osType, Architecture: ""})) + img, err := remote.NewImage(repoName, authn.DefaultKeychain, remote.WithDefaultPlatform(v1.Platform{OS: osType, Architecture: ""})) h.AssertNil(t, err) h.AssertNil(t, img.Save()) }) diff --git a/pkg/testmocks/mock_access_checker.go b/pkg/testmocks/mock_access_checker.go new file mode 100644 index 0000000000..558b85a580 --- /dev/null +++ b/pkg/testmocks/mock_access_checker.go @@ -0,0 +1,48 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/buildpacks/pack/pkg/client (interfaces: AccessChecker) + +// Package testmocks is a generated GoMock package. +package testmocks + +import ( + reflect "reflect" + + gomock "github.com/golang/mock/gomock" +) + +// MockAccessChecker is a mock of AccessChecker interface. +type MockAccessChecker struct { + ctrl *gomock.Controller + recorder *MockAccessCheckerMockRecorder +} + +// MockAccessCheckerMockRecorder is the mock recorder for MockAccessChecker. +type MockAccessCheckerMockRecorder struct { + mock *MockAccessChecker +} + +// NewMockAccessChecker creates a new mock instance. +func NewMockAccessChecker(ctrl *gomock.Controller) *MockAccessChecker { + mock := &MockAccessChecker{ctrl: ctrl} + mock.recorder = &MockAccessCheckerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockAccessChecker) EXPECT() *MockAccessCheckerMockRecorder { + return m.recorder +} + +// Check mocks base method. +func (m *MockAccessChecker) Check(arg0 string) bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Check", arg0) + ret0, _ := ret[0].(bool) + return ret0 +} + +// Check indicates an expected call of Check. +func (mr *MockAccessCheckerMockRecorder) Check(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Check", reflect.TypeOf((*MockAccessChecker)(nil).Check), arg0) +} diff --git a/pkg/testmocks/mock_docker_client.go b/pkg/testmocks/mock_docker_client.go index 18b51c18f1..c094670cd5 100644 --- a/pkg/testmocks/mock_docker_client.go +++ b/pkg/testmocks/mock_docker_client.go @@ -19,6 +19,7 @@ import ( network "github.com/docker/docker/api/types/network" registry "github.com/docker/docker/api/types/registry" swarm "github.com/docker/docker/api/types/swarm" + system "github.com/docker/docker/api/types/system" volume "github.com/docker/docker/api/types/volume" gomock "github.com/golang/mock/gomock" v1 "github.com/opencontainers/image-spec/specs-go/v1" @@ -179,7 +180,7 @@ func (mr *MockCommonAPIClientMockRecorder) ConfigUpdate(arg0, arg1, arg2, arg3 i } // ContainerAttach mocks base method. -func (m *MockCommonAPIClient) ContainerAttach(arg0 context.Context, arg1 string, arg2 types.ContainerAttachOptions) (types.HijackedResponse, error) { +func (m *MockCommonAPIClient) ContainerAttach(arg0 context.Context, arg1 string, arg2 container.AttachOptions) (types.HijackedResponse, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ContainerAttach", arg0, arg1, arg2) ret0, _ := ret[0].(types.HijackedResponse) @@ -194,7 +195,7 @@ func (mr *MockCommonAPIClientMockRecorder) ContainerAttach(arg0, arg1, arg2 inte } // ContainerCommit mocks base method. -func (m *MockCommonAPIClient) ContainerCommit(arg0 context.Context, arg1 string, arg2 types.ContainerCommitOptions) (types.IDResponse, error) { +func (m *MockCommonAPIClient) ContainerCommit(arg0 context.Context, arg1 string, arg2 container.CommitOptions) (types.IDResponse, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ContainerCommit", arg0, arg1, arg2) ret0, _ := ret[0].(types.IDResponse) @@ -224,10 +225,10 @@ func (mr *MockCommonAPIClientMockRecorder) ContainerCreate(arg0, arg1, arg2, arg } // ContainerDiff mocks base method. -func (m *MockCommonAPIClient) ContainerDiff(arg0 context.Context, arg1 string) ([]container.ContainerChangeResponseItem, error) { +func (m *MockCommonAPIClient) ContainerDiff(arg0 context.Context, arg1 string) ([]container.FilesystemChange, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ContainerDiff", arg0, arg1) - ret0, _ := ret[0].([]container.ContainerChangeResponseItem) + ret0, _ := ret[0].([]container.FilesystemChange) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -284,7 +285,7 @@ func (mr *MockCommonAPIClientMockRecorder) ContainerExecInspect(arg0, arg1 inter } // ContainerExecResize mocks base method. -func (m *MockCommonAPIClient) ContainerExecResize(arg0 context.Context, arg1 string, arg2 types.ResizeOptions) error { +func (m *MockCommonAPIClient) ContainerExecResize(arg0 context.Context, arg1 string, arg2 container.ResizeOptions) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ContainerExecResize", arg0, arg1, arg2) ret0, _ := ret[0].(error) @@ -372,7 +373,7 @@ func (mr *MockCommonAPIClientMockRecorder) ContainerKill(arg0, arg1, arg2 interf } // ContainerList mocks base method. -func (m *MockCommonAPIClient) ContainerList(arg0 context.Context, arg1 types.ContainerListOptions) ([]types.Container, error) { +func (m *MockCommonAPIClient) ContainerList(arg0 context.Context, arg1 container.ListOptions) ([]types.Container, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ContainerList", arg0, arg1) ret0, _ := ret[0].([]types.Container) @@ -387,7 +388,7 @@ func (mr *MockCommonAPIClientMockRecorder) ContainerList(arg0, arg1 interface{}) } // ContainerLogs mocks base method. -func (m *MockCommonAPIClient) ContainerLogs(arg0 context.Context, arg1 string, arg2 types.ContainerLogsOptions) (io.ReadCloser, error) { +func (m *MockCommonAPIClient) ContainerLogs(arg0 context.Context, arg1 string, arg2 container.LogsOptions) (io.ReadCloser, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ContainerLogs", arg0, arg1, arg2) ret0, _ := ret[0].(io.ReadCloser) @@ -416,7 +417,7 @@ func (mr *MockCommonAPIClientMockRecorder) ContainerPause(arg0, arg1 interface{} } // ContainerRemove mocks base method. -func (m *MockCommonAPIClient) ContainerRemove(arg0 context.Context, arg1 string, arg2 types.ContainerRemoveOptions) error { +func (m *MockCommonAPIClient) ContainerRemove(arg0 context.Context, arg1 string, arg2 container.RemoveOptions) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ContainerRemove", arg0, arg1, arg2) ret0, _ := ret[0].(error) @@ -444,7 +445,7 @@ func (mr *MockCommonAPIClientMockRecorder) ContainerRename(arg0, arg1, arg2 inte } // ContainerResize mocks base method. -func (m *MockCommonAPIClient) ContainerResize(arg0 context.Context, arg1 string, arg2 types.ResizeOptions) error { +func (m *MockCommonAPIClient) ContainerResize(arg0 context.Context, arg1 string, arg2 container.ResizeOptions) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ContainerResize", arg0, arg1, arg2) ret0, _ := ret[0].(error) @@ -472,7 +473,7 @@ func (mr *MockCommonAPIClientMockRecorder) ContainerRestart(arg0, arg1, arg2 int } // ContainerStart mocks base method. -func (m *MockCommonAPIClient) ContainerStart(arg0 context.Context, arg1 string, arg2 types.ContainerStartOptions) error { +func (m *MockCommonAPIClient) ContainerStart(arg0 context.Context, arg1 string, arg2 container.StartOptions) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ContainerStart", arg0, arg1, arg2) ret0, _ := ret[0].(error) @@ -827,10 +828,10 @@ func (mr *MockCommonAPIClientMockRecorder) ImageInspectWithRaw(arg0, arg1 interf } // ImageList mocks base method. -func (m *MockCommonAPIClient) ImageList(arg0 context.Context, arg1 types.ImageListOptions) ([]types.ImageSummary, error) { +func (m *MockCommonAPIClient) ImageList(arg0 context.Context, arg1 types.ImageListOptions) ([]image.Summary, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ImageList", arg0, arg1) - ret0, _ := ret[0].([]types.ImageSummary) + ret0, _ := ret[0].([]image.Summary) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -887,10 +888,10 @@ func (mr *MockCommonAPIClientMockRecorder) ImagePush(arg0, arg1, arg2 interface{ } // ImageRemove mocks base method. -func (m *MockCommonAPIClient) ImageRemove(arg0 context.Context, arg1 string, arg2 types.ImageRemoveOptions) ([]types.ImageDeleteResponseItem, error) { +func (m *MockCommonAPIClient) ImageRemove(arg0 context.Context, arg1 string, arg2 types.ImageRemoveOptions) ([]image.DeleteResponse, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ImageRemove", arg0, arg1, arg2) - ret0, _ := ret[0].([]types.ImageDeleteResponseItem) + ret0, _ := ret[0].([]image.DeleteResponse) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -961,10 +962,10 @@ func (mr *MockCommonAPIClientMockRecorder) ImagesPrune(arg0, arg1 interface{}) * } // Info mocks base method. -func (m *MockCommonAPIClient) Info(arg0 context.Context) (types.Info, error) { +func (m *MockCommonAPIClient) Info(arg0 context.Context) (system.Info, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Info", arg0) - ret0, _ := ret[0].(types.Info) + ret0, _ := ret[0].(system.Info) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -1338,7 +1339,7 @@ func (mr *MockCommonAPIClientMockRecorder) PluginUpgrade(arg0, arg1, arg2 interf } // RegistryLogin mocks base method. -func (m *MockCommonAPIClient) RegistryLogin(arg0 context.Context, arg1 types.AuthConfig) (registry.AuthenticateOKBody, error) { +func (m *MockCommonAPIClient) RegistryLogin(arg0 context.Context, arg1 registry.AuthConfig) (registry.AuthenticateOKBody, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "RegistryLogin", arg0, arg1) ret0, _ := ret[0].(registry.AuthenticateOKBody) @@ -1442,10 +1443,10 @@ func (mr *MockCommonAPIClientMockRecorder) ServerVersion(arg0 interface{}) *gomo } // ServiceCreate mocks base method. -func (m *MockCommonAPIClient) ServiceCreate(arg0 context.Context, arg1 swarm.ServiceSpec, arg2 types.ServiceCreateOptions) (types.ServiceCreateResponse, error) { +func (m *MockCommonAPIClient) ServiceCreate(arg0 context.Context, arg1 swarm.ServiceSpec, arg2 types.ServiceCreateOptions) (swarm.ServiceCreateResponse, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ServiceCreate", arg0, arg1, arg2) - ret0, _ := ret[0].(types.ServiceCreateResponse) + ret0, _ := ret[0].(swarm.ServiceCreateResponse) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -1488,7 +1489,7 @@ func (mr *MockCommonAPIClientMockRecorder) ServiceList(arg0, arg1 interface{}) * } // ServiceLogs mocks base method. -func (m *MockCommonAPIClient) ServiceLogs(arg0 context.Context, arg1 string, arg2 types.ContainerLogsOptions) (io.ReadCloser, error) { +func (m *MockCommonAPIClient) ServiceLogs(arg0 context.Context, arg1 string, arg2 container.LogsOptions) (io.ReadCloser, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ServiceLogs", arg0, arg1, arg2) ret0, _ := ret[0].(io.ReadCloser) @@ -1517,10 +1518,10 @@ func (mr *MockCommonAPIClientMockRecorder) ServiceRemove(arg0, arg1 interface{}) } // ServiceUpdate mocks base method. -func (m *MockCommonAPIClient) ServiceUpdate(arg0 context.Context, arg1 string, arg2 swarm.Version, arg3 swarm.ServiceSpec, arg4 types.ServiceUpdateOptions) (types.ServiceUpdateResponse, error) { +func (m *MockCommonAPIClient) ServiceUpdate(arg0 context.Context, arg1 string, arg2 swarm.Version, arg3 swarm.ServiceSpec, arg4 types.ServiceUpdateOptions) (swarm.ServiceUpdateResponse, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ServiceUpdate", arg0, arg1, arg2, arg3, arg4) - ret0, _ := ret[0].(types.ServiceUpdateResponse) + ret0, _ := ret[0].(swarm.ServiceUpdateResponse) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -1664,7 +1665,7 @@ func (mr *MockCommonAPIClientMockRecorder) TaskList(arg0, arg1 interface{}) *gom } // TaskLogs mocks base method. -func (m *MockCommonAPIClient) TaskLogs(arg0 context.Context, arg1 string, arg2 types.ContainerLogsOptions) (io.ReadCloser, error) { +func (m *MockCommonAPIClient) TaskLogs(arg0 context.Context, arg1 string, arg2 container.LogsOptions) (io.ReadCloser, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "TaskLogs", arg0, arg1, arg2) ret0, _ := ret[0].(io.ReadCloser) @@ -1725,7 +1726,7 @@ func (mr *MockCommonAPIClientMockRecorder) VolumeInspectWithRaw(arg0, arg1 inter } // VolumeList mocks base method. -func (m *MockCommonAPIClient) VolumeList(arg0 context.Context, arg1 filters.Args) (volume.ListResponse, error) { +func (m *MockCommonAPIClient) VolumeList(arg0 context.Context, arg1 volume.ListOptions) (volume.ListResponse, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "VolumeList", arg0, arg1) ret0, _ := ret[0].(volume.ListResponse) diff --git a/pkg/testmocks/mock_image_factory.go b/pkg/testmocks/mock_image_factory.go index 42e4ec6b42..9319c4c750 100644 --- a/pkg/testmocks/mock_image_factory.go +++ b/pkg/testmocks/mock_image_factory.go @@ -9,6 +9,7 @@ import ( imgutil "github.com/buildpacks/imgutil" gomock "github.com/golang/mock/gomock" + v1 "github.com/google/go-containerregistry/pkg/v1" ) // MockImageFactory is a mock of ImageFactory interface. @@ -35,7 +36,7 @@ func (m *MockImageFactory) EXPECT() *MockImageFactoryMockRecorder { } // NewImage mocks base method. -func (m *MockImageFactory) NewImage(arg0 string, arg1 bool, arg2 string) (imgutil.Image, error) { +func (m *MockImageFactory) NewImage(arg0 string, arg1 bool, arg2 v1.Platform) (imgutil.Image, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "NewImage", arg0, arg1, arg2) ret0, _ := ret[0].(imgutil.Image) diff --git a/pkg/testmocks/mock_index_factory.go b/pkg/testmocks/mock_index_factory.go new file mode 100644 index 0000000000..dc38d11f6a --- /dev/null +++ b/pkg/testmocks/mock_index_factory.go @@ -0,0 +1,116 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/buildpacks/pack/pkg/client (interfaces: IndexFactory) + +// Package testmocks is a generated GoMock package. +package testmocks + +import ( + reflect "reflect" + + imgutil "github.com/buildpacks/imgutil" + index "github.com/buildpacks/imgutil/index" + gomock "github.com/golang/mock/gomock" +) + +// MockIndexFactory is a mock of IndexFactory interface. +type MockIndexFactory struct { + ctrl *gomock.Controller + recorder *MockIndexFactoryMockRecorder +} + +// MockIndexFactoryMockRecorder is the mock recorder for MockIndexFactory. +type MockIndexFactoryMockRecorder struct { + mock *MockIndexFactory +} + +// NewMockIndexFactory creates a new mock instance. +func NewMockIndexFactory(ctrl *gomock.Controller) *MockIndexFactory { + mock := &MockIndexFactory{ctrl: ctrl} + mock.recorder = &MockIndexFactoryMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockIndexFactory) EXPECT() *MockIndexFactoryMockRecorder { + return m.recorder +} + +// CreateIndex mocks base method. +func (m *MockIndexFactory) CreateIndex(arg0 string, arg1 ...index.Option) (imgutil.ImageIndex, error) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0} + for _, a := range arg1 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "CreateIndex", varargs...) + ret0, _ := ret[0].(imgutil.ImageIndex) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateIndex indicates an expected call of CreateIndex. +func (mr *MockIndexFactoryMockRecorder) CreateIndex(arg0 interface{}, arg1 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0}, arg1...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateIndex", reflect.TypeOf((*MockIndexFactory)(nil).CreateIndex), varargs...) +} + +// FetchIndex mocks base method. +func (m *MockIndexFactory) FetchIndex(arg0 string, arg1 ...index.Option) (imgutil.ImageIndex, error) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0} + for _, a := range arg1 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "FetchIndex", varargs...) + ret0, _ := ret[0].(imgutil.ImageIndex) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FetchIndex indicates an expected call of FetchIndex. +func (mr *MockIndexFactoryMockRecorder) FetchIndex(arg0 interface{}, arg1 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0}, arg1...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FetchIndex", reflect.TypeOf((*MockIndexFactory)(nil).FetchIndex), varargs...) +} + +// FindIndex mocks base method. +func (m *MockIndexFactory) FindIndex(arg0 string, arg1 ...index.Option) (imgutil.ImageIndex, error) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0} + for _, a := range arg1 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "FindIndex", varargs...) + ret0, _ := ret[0].(imgutil.ImageIndex) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FindIndex indicates an expected call of FindIndex. +func (mr *MockIndexFactoryMockRecorder) FindIndex(arg0 interface{}, arg1 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0}, arg1...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindIndex", reflect.TypeOf((*MockIndexFactory)(nil).FindIndex), varargs...) +} + +// LoadIndex mocks base method. +func (m *MockIndexFactory) LoadIndex(arg0 string, arg1 ...index.Option) (imgutil.ImageIndex, error) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0} + for _, a := range arg1 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "LoadIndex", varargs...) + ret0, _ := ret[0].(imgutil.ImageIndex) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// LoadIndex indicates an expected call of LoadIndex. +func (mr *MockIndexFactoryMockRecorder) LoadIndex(arg0 interface{}, arg1 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0}, arg1...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoadIndex", reflect.TypeOf((*MockIndexFactory)(nil).LoadIndex), varargs...) +} diff --git a/testhelpers/index_manifest_builder.go b/testhelpers/index_manifest_builder.go new file mode 100644 index 0000000000..2d2cbfe016 --- /dev/null +++ b/testhelpers/index_manifest_builder.go @@ -0,0 +1,56 @@ +package testhelpers + +import ( + "bytes" + "strings" + + "github.com/google/go-containerregistry/pkg/name" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/types" + + "github.com/buildpacks/pack/pkg/dist" +) + +func FakeIndexManifestBuilderFn(targets []dist.Target) func(name.Reference) (*v1.IndexManifest, error) { + var manifests = make([]v1.Descriptor, 0) + for _, t := range targets { + if err := t.Range(func(target dist.Target, distroName, distroVersion string) error { + targetStr := strings.Join([]string{ + t.OS, + t.Arch, + t.ArchVariant, + }, "") + + hash, size, err := v1.SHA256(bytes.NewBufferString(strings.Join([]string{targetStr, distroName, distroVersion}, ""))) + manifests = append(manifests, v1.Descriptor{ + MediaType: types.OCIManifestSchema1, + Size: size, + Digest: hash, + URLs: t.URLs(), + Annotations: t.Specs.Annotations, + Platform: &v1.Platform{ + OS: t.OS, + Architecture: t.Arch, + Variant: t.ArchVariant, + OSVersion: distroVersion, + OSFeatures: t.Specs.OSFeatures, + Features: t.Specs.Features, + }, + }) + return err + }); err != nil { + return func(name.Reference) (*v1.IndexManifest, error) { + return nil, err + } + } + } + + return func(ref name.Reference) (*v1.IndexManifest, error) { + return &v1.IndexManifest{ + MediaType: types.OCIImageIndex, + SchemaVersion: 1, + Manifests: manifests, + Annotations: map[string]string{"some-key": "some-version"}, + }, nil + } +} diff --git a/testhelpers/registry.go b/testhelpers/registry.go index 6839e6815c..81c9a21aae 100644 --- a/testhelpers/registry.go +++ b/testhelpers/registry.go @@ -169,7 +169,7 @@ func startRegistry(t *testing.T, runRegistryName, username, password string) (st err = dockerCli(t).CopyToContainer(ctx, ctr.ID, "/", htpasswdTar, dockertypes.CopyToContainerOptions{}) AssertNil(t, err) - err = dockerCli(t).ContainerStart(ctx, ctr.ID, dockertypes.ContainerStartOptions{}) + err = dockerCli(t).ContainerStart(ctx, ctr.ID, dockercontainer.StartOptions{}) AssertNil(t, err) runRegistryPort, err := waitForPortBinding(t, ctr.ID, "5000/tcp", 30*time.Second) diff --git a/testhelpers/testhelpers.go b/testhelpers/testhelpers.go index 3eeb4b6431..7e56adb418 100644 --- a/testhelpers/testhelpers.go +++ b/testhelpers/testhelpers.go @@ -664,7 +664,7 @@ func SkipUnless(t *testing.T, expression bool, reason string) { func RunContainer(ctx context.Context, dockerCli client.CommonAPIClient, id string, stdout io.Writer, stderr io.Writer) error { bodyChan, errChan := container.ContainerWaitWrapper(ctx, dockerCli, id, dcontainer.WaitConditionNextExit) - logs, err := dockerCli.ContainerAttach(ctx, id, dockertypes.ContainerAttachOptions{ + logs, err := dockerCli.ContainerAttach(ctx, id, dcontainer.AttachOptions{ Stream: true, Stdout: true, Stderr: true, @@ -673,7 +673,7 @@ func RunContainer(ctx context.Context, dockerCli client.CommonAPIClient, id stri return err } - if err := dockerCli.ContainerStart(ctx, id, dockertypes.ContainerStartOptions{}); err != nil { + if err := dockerCli.ContainerStart(ctx, id, dcontainer.StartOptions{}); err != nil { return errors.Wrap(err, "container start") } diff --git a/tools/go.sum b/tools/go.sum index 3511f5a3bf..3617a95580 100644 --- a/tools/go.sum +++ b/tools/go.sum @@ -72,6 +72,7 @@ github.com/ashanbrown/forbidigo v1.5.1/go.mod h1:Y8j9jy9ZYAEHXdu723cUlraTqbzjKF1 github.com/ashanbrown/makezero v1.1.1 h1:iCQ87C0V0vSyO+M9E/FZYbu65auqH0lnsOkf5FcB28s= github.com/ashanbrown/makezero v1.1.1/go.mod h1:i1bJLCRSCHOcOa9Y6MyF2FTfMZMFdHvxKHxgO5Z1axI= github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -130,6 +131,7 @@ github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4 github.com/firefart/nonamedreturns v1.0.4 h1:abzI1p7mAEPYuR4A+VLKn4eNDOycjYo2phmY9sfv40Y= github.com/firefart/nonamedreturns v1.0.4/go.mod h1:TDhe/tjI1BXo48CmYbUduTV7BdIga8MAO/xbKdcVsGI= github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= +github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= github.com/fzipp/gocyclo v0.6.0 h1:lsblElZG7d3ALtGMx9fmxeTKZaLLpU8mET09yN4BBLo= @@ -146,6 +148,7 @@ github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9 github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= +github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-toolsmith/astcast v1.1.0 h1:+JN9xZV1A+Re+95pgnMgDboWNVnIMMQXwfBwLRPgSC8= github.com/go-toolsmith/astcast v1.1.0/go.mod h1:qdcuFWeGGS2xX5bLM/c3U9lewg7+Zu4mr+xPwZIB4ZU= @@ -159,6 +162,7 @@ github.com/go-toolsmith/astfmt v1.1.0/go.mod h1:OrcLlRwu0CuiIBp/8b5PYF9ktGVZUjlN github.com/go-toolsmith/astp v1.1.0 h1:dXPuCl6u2llURjdPLLDxJeZInAeZ0/eZwFJmqZMnpQA= github.com/go-toolsmith/astp v1.1.0/go.mod h1:0T1xFGz9hicKs8Z5MfAqSUitoUYS30pDMsRVIDHs8CA= github.com/go-toolsmith/pkgload v1.2.2 h1:0CtmHq/02QhxcF7E9N5LIFcYFsMR5rdovfqTtRKkgIk= +github.com/go-toolsmith/pkgload v1.2.2/go.mod h1:R2hxLNRKuAsiXCo2i5J6ZQPhnPMOVtU+f0arbFPWCus= github.com/go-toolsmith/strparse v1.0.0/go.mod h1:YI2nUKP9YGZnL/L1/DLFBfixrcjslWct4wyljWhSRy8= github.com/go-toolsmith/strparse v1.1.0 h1:GAioeZUK9TGxnLS+qfdqNbA4z0SSm5zVNtCQiyP2Bvw= github.com/go-toolsmith/strparse v1.1.0/go.mod h1:7ksGy58fsaQkGQlY8WVoBFNyEPMGuJin1rfoPS4lBSQ= @@ -269,6 +273,7 @@ github.com/gostaticanalysis/nilerr v0.1.1 h1:ThE+hJP0fEp4zWLkWHWcRyI2Od0p7DlgYG3 github.com/gostaticanalysis/nilerr v0.1.1/go.mod h1:wZYb6YI5YAxxq0i1+VJbY0s2YONW0HU0GPE3+5PWN4A= github.com/gostaticanalysis/testutil v0.3.1-0.20210208050101-bfb5c8eec0e4/go.mod h1:D+FIZ+7OahH3ePw/izIEeH5I06eKs1IKI4Xr64/Am3M= github.com/gostaticanalysis/testutil v0.4.0 h1:nhdCmubdmDF6VEatUNjgUZBJKWRqugoISdUv3PPQgHY= +github.com/gostaticanalysis/testutil v0.4.0/go.mod h1:bLIoPefWXrRi/ssLFWX1dx7Repi5x3CuviD3dgAZaBU= github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= @@ -317,9 +322,11 @@ github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kulti/thelper v0.6.3 h1:ElhKf+AlItIu+xGnI990no4cE2+XaSu1ULymV2Yulxs= github.com/kulti/thelper v0.6.3/go.mod h1:DsqKShOvP40epevkFrvIwkCMNYxMeTNjdWL4dqWHZ6I= github.com/kunwardeep/paralleltest v1.0.6 h1:FCKYMF1OF2+RveWlABsdnmsvJrei5aoyZoaGS+Ugg8g= @@ -375,6 +382,7 @@ github.com/nakabonne/nestif v0.3.1/go.mod h1:9EtoZochLn5iUprVDmDjqGKPofoUEBL8U4N github.com/nbutton23/zxcvbn-go v0.0.0-20210217022336-fa2cb2858354 h1:4kuARK6Y6FxaNu/BnU2OAaLF86eTVhP2hjTB6iMvItA= github.com/nbutton23/zxcvbn-go v0.0.0-20210217022336-fa2cb2858354/go.mod h1:KSVJerMDfblTH7p5MZaTt+8zaT2iEk3AkVb9PQdZuE8= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nishanths/exhaustive v0.9.5 h1:TzssWan6orBiLYVqewCG8faud9qlFntJE30ACpzmGME= github.com/nishanths/exhaustive v0.9.5/go.mod h1:IbwrGdVMizvDcIxPYGVdQn5BqWJaOwpCvg4RGb8r/TA= github.com/nishanths/predeclared v0.2.2 h1:V2EPdZPliZymNAn79T8RkNApBjMmVKh5XRpLm/w98Vk= @@ -384,7 +392,9 @@ github.com/nunnatsa/ginkgolinter v0.9.0/go.mod h1:FHaMLURXP7qImeH6bvxWJUpyH+2tuq github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/onsi/ginkgo/v2 v2.8.0 h1:pAM+oBNPrpXRs+E/8spkeGx9QgekbRVyr74EUvRVOUI= +github.com/onsi/ginkgo/v2 v2.8.0/go.mod h1:6JsQiECmxCa3V5st74AL/AmsV482EDdVrGaVW6z3oYU= github.com/onsi/gomega v1.26.0 h1:03cDLK28U6hWvCAns6NeydX3zIm4SF3ci69ulidS32Q= +github.com/onsi/gomega v1.26.0/go.mod h1:r+zV744Re+DiYCIPRlYOTxn0YkOLcAnW8k1xXdMPGhM= github.com/otiai10/copy v1.2.0 h1:HvG945u96iNadPoG2/Ja2+AUJeW5YuFQMixq9yirC+k= github.com/otiai10/copy v1.2.0/go.mod h1:rrF5dJ5F0t/EWSYODDu4j9/vEeYHMkc8jt0zJChqQWw= github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE= @@ -436,6 +446,7 @@ github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 h1:M8mH9eK4OUR4l github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567/go.mod h1:DWNGW8A4Y+GyBgPuaQJuWiy0XYftx4Xm/y5Jqk9I6VQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryancurrah/gomodguard v1.3.0 h1:q15RT/pd6UggBXVBuLps8BXRvl5GPBcwVA7BJHMLuTw= github.com/ryancurrah/gomodguard v1.3.0/go.mod h1:ggBxb3luypPEzqVtq33ee7YSN35V28XeGnid8dnni50= @@ -547,6 +558,7 @@ go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= +go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= @@ -652,6 +664,7 @@ golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -934,6 +947,7 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=