1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134
|
package source
import (
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/containers/image/v5/types"
"github.com/containers/storage/pkg/archive"
"github.com/containers/storage/pkg/fileutils"
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 := fileutils.Exists(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, 0o644)
}
|