buildah/internal/source/add.go

134 lines
3.7 KiB
Go
Raw Normal View History

package source
import (
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/containers/image/v5/types"
"github.com/containers/storage/pkg/archive"
specV1 "github.com/opencontainers/image-spec/specs-go/v1"
)
// AddOptions include data to alter certain knobs when adding a source artifact
// to a source image.
type AddOptions struct {
// Annotations for the source artifact.
Annotations []string
}
// annotations parses the specified annotations and transforms them into a map.
// A given annotation can be specified only once.
func (o *AddOptions) annotations() (map[string]string, error) {
annotations := make(map[string]string)
for _, unparsed := range o.Annotations {
key, value, hasValue := strings.Cut(unparsed, "=")
if !hasValue {
return nil, fmt.Errorf("invalid annotation %q (expected format is \"key=value\")", unparsed)
}
if _, exists := annotations[key]; exists {
return nil, fmt.Errorf("annotation %q specified more than once", key)
}
annotations[key] = value
}
return annotations, nil
}
// Add adds the specified source artifact at `artifactPath` to the source image
// at `sourcePath`. Note that the artifact will be added as a gzip-compressed
// tar ball. Add attempts to auto-tar and auto-compress only if necessary.
func Add(ctx context.Context, sourcePath string, artifactPath string, options AddOptions) error {
// Let's first make sure `sourcePath` exists and that we can access it.
if _, err := os.Stat(sourcePath); err != nil {
return err
}
annotations, err := options.annotations()
if err != nil {
return err
}
ociDest, err := openOrCreateSourceImage(ctx, sourcePath)
if err != nil {
return err
}
defer ociDest.Close()
tarStream, err := archive.TarWithOptions(artifactPath, &archive.TarOptions{Compression: archive.Gzip})
if err != nil {
return fmt.Errorf("creating compressed tar stream: %w", err)
}
info := types.BlobInfo{
Size: -1, // "unknown": we'll get that information *after* adding
}
addedBlob, err := ociDest.PutBlob(ctx, tarStream, info, nil, false)
if err != nil {
return fmt.Errorf("adding source artifact: %w", err)
}
// Add the new layers to the source image's manifest.
manifest, oldManifestDigest, _, err := readManifestFromOCIPath(ctx, sourcePath)
if err != nil {
return err
}
manifest.Layers = append(manifest.Layers,
specV1.Descriptor{
MediaType: specV1.MediaTypeImageLayerGzip,
Digest: addedBlob.Digest,
Size: addedBlob.Size,
Annotations: annotations,
},
)
manifestDigest, manifestSize, err := writeManifest(ctx, manifest, ociDest)
if err != nil {
return err
}
// Now, as we've written the updated manifest, we can delete the
// previous one. `types.ImageDestination` doesn't expose a high-level
// API to manage multi-manifest destination, so we need to do it
// manually. Not an issue, since paths are predictable for an OCI
// layout.
if err := removeBlob(oldManifestDigest, sourcePath); err != nil {
return fmt.Errorf("removing old manifest: %w", err)
}
manifestDescriptor := specV1.Descriptor{
MediaType: specV1.MediaTypeImageManifest,
Digest: *manifestDigest,
Size: manifestSize,
}
if err := updateIndexWithNewManifestDescriptor(&manifestDescriptor, sourcePath); err != nil {
return err
}
return nil
}
func updateIndexWithNewManifestDescriptor(manifest *specV1.Descriptor, sourcePath string) error {
index := specV1.Index{}
indexPath := filepath.Join(sourcePath, "index.json")
rawData, err := os.ReadFile(indexPath)
if err != nil {
return err
}
if err := json.Unmarshal(rawData, &index); err != nil {
return err
}
index.Manifests = []specV1.Descriptor{*manifest}
rawData, err = json.Marshal(&index)
if err != nil {
return err
}
return os.WriteFile(indexPath, rawData, 0644)
}