135 lines
3.8 KiB
Go
135 lines
3.8 KiB
Go
package source
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"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 {
|
|
parsed := strings.SplitN(unparsed, "=", 2)
|
|
if len(parsed) != 2 {
|
|
return nil, fmt.Errorf("invalid annotation %q (expected format is \"key=value\")", unparsed)
|
|
}
|
|
if _, exists := annotations[parsed[0]]; exists {
|
|
return nil, fmt.Errorf("annotation %q specified more than once", parsed[0])
|
|
}
|
|
annotations[parsed[0]] = parsed[1]
|
|
}
|
|
|
|
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 := ioutil.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 ioutil.WriteFile(indexPath, rawData, 0644)
|
|
}
|