Skip to content

Commit

Permalink
WIP - first version
Browse files Browse the repository at this point in the history
Signed-off-by: Juan Bustamante <[email protected]>
  • Loading branch information
jjbustamante committed Jan 11, 2023
1 parent 940fd93 commit a409fc5
Show file tree
Hide file tree
Showing 9 changed files with 156 additions and 21 deletions.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ require (
github.com/Masterminds/semver v1.5.0
github.com/Microsoft/go-winio v0.6.0
github.com/apex/log v1.9.0
github.com/buildpacks/imgutil v0.0.0-20221109161319-3252b1f13dfa
github.com/buildpacks/imgutil v0.0.0-20221215222256-74b012624b31
github.com/buildpacks/lifecycle v0.15.1
github.com/docker/cli v20.10.21+incompatible
github.com/docker/docker v20.10.21+incompatible
Expand Down
6 changes: 6 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,12 @@ github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0Bsq
github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE=
github.com/buildpacks/imgutil v0.0.0-20221109161319-3252b1f13dfa h1:g72s4SWu3zI2FY/0Vu4IAoc9UPbGmy5/IR+pJT3SRB4=
github.com/buildpacks/imgutil v0.0.0-20221109161319-3252b1f13dfa/go.mod h1:/aefwfzb14yQfd4KauUS5B0UVJpi+Fno25mh/Us81Ak=
github.com/buildpacks/imgutil v0.0.0-20221207215820-ae64b7e87bc6 h1:WXIoV4o2ri7q/7Vzmw9K9iOUvaysgUXZnejaOOH59jg=
github.com/buildpacks/imgutil v0.0.0-20221207215820-ae64b7e87bc6/go.mod h1:7XJyKf8MwQfHcsjjA9et24BWN3gE1FztxnN8de54mL0=
github.com/buildpacks/imgutil v0.0.0-20221214232237-ce32548bed85 h1:1SskqULXLk63UQb6HW6mYD/nb8HoxoAjRs3yt2TdZ/s=
github.com/buildpacks/imgutil v0.0.0-20221214232237-ce32548bed85/go.mod h1:7XJyKf8MwQfHcsjjA9et24BWN3gE1FztxnN8de54mL0=
github.com/buildpacks/imgutil v0.0.0-20221215222256-74b012624b31 h1:VwVOoV9htJvGVurT0kRKXtzP9XeNnlcPT/2T7RtsVRc=
github.com/buildpacks/imgutil v0.0.0-20221215222256-74b012624b31/go.mod h1:7XJyKf8MwQfHcsjjA9et24BWN3gE1FztxnN8de54mL0=
github.com/buildpacks/lifecycle v0.15.1 h1:GVEGMaEemFOj1gY9Y2O3lZyYa2c11Bo/K+BgnVhGgrg=
github.com/buildpacks/lifecycle v0.15.1/go.mod h1:JjyTCa8GBFCRInA36vsMllVntv/Hs/ccHpVIE6VebH8=
github.com/caarlos0/ctrlc v1.0.0/go.mod h1:CdXpj4rmq0q/1Eb44M9zi2nKB0QraNKuRGYGrrHhcQw=
Expand Down
20 changes: 14 additions & 6 deletions internal/build/lifecycle_execution.go
Original file line number Diff line number Diff line change
Expand Up @@ -672,12 +672,20 @@ func (l *LifecycleExecution) Export(ctx context.Context, buildCache, launchCache
)
export = phaseFactory.New(NewPhaseConfigProvider("exporter", l, opts...))
} else {
opts = append(
opts,
WithDaemonAccess(l.opts.DockerHost),
WithFlags("-daemon", "-launch-cache", l.mountPaths.launchCacheDir()),
WithBinds(fmt.Sprintf("%s:%s", launchCache.Name(), l.mountPaths.launchCacheDir())),
)
if l.opts.LayoutPath != "" {
opts = append(
opts,
WithFlags("-layout"),
WithBinds(fmt.Sprintf("%s:%s", l.opts.LayoutPath, l.mountPaths.LayoutDir())),
)
} else {
opts = append(
opts,
WithDaemonAccess(l.opts.DockerHost),
WithFlags("-daemon", "-launch-cache", l.mountPaths.launchCacheDir()),
WithBinds(fmt.Sprintf("%s:%s", launchCache.Name(), l.mountPaths.launchCacheDir())),
)
}
export = phaseFactory.New(NewPhaseConfigProvider("exporter", l, opts...))
}

