pkg/crate/oci.go (211 lines of code) (raw):
// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you under
// the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package crate
import (
"bytes"
"errors"
"fmt"
"path"
"strings"
"time"
"github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"oras.land/oras-go/pkg/content"
containerv1 "github.com/elastic/harp/api/gen/go/harp/container/v1"
"github.com/elastic/harp/build/version"
"github.com/elastic/harp/pkg/container"
"github.com/elastic/harp/pkg/crate/schema"
schemav1 "github.com/elastic/harp/pkg/crate/schema/v1"
"github.com/elastic/harp/pkg/sdk/types"
)
// StoreSetter is the interface used to mock the image store.
type StoreSetter interface {
Set(ocispec.Descriptor, []byte)
}
// StoreGetter is the interface used to mock the image store.
type StoreGetter interface {
GetByName(name string) (ocispec.Descriptor, []byte, bool)
}
// PrepareImage is used to assemble the OCI image according to given specification.
func PrepareImage(store StoreSetter, image *Image) ([]byte, *ocispec.Descriptor, error) {
// Check arguments
if types.IsNil(store) {
return nil, nil, errors.New("unable to prepare an image with nil storage")
}
if image == nil {
return nil, nil, errors.New("given image is nil")
}
// Add config
config, err := addConfig(store, image)
if err != nil {
return nil, nil, fmt.Errorf("unable to add config layer: %w", err)
}
layers := []ocispec.Descriptor{}
// Add all containers
for _, c := range image.Containers {
// Create a layer for each sealed containers
sealedContainerLayer, errLayer := addSealedContainer(store, c)
if errLayer != nil {
return nil, nil, fmt.Errorf("unable to add container layer: %w", errLayer)
}
// Add to manifest
layers = append(layers, *sealedContainerLayer)
}
// Add all template archive
for _, ta := range image.TemplateArchives {
// Create a layer for each template archive
templateLayer, errLayer := addTemplateArchive(store, ta)
if errLayer != nil {
return nil, nil, fmt.Errorf("unable to add template layer: %w", errLayer)
}
// Add to manifest
layers = append(layers, *templateLayer)
}
// Generate manifest.
manifestBytes, manifest, errManifest := content.GenerateManifest(config, map[string]string{
ocispec.AnnotationCreated: time.Now().UTC().Format(time.RFC3339),
ocispec.AnnotationVersion: version.Version,
}, layers...)
if errManifest != nil {
return nil, nil, fmt.Errorf("unable to generate manifest: %w", errManifest)
}
// No error
return manifestBytes, &manifest, nil
}
// ExtractImage extracts from the given OCI storage the crate image descriptor.
func ExtractImage(store StoreGetter) (*Image, error) {
// Check arguments
if store == nil {
return nil, errors.New("can't extract crate from a nil store")
}
// Retrieve config
cfg, err := getConfig(store)
if err != nil {
return nil, fmt.Errorf("unable to retrieve config from crate: %w", err)
}
var containers []*SealedContainer
// Retrieve container
for _, layerName := range cfg.Containers() {
c, err := getSealedContainer(store, layerName)
if err != nil {
return nil, fmt.Errorf("unable to retrieve container from crate: %w", err)
}
// Add to containers
containers = append(containers, &SealedContainer{
Name: strings.TrimPrefix(layerName, "containers/"),
Container: c,
})
}
// Retrieve archives
var archives []*TemplateArchive
for _, layerName := range cfg.Templates() {
c, err := getTemplateArchive(store, layerName)
if err != nil {
return nil, fmt.Errorf("unable to retrieve archive from crate: %w", err)
}
// Add to containers
archives = append(archives, &TemplateArchive{
Name: strings.TrimPrefix(layerName, "templates/"),
Archive: c,
})
}
// No error
return &Image{
Config: cfg,
Containers: containers,
TemplateArchives: archives,
}, nil
}
// -----------------------------------------------------------------------------
func getConfig(store StoreGetter) (schema.Config, error) {
// Check arguments
if store == nil {
return nil, errors.New("can't extract crate from a nil store")
}
// Extract config from image
configDesc, configRaw, hasConfigLayer := store.GetByName("_config.json")
if !hasConfigLayer {
return nil, errors.New("unable to lookup config layer from crate")
}
if configDesc.MediaType != harpConfigMediaType {
return nil, errors.New("invalid config layer media type value")
}
// Decode config
cfg, err := schemav1.ParseConfig(configRaw)
if err != nil {
return nil, fmt.Errorf("unable to decode config object: %w", err)
}
// No error
return cfg, nil
}
// AddConfig register the OCI configuration layer to retrieve information about
// the image.
func addConfig(store StoreSetter, image *Image) (*ocispec.Descriptor, error) {
// Check arguments
if types.IsNil(store) {
return nil, errors.New("unable to register sealed container with nil storage")
}
if image == nil {
return nil, errors.New("given image is nil")
}
// Render config as JSON.
configBytes, err := schema.RenderConfig(image.Config)
if err != nil {
return nil, err
}
// Prepare layer
configDesc := ocispec.Descriptor{
MediaType: harpConfigMediaType,
Digest: digest.FromBytes(configBytes),
Size: int64(len(configBytes)),
Annotations: map[string]string{
ocispec.AnnotationTitle: "_config.json",
},
}
// Assign to image store
store.Set(configDesc, configBytes)
// No error
return &configDesc, nil
}
func getSealedContainer(store StoreGetter, layerName string) (*containerv1.Container, error) {
// Check arguments
if store == nil {
return nil, errors.New("can't extract container from a nil store")
}
// Extract config from image
containerDesc, containerRaw, hasContainerLayer := store.GetByName(layerName)
if !hasContainerLayer {
return nil, fmt.Errorf("unable to lookup conainer '%s' layer from crate", layerName)
}
if containerDesc.MediaType != harpContainerLayerMediaType {
return nil, fmt.Errorf("invalid container layer media type value for '%s'", layerName)
}
// Validate as container
c, err := container.Load(bytes.NewReader(containerRaw))
if err != nil {
return nil, fmt.Errorf("unable to decode csealed container from layer '%s': %w", layerName, err)
}
// Must be sealed.
if !container.IsSealed(c) {
return nil, fmt.Errorf("container layer '%s' has an unsealed container", layerName)
}
// No error
return c, nil
}
// AddSealedContainer registers a new layer to the current store for the given selaed container.
func addSealedContainer(store StoreSetter, c *SealedContainer) (*ocispec.Descriptor, error) {
// Check arguments
if types.IsNil(store) {
return nil, errors.New("unable to register sealed container with nil storage")
}
if c == nil {
return nil, errors.New("given container is nil")
}
if !container.IsSealed(c.Container) {
return nil, errors.New("the given container must be sealed")
}
// Dump the container
var payload bytes.Buffer
if err := container.Dump(&payload, c.Container); err != nil {
return nil, fmt.Errorf("unable to dump container: %w", err)
}
// Get layer content
body := payload.Bytes()
// Prepare a layer
containerDesc := ocispec.Descriptor{
MediaType: harpContainerLayerMediaType,
Digest: digest.FromBytes(body),
Size: int64(len(body)),
Annotations: map[string]string{
ocispec.AnnotationTitle: path.Join("containers", path.Clean(c.Name)),
},
}
// Assign the store
store.Set(containerDesc, body)
// No error
return &containerDesc, nil
}
func getTemplateArchive(store StoreGetter, layerName string) ([]byte, error) {
// Check arguments
if store == nil {
return nil, errors.New("can't extract archive from a nil store")
}
// Extract archive from image
archiveDesc, archiveRaw, hasArchiveLayer := store.GetByName(layerName)
if !hasArchiveLayer {
return nil, fmt.Errorf("unable to lookup archive '%s' layer from crate", layerName)
}
if archiveDesc.MediaType != harpDataLayerMediaType {
return nil, fmt.Errorf("invalid archive layer media type value for '%s'", layerName)
}
// No error
return archiveRaw, nil
}
// AddTemplateArchive registers a new layer to the current store for the given archive.
func addTemplateArchive(store StoreSetter, ta *TemplateArchive) (*ocispec.Descriptor, error) {
// Check arguments
if types.IsNil(store) {
return nil, errors.New("unable to register sealed container with nil storage")
}
if ta == nil {
return nil, errors.New("given template archive is nil")
}
// Prepare a layer
containerDesc := ocispec.Descriptor{
MediaType: harpDataLayerMediaType,
Digest: digest.FromBytes(ta.Archive),
Size: int64(len(ta.Archive)),
Annotations: map[string]string{
ocispec.AnnotationTitle: path.Join("templates", path.Clean(ta.Name)),
},
}
// Assign the store
store.Set(containerDesc, ta.Archive)
// No error
return &containerDesc, nil
}