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 12, 2023
1 parent d1024e9 commit c1c247a
Show file tree
Hide file tree
Showing 7 changed files with 164 additions and 23 deletions.
33 changes: 25 additions & 8 deletions internal/build/lifecycle_execution.go
Original file line number Diff line number Diff line change
Expand Up @@ -494,7 +494,14 @@ func (l *LifecycleExecution) Analyze(ctx context.Context, buildCache, launchCach
flagsOp := WithFlags(flags...)

var analyze RunnerCleaner
if l.opts.Publish {

layoutOpts := NullOp()
if l.opts.LayoutPath != "" {
args = append([]string{"-layout"}, args...)
layoutOpts = WithBinds(fmt.Sprintf("%s:%s", l.opts.LayoutPath, l.mountPaths.LayoutDir()))
}

if l.opts.Publish || l.opts.LayoutPath != "" {
authConfig, err := auth.BuildEnvVar(authn.DefaultKeychain, l.opts.Image.String(), l.opts.RunImage, l.opts.CacheImage, l.opts.PreviousImage)
if err != nil {
return err
Expand All @@ -505,14 +512,15 @@ func (l *LifecycleExecution) Analyze(ctx context.Context, buildCache, launchCach
l,
WithLogPrefix("analyzer"),
WithImage(l.opts.LifecycleImage),
WithEnv(fmt.Sprintf("%s=%d", builder.EnvUID, l.opts.Builder.UID()), fmt.Sprintf("%s=%d", builder.EnvGID, l.opts.Builder.GID())),
WithEnv(fmt.Sprintf("%s=%d", builder.EnvUID, l.opts.Builder.UID()), fmt.Sprintf("%s=%d", builder.EnvGID, l.opts.Builder.GID()), "CNB_EXPERIMENTAL_MODE=warn"),
WithRegistryAccess(authConfig),
WithRoot(),
WithArgs(l.withLogLevel(args...)...),
WithNetwork(l.opts.Network),
flagsOp,
cacheBindOp,
stackOp,
layoutOpts,
)

analyze = phaseFactory.New(configProvider)
Expand Down Expand Up @@ -672,12 +680,21 @@ 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())),
WithEnv("CNB_EXPERIMENTAL_MODE=warn"),
)
} 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
3 changes: 3 additions & 0 deletions internal/build/lifecycle_executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ var (
api.MustParse("0.8"),
api.MustParse("0.9"),
api.MustParse("0.10"),
api.MustParse("0.11"),
api.MustParse("0.12"),
}
)

Expand Down Expand Up @@ -85,6 +87,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 @@ -65,12 +65,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 @@ -172,6 +175,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 @@ -244,7 +248,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 @@ -273,6 +277,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 @@ -339,3 +347,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 ""
}
75 changes: 68 additions & 7 deletions pkg/client/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,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 @@ -222,10 +226,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 @@ -249,8 +264,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 @@ -377,9 +405,10 @@ 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,
LayoutPath: layoutRootPath,
}

lifecycleVersion := ephemeralBuilder.LifecycleDescriptor().Info.Version
Expand All @@ -388,7 +417,7 @@ func (c *Client) Build(ctx context.Context, opts BuildOptions) error {
lifecycleSupportsCreator := !lifecycleVersion.LessThan(semver.MustParse(minLifecycleVersionSupportingCreator))

if lifecycleSupportsCreator && opts.TrustBuilder(opts.Builder) {
lifecycleOpts.UseCreator = true
lifecycleOpts.UseCreator = false
// no need to fetch a lifecycle image, it won't be used
if err := c.lifecycleExecutor.Execute(ctx, lifecycleOpts); err != nil {
return errors.Wrap(err, "executing lifecycle")
Expand Down Expand Up @@ -495,11 +524,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 @@ -635,6 +664,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 c1c247a

Please sign in to comment.