Skip to content

Commit

Permalink
Add CreateTar to image API
Browse files Browse the repository at this point in the history
Add api call which uses go-container registry to create OCI images from
standard tar archives.

Consume new API when generating final images instead of docker building them
and adapts/add tests as necessary.

This change now allows to carry over xattrs to final images.

Fixes #266
  • Loading branch information
mudler committed Oct 29, 2021
1 parent 1b35a67 commit c7f9708
Show file tree
Hide file tree
Showing 16 changed files with 306 additions and 69 deletions.
6 changes: 1 addition & 5 deletions pkg/api/core/image/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ package image

import (
"encoding/json"
"fmt"
"os"
"strings"

Expand Down Expand Up @@ -156,10 +155,7 @@ func (c *Cache) All(fn func(CacheResult)) {
}

for key := range c.store.Keys(nil) {
val, err := c.store.Read(key)
if err != nil {
panic(fmt.Sprintf("key %s had no value", key))
}
val, _ := c.store.Read(key)
fn(CacheResult{key: key, value: string(val)})
}
}
Expand Down
81 changes: 81 additions & 0 deletions pkg/api/core/image/create.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// Copyright © 2021 Ettore Di Giacinto <[email protected]>
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along
// with this program; if not, see <http://www.gnu.org/licenses/>.

package image

import (
"io"
"os"

"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/mutate"
"github.com/google/go-containerregistry/pkg/v1/tarball"
"github.com/pkg/errors"
)

func imageFromTar(imagename string, r io.Reader) (name.Reference, v1.Image, error) {
newRef, err := name.ParseReference(imagename)
if err != nil {
return nil, nil, err
}

layer, err := tarball.LayerFromReader(r)
if err != nil {
return nil, nil, err
}

img, err := mutate.Append(empty.Image, mutate.Addendum{
Layer: layer,
History: v1.History{
CreatedBy: "luet",
Comment: "Custom image",
},
})
if err != nil {
return nil, nil, err
}

return newRef, img, nil
}

// CreateTar a imagetarball from a standard tarball
func CreateTar(srctar, dstimageTar, imagename string) error {
f, err := os.Open(srctar)
if err != nil {
return errors.Wrap(err, "Cannot open "+srctar)
}
defer f.Close()

return CreateTarReader(f, dstimageTar, imagename)
}

// CreateTarReader a imagetarball from a standard tarball
func CreateTarReader(r io.Reader, dstimageTar, imagename string) error {
dstFile, err := os.Create(dstimageTar)
if err != nil {
return errors.Wrap(err, "Cannot create "+dstimageTar)
}
defer dstFile.Close()

newRef, img, err := imageFromTar(imagename, r)
if err != nil {
return err
}

// NOTE: We might also stream that back to the daemon with daemon.Write(tag, img)
return tarball.Write(newRef, img, dstFile)
}
79 changes: 79 additions & 0 deletions pkg/api/core/image/create_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// Copyright © 2021 Ettore Di Giacinto <[email protected]>
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along
// with this program; if not, see <http://www.gnu.org/licenses/>.

package image_test

import (
"os"
"path/filepath"

. "github.com/mudler/luet/pkg/api/core/image"
"github.com/mudler/luet/pkg/api/core/types"
"github.com/mudler/luet/pkg/api/core/types/artifact"
"github.com/mudler/luet/pkg/compiler/backend"
"github.com/mudler/luet/pkg/helpers/file"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)

var _ = Describe("Create", func() {
Context("Creates an OCI image from a standard tar", func() {
It("creates an image which is loadable", func() {
ctx := types.NewContext()

dst, err := ctx.Config.System.TempFile("dst")
Expect(err).ToNot(HaveOccurred())
defer os.RemoveAll(dst.Name())
srcTar, err := ctx.Config.System.TempFile("srcTar")
Expect(err).ToNot(HaveOccurred())
defer os.RemoveAll(srcTar.Name())

b := backend.NewSimpleDockerBackend(ctx)

b.DownloadImage(backend.Options{ImageName: "alpine"})
img, err := b.ImageReference("alpine", false)
Expect(err).ToNot(HaveOccurred())

_, dir, err := Extract(ctx, img, false, nil)
Expect(err).ToNot(HaveOccurred())

defer os.RemoveAll(dir)

Expect(file.Touch(filepath.Join(dir, "test"))).ToNot(HaveOccurred())
Expect(file.Exists(filepath.Join(dir, "bin"))).To(BeTrue())

a := artifact.NewPackageArtifact(srcTar.Name())
a.Compress(dir, 1)

// Unfortunately there is no other easy way to test this
err = CreateTar(srcTar.Name(), dst.Name(), "testimage")
Expect(err).ToNot(HaveOccurred())

b.LoadImage(dst.Name())

Expect(b.ImageExists("testimage")).To(BeTrue())

img, err = b.ImageReference("testimage", false)
Expect(err).ToNot(HaveOccurred())

_, dir, err = Extract(ctx, img, false, nil)
Expect(err).ToNot(HaveOccurred())

defer os.RemoveAll(dir)
Expect(file.Exists(filepath.Join(dir, "bin"))).To(BeTrue())
Expect(file.Exists(filepath.Join(dir, "test"))).To(BeTrue())
})
})
})
17 changes: 12 additions & 5 deletions pkg/api/core/image/extract.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,10 @@ import (
"github.com/pkg/errors"
)

