Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor: local images should be v1.Images #238

Merged
merged 2 commits into from
Jan 16, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
507 changes: 507 additions & 0 deletions cnb_image.go

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions fakes/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,14 @@ func (i *Image) Identifier() (imgutil.Identifier, error) {
return i.identifier, nil
}

func (i *Image) Kind() string {
return ""
}

func (i *Image) UnderlyingImage() v1.Image {
return nil
}

func (i *Image) Rebase(baseTopLayer string, newBase imgutil.Image) error {
i.base = newBase.Name()
return nil
Expand Down
6 changes: 4 additions & 2 deletions image.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,12 @@ type Image interface {
Env(key string) (string, error)
// Found tells whether the image exists in the repository by `Name()`.
Found() bool
// Valid returns true if the image is well-formed (e.g. all manifest layers exist on the registry).
Valid() bool
GetAnnotateRefName() (string, error)
// GetLayer retrieves layer by diff id. Returns a reader of the uncompressed contents of the layer.
GetLayer(diffID string) (io.ReadCloser, error)
History() ([]v1.History, error)
Identifier() (Identifier, error)
Kind() string
Label(string) (string, error)
Labels() (map[string]string, error)
// ManifestSize returns the size of the manifest. If a manifest doesn't exist, it returns 0.
Expand All @@ -37,6 +36,9 @@ type Image interface {
OSVersion() (string, error)
// TopLayer returns the diff id for the top layer
TopLayer() (string, error)
UnderlyingImage() v1.Image
// Valid returns true if the image is well-formed (e.g. all manifest layers exist on the registry).
Valid() bool
Variant() (string, error)
WorkingDir() (string, error)

Expand Down
8 changes: 8 additions & 0 deletions layout/layout.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,10 @@ func (i *Image) Identifier() (imgutil.Identifier, error) {
return newLayoutIdentifier(i.path, hash)
}

func (i *Image) Kind() string {
return fmt.Sprintf("%T", i)
}

func (i *Image) Label(key string) (string, error) {
cfg, err := i.Image.ConfigFile()
if err != nil {
Expand Down Expand Up @@ -271,6 +275,10 @@ func (i *Image) TopLayer() (string, error) {
return hex.String(), nil
}

func (i *Image) UnderlyingImage() v1.Image {
return i.Image
}

func (i *Image) Variant() (string, error) {
cfg, err := i.Image.ConfigFile()
if err != nil {
Expand Down
8 changes: 8 additions & 0 deletions local/local.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,10 @@ func (i *Image) Identifier() (imgutil.Identifier, error) {
}, nil
}

func (i *Image) Kind() string {
return fmt.Sprintf("%T", i)
}

func (i *Image) Label(key string) (string, error) {
labels := i.inspect.Config.Labels
return labels[key], nil
Expand Down Expand Up @@ -155,6 +159,10 @@ func (i *Image) TopLayer() (string, error) {
return topLayer, nil
}

func (i *Image) UnderlyingImage() v1.Image {
return nil
}

func (i *Image) Variant() (string, error) {
return i.inspect.Variant, nil
}
Expand Down
103 changes: 103 additions & 0 deletions locallayout/image.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package locallayout

import (
"errors"
"fmt"
"io"
"strings"
"sync"

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

"github.com/buildpacks/imgutil"
)

// Image wraps an imgutil.CNBImageCore and implements the methods needed to complete the imgutil.Image interface.
type Image struct {
*imgutil.CNBImageCore
lastIdentifier string
daemonOS string
downloadOnce *sync.Once
}

var _ imgutil.Image = &Image{}

func (i *Image) Found() bool {
return i.lastIdentifier != ""
}

func (i *Image) Identifier() (imgutil.Identifier, error) {
return idStringer{
id: strings.TrimPrefix(i.lastIdentifier, "sha256:"),
}, nil
}

type idStringer struct {
id string
}

func (i idStringer) String() string {
return i.id
}

// GetLayer returns an io.ReadCloser with uncompressed layer data.
// The layer will always have data, even if that means downloading all of the image layers from the daemon.
func (i *Image) GetLayer(diffID string) (io.ReadCloser, error) {
layerHash, err := v1.NewHash(diffID)
if err != nil {
return nil, err
}
if err = i.ensureLayers(); err != nil {
return nil, err
}
layer, err := i.LayerByDiffID(layerHash)
if err != nil {
return nil, fmt.Errorf("image %q does not contain layer with diff ID %q", i.Name(), layerHash.String())
}
return layer.Uncompressed()
}

func (i *Image) ensureLayers() error {
var err error
i.downloadOnce.Do(func() {
err = i.Store.DownloadLayersFor(i.lastIdentifier)
})
if err != nil {
return fmt.Errorf("fetching base layers: %w", err)
}
return nil
}

func (i *Image) SetOS(osVal string) error {
if osVal != i.daemonOS {
return errors.New("invalid os: must match the daemon")
}
return i.CNBImageCore.SetOS(osVal)
}

func (i *Image) Rebase(baseTopLayerDiffID string, withNewBase imgutil.Image) error {
if err := i.ensureLayers(); err != nil {
return err
}
return i.CNBImageCore.Rebase(baseTopLayerDiffID, withNewBase)
}

func (i *Image) Save(additionalNames ...string) error {
var err error
i.lastIdentifier, err = i.Store.Save(i, i.Name(), additionalNames...)
return err
}

func (i *Image) SaveAs(name string, additionalNames ...string) error {
var err error
i.lastIdentifier, err = i.Store.Save(i, name, additionalNames...)
return err
}

func (i *Image) SaveFile() (string, error) {
return i.Store.SaveFile(i, i.Name())
}

func (i *Image) Delete() error {
return i.Store.Delete(i.lastIdentifier)
}
128 changes: 128 additions & 0 deletions locallayout/new.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package locallayout

import (
"context"
"fmt"
"sync"

"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/image"
"github.com/docker/docker/client"

"github.com/buildpacks/imgutil"
)

// NewImage returns a new image that can be modified and saved to a docker daemon
// via a tarball in OCI layout format.
natalieparellano marked this conversation as resolved.
Show resolved Hide resolved
func NewImage(repoName string, dockerClient DockerClient, ops ...func(*imgutil.ImageOptions)) (imgutil.Image, error) {
options := &imgutil.ImageOptions{}
for _, op := range ops {
op(options)
}

var err error
options.Platform, err = processDefaultPlatformOption(options.Platform, dockerClient)
if err != nil {
return nil, err
}

options.PreviousImage, err = processPreviousImageOption(options.PreviousImageRepoName, dockerClient)
if err != nil {
return nil, err
}

var (
baseIdentifier string
store imgutil.ImageStore = &Store{dockerClient: dockerClient}
)
baseImage, err := processBaseImageOption(options.BaseImageRepoName, dockerClient)
if err != nil {
return nil, err
}
if baseImage != nil {
options.BaseImage = baseImage
baseIdentifier = baseImage.identifier
store = baseImage.store
}

cnbImage, err := imgutil.NewCNBImage(repoName, store, *options)
if err != nil {
return nil, err
}

return &Image{
CNBImageCore: cnbImage,
lastIdentifier: baseIdentifier,
daemonOS: options.Platform.OS,
downloadOnce: &sync.Once{},
}, nil
}

func processDefaultPlatformOption(requestedPlatform imgutil.Platform, dockerClient DockerClient) (imgutil.Platform, error) {
dockerPlatform, err := defaultPlatform(dockerClient)
if err != nil {
return imgutil.Platform{}, err
}
if (requestedPlatform == imgutil.Platform{}) {
return dockerPlatform, nil
}
if requestedPlatform.OS != "" && requestedPlatform.OS != dockerPlatform.OS {
return imgutil.Platform{},
fmt.Errorf("invalid os: platform os %q must match the daemon os %q", requestedPlatform.OS, dockerPlatform.OS)
}
return requestedPlatform, nil
}

func defaultPlatform(dockerClient DockerClient) (imgutil.Platform, error) {
daemonInfo, err := dockerClient.ServerVersion(context.Background())
if err != nil {
return imgutil.Platform{}, err
}
return imgutil.Platform{
OS: daemonInfo.Os,
Architecture: daemonInfo.Arch,
}, nil
}

func processPreviousImageOption(repoName string, dockerClient DockerClient) (*v1ImageFacade, error) {
if repoName == "" {
return nil, nil
}
inspect, history, err := getInspectAndHistory(repoName, dockerClient)
if err != nil {
return nil, err
}
if inspect == nil {
return nil, nil
}
return newV1ImageFacadeFromInspect(*inspect, history, dockerClient, true)
}

func processBaseImageOption(repoName string, dockerClient DockerClient) (*v1ImageFacade, error) {
if repoName == "" {
return nil, nil
}
inspect, history, err := getInspectAndHistory(repoName, dockerClient)
if err != nil {
return nil, err
}
if inspect == nil {
return nil, nil
}
return newV1ImageFacadeFromInspect(*inspect, history, dockerClient, false)
}

func getInspectAndHistory(repoName string, dockerClient DockerClient) (*types.ImageInspect, []image.HistoryResponseItem, error) {
inspect, _, err := dockerClient.ImageInspectWithRaw(context.Background(), repoName)
if err != nil {
if client.IsErrNotFound(err) {
return nil, nil, nil
}
return nil, nil, fmt.Errorf("inspecting image %q: %w", repoName, err)
}
history, err := dockerClient.ImageHistory(context.Background(), repoName)
if err != nil {
return nil, nil, fmt.Errorf("get history for image %q: %w", repoName, err)
}
return &inspect, history, nil
}
Loading