329 lines
7.9 KiB
Go
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
|
|
}
|