buildr/modules/packaging/ociimg/oci_image.go
Peter e60726ef9e
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
feat: implement new and man for plugin modules
- use extracted shared libraries
2023-08-23 22:06:26 +02:00

329 lines
7.9 KiB
Go

package ociimg
import (
"archive/tar"
"context"
"fmt"
"io"
"log/slog"
"os"
"sync"
"time"
"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/google/go-containerregistry/pkg/v1/empty"
"github.com/google/go-containerregistry/pkg/v1/mutate"
"github.com/google/go-containerregistry/pkg/v1/tarball"
"github.com/google/go-containerregistry/pkg/v1/types"
specsv1 "github.com/opencontainers/image-spec/specs-go/v1"
"code.icb4dc0.de/buildr/buildr/modules"
)
var (
_ modules.Module = (*ContainerImage)(nil)
defaultCreationTime = time.Unix(0, 0)
)
type ContainerImage struct {
Content map[string]string `hcl:"content"`
cache *sync.Map
BaseImage string `hcl:"base_image"`
ImageName string `hcl:"image_name"`
Platform string `hcl:"platform"`
Tags []string `hcl:"tags"`
Entrypoint []string `hcl:"entrypoint,optional"`
Command []string `hcl:"command,optional"`
RegistryCredentials []RegistryAuth `hcl:"registry_auth,block"`
publishers []Publisher
PublishToDaemon bool `hcl:"publish_to_daemon,optional"`
PublishToRegistry bool `hcl:"publish_to_registry,optional"`
}
func (o ContainerImage) Type() string {
return "container_image"
}
func (o ContainerImage) Category() modules.Category {
return modules.CategoryPackage
}
//nolint:funlen,gocyclo // refactor later
func (o ContainerImage) Execute(ctx modules.ExecutionContext) error {
o.cache = new(sync.Map)
logger := ctx.Logger()
if err := o.initPublishers(logger); err != nil {
return err
}
baseRef, base, err := o.getBaseImage(ctx, logger, o.BaseImage)
if err != nil {
return err
}
mt, err := base.MediaType()
if err != nil {
return err
}
// Annotate the base image we pass to the build function with
// annotations indicating the digest (and possibly tag) of the
// base image. This will be inherited by the image produced.
if mt != types.DockerManifestList {
var baseDigest v1.Hash
if baseDigest, err = base.Digest(); err != nil {
return err
}
anns := map[string]string{
specsv1.AnnotationBaseImageDigest: baseDigest.String(),
specsv1.AnnotationBaseImageName: baseRef.Name(),
}
base = mutate.Annotations(base, anns).(imageOrIndex)
}
platform, err := v1.ParsePlatform(o.Platform)
if err != nil {
return err
}
var buildResult imageOrIndex
//nolint:exhaustive // not necessary
switch mt {
case types.OCIImageIndex, types.DockerManifestList:
baseIdx, ok := base.(v1.ImageIndex)
if !ok {
return fmt.Errorf("failed to interpret base as index: %v", base)
}
var im *v1.IndexManifest
if im, err = baseIdx.IndexManifest(); err != nil {
return err
}
var (
img v1.Image
desc v1.Descriptor
)
img, desc, err = imageFromIndex(baseIdx, platform)
if err != nil {
return err
}
img = mutate.Annotations(img, map[string]string{
specsv1.AnnotationBaseImageDigest: desc.Digest.String(),
specsv1.AnnotationBaseImageName: baseRef.Name(),
}).(v1.Image)
img, err = o.buildImage(ctx, img, platform)
if err != nil {
return err
}
adds := mutate.IndexAddendum{
Add: img,
Descriptor: v1.Descriptor{
URLs: desc.URLs,
MediaType: desc.MediaType,
Annotations: desc.Annotations,
Platform: desc.Platform,
},
}
buildResult = mutate.AppendManifests(
mutate.Annotations(
mutate.IndexMediaType(empty.Index, mt),
im.Annotations,
).(v1.ImageIndex),
adds,
)
case types.OCIManifestSchema1, types.DockerManifestSchema2:
baseImage, ok := base.(v1.Image)
if !ok {
return fmt.Errorf("failed to interpret base as image: %v", base)
}
buildResult, err = o.buildImage(ctx, baseImage, platform)
if err != nil {
return err
}
default:
return nil
}
for i := range o.publishers {
if _, err := o.publishers[i].Publish(ctx, buildResult); err != nil {
return err
}
}
return nil
}
func (o *ContainerImage) initPublishers(logger *slog.Logger) error {
if o.PublishToDaemon {
cli, err := client.NewClientWithOpts(
client.WithHostFromEnv(),
client.WithTLSClientConfigFromEnv(),
client.WithVersionFromEnv(),
)
if err != nil {
return err
}
o.publishers = append(o.publishers, newContainerDaemon(o.ImageName, logger, o.Tags, cli))
}
if o.PublishToRegistry {
opts := []registryPublisherOption{
withTags(o.Tags...),
}
if o.RegistryCredentials != nil {
opts = append(opts, withKeyChain(authn.NewMultiKeychain(authn.DefaultKeychain, KeyChain(o.RegistryCredentials))))
}
o.publishers = append(o.publishers, newRegistryPublisher(o.ImageName, logger, opts...))
}
return nil
}
func (o ContainerImage) buildImage(ctx modules.ExecutionContext, base v1.Image, platform *v1.Platform) (v1.Image, error) {
// Layers should be typed to match the underlying image, since some
// registries reject mixed-type layers.
var layerMediaType types.MediaType
mt, err := base.MediaType()
if err != nil {
return nil, err
}
//nolint:exhaustive // not necessary
switch mt {
case types.OCIManifestSchema1:
layerMediaType = types.OCILayer
case types.DockerManifestSchema2:
layerMediaType = types.DockerLayer
}
if err := o.platformMatches(platform); err != nil {
return nil, err
}
contentTemp, err := os.CreateTemp(os.TempDir(), "buildr-oci-image-")
if err != nil {
return nil, err
}
defer func() {
tempFilePath := contentTemp.Name()
_ = contentTemp.Close()
_ = os.Remove(tempFilePath)
}()
if err := o.tarFiles(contentTemp, ctx.WorkingDir()); err != nil {
return nil, err
}
layer, err := tarball.LayerFromOpener(func() (io.ReadCloser, error) {
if _, err = contentTemp.Seek(0, 0); err != nil {
return nil, err
}
return io.NopCloser(contentTemp), nil
}, tarball.WithCompressedCaching, tarball.WithMediaType(layerMediaType))
if err != nil {
return nil, err
}
withContent, err := mutate.Append(base, mutate.Addendum{
Layer: layer,
History: v1.History{
Author: "buildr",
CreatedBy: "oci_image",
Created: v1.Time{Time: defaultCreationTime},
},
})
if err != nil {
return nil, err
}
cfg, err := withContent.ConfigFile()
if err != nil {
return nil, err
}
cfg = cfg.DeepCopy()
cfg.Config.Entrypoint = o.Entrypoint
cfg.Config.Cmd = o.Command
cfg.Author = "buildr"
if cfg.Config.Labels == nil {
cfg.Config.Labels = map[string]string{}
}
return mutate.ConfigFile(withContent, cfg)
}
func (o ContainerImage) tarFiles(outWriter io.Writer, cwd string) error {
tw := tar.NewWriter(outWriter)
defer func() {
_ = tw.Close()
}()
tree, err := buildDirFromContent(cwd, o.Content)
if err != nil {
return err
}
return tree.writeToTar(tw, "/")
}
func (o ContainerImage) platformMatches(platform *v1.Platform) error {
parsed, err := v1.ParsePlatform(o.Platform)
if err != nil {
return err
}
if parsed.Satisfies(*platform) {
return nil
}
return fmt.Errorf("platform %s does not satisfy %s", platform.String(), o.Platform)
}
func (o ContainerImage) getBaseImage(ctx context.Context, logger *slog.Logger, imageRef string) (name.Reference, imageOrIndex, error) {
ref, err := name.ParseReference(imageRef, name.WithDefaultRegistry("docker.io"))
if err != nil {
return nil, nil, fmt.Errorf("parsing base image (%q): %w", imageRef, err)
}
if v, ok := o.cache.Load(ref.String()); ok {
return ref, v.(imageOrIndex), nil
}
result, err := fetch(ctx, ref)
if err != nil {
return ref, result, err
}
if dig, ok := ref.(name.Digest); ok {
logger.Info("Using base", slog.String("base_image", fmt.Sprintf("%s@%s", ref, dig)))
} else {
dig, err := result.Digest()
if err != nil {
return ref, result, err
}
logger.Info("Using base", slog.String("base_image", fmt.Sprintf("%s@%s", ref, dig)))
}
o.cache.Store(ref.String(), result)
return ref, result, nil
}