135 lines
		
	
	
		
			3.8 KiB
		
	
	
	
		
			Go
		
	
	
	
		
		
			
		
	
	
			135 lines
		
	
	
		
			3.8 KiB
		
	
	
	
		
			Go
		
	
	
	
|  | package source | ||
|  | 
 | ||
|  | import ( | ||
|  | 	"context" | ||
|  | 	"encoding/json" | ||
|  | 	"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" | ||
|  | 	"github.com/pkg/errors" | ||
|  | ) | ||
|  | 
 | ||
|  | // 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, errors.Errorf("invalid annotation %q (expected format is \"key=value\")", unparsed) | ||
|  | 		} | ||
|  | 		if _, exists := annotations[parsed[0]]; exists { | ||
|  | 			return nil, errors.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 errors.Wrap(err, "error creating compressed tar stream") | ||
|  | 	} | ||
|  | 
 | ||
|  | 	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 errors.Wrap(err, "error adding source artifact") | ||
|  | 	} | ||
|  | 
 | ||
|  | 	// 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 errors.Wrap(err, "error removing old manifest") | ||
|  | 	} | ||
|  | 
 | ||
|  | 	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) | ||
|  | } |