Expand Down
1 change: 1 addition & 0 deletions internal/build/lifecycle_executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ type LifecycleOptions struct {
CacheImage string
HTTPProxy string
HTTPSProxy string
LayoutPath string
NoProxy string
Network string
AdditionalTags []string
Expand Down
4 changes: 4 additions & 0 deletions internal/build/mount_paths.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,7 @@ func (m mountPaths) launchCacheDir() string {
func (m mountPaths) sbomDir() string {
return m.join(m.volume, "layers", "sbom")
}

func (m mountPaths) LayoutDir() string {
return m.join(m.volume, "oci")
}
24 changes: 20 additions & 4 deletions internal/commands/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,12 +66,15 @@ func Build(logger logging.Logger, cfg config.Config, packClient PackClient) *cob
"be provided directly to build using `--builder`, or can be set using the `set-default-builder` command. For more " +
"on how to use `pack build`, see: https://buildpacks.io/docs/app-developer-guide/build-an-app/.",
RunE: logError(logger, func(cmd *cobra.Command, args []string) error {
if err := validateBuildFlags(&flags, cfg, packClient, logger); err != nil {
imageName := args[0]
layoutPath := layoutFeature(imageName)
if layoutPath != "" {
imageName = layoutPath
}
if err := validateBuildFlags(&flags, cfg, packClient, layoutPath, logger); err != nil {
return err
}

imageName := args[0]

descriptor, actualDescriptorPath, err := parseProjectToml(flags.AppPath, flags.DescriptorPath)
if err != nil {
return err
Expand Down Expand Up @@ -173,6 +176,7 @@ func Build(logger logging.Logger, cfg config.Config, packClient PackClient) *cob
Interactive: flags.Interactive,
SBOMDestinationDir: flags.SBOMDestinationDir,
CreationTime: dateTime,
LayoutAppPath: layoutPath,
}); err != nil {
return errors.Wrap(err, "failed to build")
}
Expand Down Expand Up @@ -245,7 +249,7 @@ This option may set DOCKER_HOST environment variable for the build container if
}
}

func validateBuildFlags(flags *BuildFlags, cfg config.Config, packClient PackClient, logger logging.Logger) error {
func validateBuildFlags(flags *BuildFlags, cfg config.Config, packClient PackClient, layoutPath string, logger logging.Logger) error {
if flags.Registry != "" && !cfg.Experimental {
return client.NewExperimentError("Support for buildpack registries is currently experimental.")
}
Expand Down Expand Up @@ -274,6 +278,10 @@ func validateBuildFlags(flags *BuildFlags, cfg config.Config, packClient PackCli
return client.NewExperimentError("Interactive mode is currently experimental.")
}

if layoutPath != "" && !cfg.Experimental {
return client.NewExperimentError("Exporting to OCI layout is currently experimental.")
}

return nil
}

Expand Down Expand Up @@ -340,3 +348,11 @@ func parseProjectToml(appPath, descriptorPath string) (projectTypes.Descriptor,
descriptor, err := project.ReadProjectDescriptor(actualPath)
return descriptor, actualPath, err
}

func layoutFeature(imageName string) string {
if strings.HasPrefix(imageName, "oci:") {
imageNameParsed := strings.Split(imageName, ":")
return imageNameParsed[1]
}
return ""
}
72 changes: 66 additions & 6 deletions pkg/client/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@ type BuildOptions struct {
// built atop.
RunImage string

// LayoutAppPath is the path to export the output image in OCI layout format
// If unset it defaults to "oci" folder in current working directory.
LayoutAppPath string

// Address of docker daemon exposed to build container
// e.g. tcp://example.com:1234, unix:///run/user/1000/podman/podman.sock
DockerHost string
Expand Down Expand Up @@ -224,10 +228,21 @@ var IsSuggestedBuilderFunc = func(b string) bool {
// If any configuration is deemed invalid, or if any lifecycle phases fail,
// an error will be returned and no image produced.
func (c *Client) Build(ctx context.Context, opts BuildOptions) error {
var layoutRootPath string
imageRef, err := c.parseTagReference(opts.Image)
if err != nil {
return errors.Wrapf(err, "invalid image name '%s'", opts.Image)
}
imgRegistry := imageRef.Context().RegistryStr()
imageName := imageRef.Name()

if opts.LayoutAppPath != "" {
layoutRootPath, imageName, err = c.processLayoutPath(opts.LayoutAppPath)
if err != nil {
return errors.Wrapf(err, "invalid path '%s' to save the image in oci layout format", imageName)
}
imgRegistry = ""
}

appPath, err := c.processAppPath(opts.AppPath)
if err != nil {
Expand All @@ -251,8 +266,21 @@ func (c *Client) Build(ctx context.Context, opts BuildOptions) error {
return errors.Wrapf(err, "invalid builder %s", style.Symbol(opts.Builder))
}

runImageName := c.resolveRunImage(opts.RunImage, imageRef.Context().RegistryStr(), builderRef.Context().RegistryStr(), bldr.Stack(), opts.AdditionalMirrors, opts.Publish)
runImage, err := c.validateRunImage(ctx, runImageName, opts.PullPolicy, opts.Publish, bldr.StackID)
runImageName := c.resolveRunImage(opts.RunImage, imgRegistry, builderRef.Context().RegistryStr(), bldr.Stack(), opts.AdditionalMirrors, opts.Publish)

fetchOptions := image.FetchOptions{Daemon: !opts.Publish, PullPolicy: opts.PullPolicy}
if layoutRootPath != "" {
reference, err := name.ParseReference(runImageName)
if err != nil {
return err
}
runImagePath := filepath.Join(layoutRootPath, reference.Context().RegistryStr(), reference.Context().RepositoryStr(), reference.Identifier())
fetchOptions.LayoutOption = image.LayoutOption{
Path: runImagePath,
Sparse: false,
}
}
runImage, err := c.validateRunImage(ctx, runImageName, fetchOptions, bldr.StackID)
if err != nil {
return errors.Wrapf(err, "invalid run-image '%s'", runImageName)
}
Expand Down Expand Up @@ -379,7 +407,7 @@ func (c *Client) Build(ctx context.Context, opts BuildOptions) error {
GID: opts.GroupID,
PreviousImage: opts.PreviousImage,
Interactive: opts.Interactive,
Termui: termui.NewTermui(imageRef.Name(), ephemeralBuilder, runImageName),
Termui: termui.NewTermui(imageName, ephemeralBuilder, runImageName),
SBOMDestinationDir: opts.SBOMDestinationDir,
CreationTime: opts.CreationTime,
}
Expand Down Expand Up @@ -497,11 +525,11 @@ func (c *Client) getBuilder(img imgutil.Image) (*builder.Builder, error) {
return bldr, nil
}

func (c *Client) validateRunImage(context context.Context, name string, pullPolicy image.PullPolicy, publish bool, expectedStack string) (imgutil.Image, error) {
if name == "" {
func (c *Client) validateRunImage(context context.Context, imageName string, opts image.FetchOptions, expectedStack string) (imgutil.Image, error) {
if imageName == "" {
return nil, errors.New("run image must be specified")
}
img, err := c.imageFetcher.Fetch(context, name, image.FetchOptions{Daemon: !publish, PullPolicy: pullPolicy})
img, err := c.imageFetcher.Fetch(context, imageName, opts)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -637,6 +665,38 @@ func (c *Client) processAppPath(appPath string) (string, error) {
return resolvedAppPath, nil
}

func (c *Client) processLayoutPath(imagePath string) (string, string, error) {
var (
fullImagePath string
err error
)
if imagePath == "" {
return "", "", nil
}

/* if fullImagePath, err = filepath.EvalSymlinks(imagePath); err != nil {
return "", "", errors.Wrap(err, "evaluate symlink")
}
*/

// Abs calls Clean on the result, so at this point the fullImagePath do not have trailing "/"
if fullImagePath, err = filepath.Abs(imagePath); err != nil {
return "", "", errors.Wrap(err, "resolve absolute path")
}

imageDirectoryName := filepath.Base(fullImagePath)
c.logger.Debugf("imageDirectoryName: %s", imageDirectoryName)
layoutRootPath := filepath.Join(filepath.Dir(fullImagePath), "oci")
c.logger.Debugf("layout root path: %s", layoutRootPath)
fullImagePath = filepath.Join(layoutRootPath, imageDirectoryName)

if err = os.MkdirAll(fullImagePath, os.ModePerm); err != nil {
return "", "", errors.Wrapf(err, "could not create the directory %s", fullImagePath)
}
c.logger.Debugf("Path to save the image: %s", fullImagePath)
return layoutRootPath, fullImagePath, nil
}

func (c *Client) processProxyConfig(config *ProxyConfig) ProxyConfig {
var (
httpProxy, httpsProxy, noProxy string
Expand Down
1 change: 0 additions & 1 deletion pkg/client/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package client
import (
"errors"
"fmt"

"github.com/google/go-containerregistry/pkg/name"

"github.com/buildpacks/pack/internal/builder"
Expand Down
47 changes: 44 additions & 3 deletions pkg/image/fetcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"context"
"encoding/base64"
"encoding/json"
"github.com/buildpacks/imgutil/layout"
"github.com/buildpacks/imgutil/layout/sparse"
"io"
"strings"

Expand All @@ -27,6 +29,11 @@ import (
// Values in these functions are set through currying.
type FetcherOption func(c *Fetcher)

type LayoutOption struct {
Path string
Sparse bool
}

// WithRegistryMirrors supply your own mirrors for registry.
func WithRegistryMirrors(registryMirrors map[string]string) FetcherOption {
return func(c *Fetcher) {
Expand All @@ -48,9 +55,10 @@ type Fetcher struct {
}

type FetchOptions struct {
Daemon bool
Platform string
PullPolicy PullPolicy
Daemon bool
Platform string
PullPolicy PullPolicy
LayoutOption LayoutOption
}

func NewFetcher(logger logging.Logger, docker client.CommonAPIClient, opts ...FetcherOption) *Fetcher {
Expand All @@ -75,6 +83,10 @@ func (f *Fetcher) Fetch(ctx context.Context, name string, options FetchOptions)
return nil, err
}

if (options.LayoutOption != LayoutOption{}) {
return f.fetchLayoutImage(name, options.LayoutOption)
}

if !options.Daemon {
return f.fetchRemoteImage(name)
}
Expand Down Expand Up @@ -125,6 +137,35 @@ func (f *Fetcher) fetchRemoteImage(name string) (imgutil.Image, error) {
return image, nil
}

func (f *Fetcher) fetchLayoutImage(name string, options LayoutOption) (imgutil.Image, error) {
var (
image imgutil.Image
err error
)

v1Image, err := remote.NewV1Image(name, f.keychain)
if err != nil {
return nil, err
}

if options.Sparse {
image, err = sparse.NewImage(options.Path, v1Image)
} else {
image, err = layout.NewImage(options.Path, layout.FromBaseImage(v1Image))
}

if err != nil {
return nil, err
}

err = image.Save()
if err != nil {
return nil, err
}

return image, nil
}

func (f *Fetcher) pullImage(ctx context.Context, imageID string, platform string) error {
regAuth, err := f.registryAuth(imageID)
if err != nil {
Expand Down

0 comments on commit a409fc5

Please sign in to comment.