// Extract dir:
// -> First extract delta considering the dir
// Afterward create artifact pointing to the dir

// ExtractDeltaFiles returns an handler to extract files in a list
// ExtractDeltaAdditionsFromImages is a filter that takes two images
// an includes and an excludes list. It computes the delta between the images
// considering the added files only, and applies a filter on them based on the regexes
// in the lists.
func ExtractDeltaAdditionsFiles(
ctx *types.Context,
srcimg v1.Image,
Expand Down Expand Up @@ -127,6 +126,9 @@ func ExtractDeltaAdditionsFiles(
}, nil
}

// ExtractFiles returns a filter that extracts files from the given path (if not empty)
// It then filters files by an include and exclude list.
// The list can be regexes
func ExtractFiles(
ctx *types.Context,
prefixPath string,
Expand Down Expand Up @@ -190,6 +192,9 @@ func ExtractFiles(
}
}

// ExtractReader perform the extracting action over the io.ReadCloser
// it extracts the files over output. Accepts a filter as an option
// and additional containerd Options
func ExtractReader(ctx *types.Context, reader io.ReadCloser, output string, keepPerms bool, filter func(h *tar.Header) (bool, error), opts ...containerdarchive.ApplyOpt) (int64, string, error) {
defer reader.Close()

Expand Down Expand Up @@ -259,6 +264,7 @@ func ExtractReader(ctx *types.Context, reader io.ReadCloser, output string, keep
return c, output, nil
}

// Extract is just syntax sugar around ExtractReader. It extracts an image into a dir
func Extract(ctx *types.Context, img v1.Image, keepPerms bool, filter func(h *tar.Header) (bool, error), opts ...containerdarchive.ApplyOpt) (int64, string, error) {
tmpdiffs, err := ctx.Config.GetSystem().TempDir("extraction")
if err != nil {
Expand All @@ -267,6 +273,7 @@ func Extract(ctx *types.Context, img v1.Image, keepPerms bool, filter func(h *ta
return ExtractReader(ctx, mutate.Extract(img), tmpdiffs, keepPerms, filter, opts...)
}

// ExtractTo is just syntax sugar around ExtractReader
func ExtractTo(ctx *types.Context, img v1.Image, output string, keepPerms bool, filter func(h *tar.Header) (bool, error), opts ...containerdarchive.ApplyOpt) (int64, string, error) {
return ExtractReader(ctx, mutate.Extract(img), output, keepPerms, filter, opts...)
}
54 changes: 16 additions & 38 deletions pkg/api/core/types/artifact/artifact.go
Original file line number Diff line number Diff line change
Expand Up @@ -174,12 +174,6 @@ func (a *PackageArtifact) GetFileName() string {
return path.Base(a.Path)
}

func (a *PackageArtifact) genDockerfile() string {
return `
FROM scratch
COPY . /`
}

// CreateArtifactForFile creates a new artifact from the given file
func CreateArtifactForFile(ctx *types.Context, s string, opts ...func(*PackageArtifact)) (*PackageArtifact, error) {
if _, err := os.Stat(s); os.IsNotExist(err) {
Expand Down Expand Up @@ -211,52 +205,36 @@ func CreateArtifactForFile(ctx *types.Context, s string, opts ...func(*PackageAr

type ImageBuilder interface {
BuildImage(backend.Options) error
LoadImage(path string) error
}

// GenerateFinalImage takes an artifact and builds a Docker image with its content
func (a *PackageArtifact) GenerateFinalImage(ctx *types.Context, imageName string, b ImageBuilder, keepPerms bool) (backend.Options, error) {
builderOpts := backend.Options{}
archive, err := ctx.Config.GetSystem().TempDir("archive")
func (a *PackageArtifact) GenerateFinalImage(ctx *types.Context, imageName string, b ImageBuilder, keepPerms bool) error {
archiveFile, err := os.Open(a.Path)
if err != nil {
return builderOpts, errors.Wrap(err, "error met while creating tempdir for "+a.Path)
}
defer os.RemoveAll(archive) // clean up

uncompressedFiles := filepath.Join(archive, "files")
dockerFile := filepath.Join(archive, "Dockerfile")

if err := os.MkdirAll(uncompressedFiles, os.ModePerm); err != nil {
return builderOpts, errors.Wrap(err, "error met while creating tempdir for "+a.Path)
}

if err := a.Unpack(ctx, uncompressedFiles, keepPerms); err != nil {
return builderOpts, errors.Wrap(err, "error met while uncompressing artifact "+a.Path)
return errors.Wrap(err, "Cannot open "+a.Path)
}
defer archiveFile.Close()

empty, err := fileHelper.DirectoryIsEmpty(uncompressedFiles)
decompressed, err := containerdCompression.DecompressStream(archiveFile)
if err != nil {
return builderOpts, errors.Wrap(err, "error met while checking if directory is empty "+uncompressedFiles)
return errors.Wrap(err, "Cannot open "+a.Path)
}

// See https://github.com/moby/moby/issues/38039.
// We can't generate FROM scratch empty images. Docker will refuse to export them
// workaround: Inject a .virtual empty file
if empty {
fileHelper.Touch(filepath.Join(uncompressedFiles, ".virtual"))
tempimage, err := ctx.Config.GetSystem().TempFile("tempimage")
if err != nil {
return errors.Wrap(err, "error met while creating tempdir for "+a.Path)
}
defer os.RemoveAll(tempimage.Name()) // clean up

data := a.genDockerfile()
if err := ioutil.WriteFile(dockerFile, []byte(data), 0644); err != nil {
return builderOpts, errors.Wrap(err, "error met while rendering artifact dockerfile "+a.Path)
if err := image.CreateTarReader(decompressed, tempimage.Name(), imageName); err != nil {
return errors.Wrap(err, "could not create image from tar")
}

builderOpts = backend.Options{
ImageName: imageName,
SourcePath: archive,
DockerFileName: dockerFile,
Context: uncompressedFiles,
if err := b.LoadImage(tempimage.Name()); err != nil {
return errors.Wrap(err, "while loading image")
}
return builderOpts, b.BuildImage(builderOpts)
return nil
}

// Compress is responsible to archive and compress to the artifact Path.
Expand Down
12 changes: 3 additions & 9 deletions pkg/api/core/types/artifact/artifact_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,9 +146,8 @@ RUN echo bar > /test2`))
err = a.Compress(tmpdir, 1)
Expect(err).ToNot(HaveOccurred())
resultingImage := imageprefix + "foo--1.0"
opts, err := a.GenerateFinalImage(ctx, resultingImage, b, false)
err = a.GenerateFinalImage(ctx, resultingImage, b, false)
Expect(err).ToNot(HaveOccurred())
Expect(opts.ImageName).To(Equal(resultingImage))

Expect(b.ImageExists(resultingImage)).To(BeTrue())

Expand Down Expand Up @@ -196,9 +195,8 @@ RUN echo bar > /test2`))
err = a.Compress(tmpdir, 1)
Expect(err).ToNot(HaveOccurred())
resultingImage := imageprefix + "foo--1.0"
opts, err := a.GenerateFinalImage(ctx, resultingImage, b, false)
err = a.GenerateFinalImage(ctx, resultingImage, b, false)
Expect(err).ToNot(HaveOccurred())
Expect(opts.ImageName).To(Equal(resultingImage))

Expect(b.ImageExists(resultingImage)).To(BeTrue())

Expand All @@ -217,11 +215,7 @@ RUN echo bar > /test2`))
)
Expect(err).ToNot(HaveOccurred())

Expect(fileHelper.DirectoryIsEmpty(result)).To(BeFalse())
content, err := ioutil.ReadFile(filepath.Join(result, ".virtual"))
Expect(err).ToNot(HaveOccurred())

Expect(string(content)).To(Equal(""))
Expect(fileHelper.DirectoryIsEmpty(result)).To(BeTrue())
})

It("Retrieves uncompressed name", func() {
Expand Down
1 change: 1 addition & 0 deletions pkg/compiler/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ func NewBackend(ctx *types.Context, s string) (CompilerBackend, error) {
type CompilerBackend interface {
BuildImage(backend.Options) error
ExportImage(backend.Options) error
LoadImage(string) error
RemoveImage(backend.Options) error
ImageDefinitionToTar(backend.Options) error

Expand Down
11 changes: 11 additions & 0 deletions pkg/compiler/backend/simpledocker.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,17 @@ func (s *SimpleDocker) CopyImage(src, dst string) error {
return nil
}

func (s *SimpleDocker) LoadImage(path string) error {
s.ctx.Debug(":whale: Loading image:", path)
cmd := exec.Command("docker", "load", "-i", path)
out, err := cmd.CombinedOutput()
if err != nil {
return errors.Wrap(err, "Failed loading image: "+string(out))
}
s.ctx.Success(":whale: Loaded image:", path)
return nil
}

func (s *SimpleDocker) DownloadImage(opts Options) error {
name := opts.ImageName
bus.Manager.Publish(bus.EventImagePrePull, opts)
Expand Down
4 changes: 4 additions & 0 deletions pkg/compiler/backend/simpleimg.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ func NewSimpleImgBackend(ctx *types.Context) *SimpleImg {
return &SimpleImg{ctx: ctx}
}

func (s *SimpleImg) LoadImage(string) error {
return errors.New("Not supported")
}

// TODO: Missing still: labels, and build args expansion
func (s *SimpleImg) BuildImage(opts Options) error {
name := opts.ImageName
Expand Down
Loading

0 comments on commit c7f9708

Please sign in to comment.