@ -24,13 +24,13 @@ import (
"io/ioutil"
"os"
"path/filepath"
"sort"
"strings"
"time"
"github.com/containerd/containerd/content"
"github.com/containerd/containerd/errdefs"
orascontent "github.com/deislabs/oras/pkg/content"
units "github.com/docker/go-units"
checksum "github.com/opencontainers/go-digest "
digest "github.com/opencontainers/go-digest "
specs "github.com/opencontainers/image-spec/specs-go "
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
@ -39,451 +39,324 @@ import (
"helm.sh/helm/pkg/chartutil"
)
var (
tableHeaders = [ ] string { "name" , "version" , "digest" , "size" , "created" }
const (
// CacheRootDir is the root directory for a cache
CacheRootDir = "cache"
)
type (
filesystemCache struct {
out io . Writer
rootDir string
store * orascontent . Memorystore
// Cache handles local/in-memory storage of Helm charts, compliant with OCI Layout
Cache struct {
debug bool
out io . Writer
rootDir string
ociStore * orascontent . OCIStore
memoryStore * orascontent . Memorystore
}
// CacheRefSummary contains as much info as available describing a chart reference in cache
// Note: fields here are sorted by the order in which they are set in FetchReference method
CacheRefSummary struct {
Name string
Repo string
Tag string
Exists bool
Manifest * ocispec . Descriptor
Config * ocispec . Descriptor
ContentLayer * ocispec . Descriptor
Size int64
Digest digest . Digest
CreatedAt time . Time
Chart * chart . Chart
}
)
func ( cache * filesystemCache ) LayersToChart ( layers [ ] ocispec . Descriptor ) ( * chart . Chart , error ) {
metaLayer , contentLayer , err := extractLayers ( layers )
if err != nil {
return nil , err
// NewCache returns a new OCI Layout-compliant cache with config
func NewCache ( opts ... CacheOption ) ( * Cache , error ) {
cache := & Cache {
out : ioutil . Discard ,
}
name , version , err := extractChartNameVersionFromLayer ( contentLayer )
if err != nil {
return nil , err
for _ , opt := range opts {
opt ( cache )
}
// Obtain raw chart meta content (json)
_ , metaJSONRaw , ok := cache . store . Get ( metaLayer )
if ! ok {
return nil , errors . New ( "error retrieving meta layer" )
// validate
if cache . rootDir == "" {
return nil , errors . New ( "must set cache root dir on initialization" )
}
return cache , nil
}
// Construct chart metadata object
metadata := chart . Metadata { }
err = json . Unmarshal ( metaJSONRaw , & metadata )
if err != nil {
// FetchReference retrieves a chart ref from cache
func ( cache * Cache ) FetchReference ( ref * Reference ) ( * CacheRefSummary , error ) {
if err := cache . init ( ) ; err != nil {
return nil , err
}
metadata . APIVersion = chart . APIVersionV1
metadata . Name = name
metadata . Version = version
// Obtain raw chart content
_ , contentRaw , ok := cache . store . Get ( contentLayer )
if ! ok {
return nil , errors . New ( "error retrieving meta layer" )
}
// Construct chart object and attach metadata
ch , err := loader . LoadArchive ( bytes . NewBuffer ( contentRaw ) )
if err != nil {
return nil , err
r := CacheRefSummary {
Name : ref . FullName ( ) ,
Repo : ref . Repo ,
Tag : ref . Tag ,
}
for _ , desc := range cache . ociStore . ListReferences ( ) {
if desc . Annotations [ ocispec . AnnotationRefName ] == r . Name {
r . Exists = true
manifestBytes , err := cache . fetchBlob ( & desc )
if err != nil {
return & r , err
}
var manifest ocispec . Manifest
err = json . Unmarshal ( manifestBytes , & manifest )
if err != nil {
return & r , err
}
r . Manifest = & desc
r . Config = & manifest . Config
numLayers := len ( manifest . Layers )
if numLayers != 1 {
return & r , errors . New (
fmt . Sprintf ( "manifest does not contain exactly 1 layer (total: %d)" , numLayers ) )
}
var contentLayer * ocispec . Descriptor
for _ , layer := range manifest . Layers {
switch layer . MediaType {
case HelmChartContentLayerMediaType :
contentLayer = & layer
}
}
if contentLayer . Size == 0 {
return & r , errors . New (
fmt . Sprintf ( "manifest does not contain a layer with mediatype %s" , HelmChartContentLayerMediaType ) )
}
r . ContentLayer = contentLayer
info , err := cache . ociStore . Info ( ctx ( cache . out , cache . debug ) , contentLayer . Digest )
if err != nil {
return & r , err
}
r . Size = info . Size
r . Digest = info . Digest
r . CreatedAt = info . CreatedAt
contentBytes , err := cache . fetchBlob ( contentLayer )
if err != nil {
return & r , err
}
ch , err := loader . LoadArchive ( bytes . NewBuffer ( contentBytes ) )
if err != nil {
return & r , err
}
r . Chart = ch
}
}
ch . Metadata = & metadata
return ch , nil
return & r , nil
}
func ( cache * filesystemCache ) ChartToLayers ( ch * chart . Chart ) ( [ ] ocispec . Descriptor , error ) {
// extract/separate the name and version from other metadata
if err := ch . Validate ( ) ; err != nil {
// StoreReference stores a chart ref in cache
func ( cache * Cache ) StoreReference ( ref * Reference , ch * chart . Chart ) ( * CacheRefSummary , error ) {
if err := cache . init ( ) ; err != nil {
return nil , err
}
name := ch . Metadata . Name
version := ch . Metadata . Version
// Create meta layer, clear name and version from Chart.yaml and convert to json
ch . Metadata . Name = ""
ch . Metadata . Version = ""
metaJSONRaw , err := json . Marshal ( ch . Metadata )
if err != nil {
return nil , err
}
metaLayer := cache . store . Add ( HelmChartMetaFileName , HelmChartMetaLayerMediaType , metaJSONRaw )
// Create content layer
// TODO: something better than this hack. Currently needed for chartutil.Save()
// If metadata does not contain Name or Version, an error is returned
// such as "no chart name specified (Chart.yaml)"
ch . Metadata = & chart . Metadata {
APIVersion : chart . APIVersionV1 ,
Name : "-" ,
Version : "0.1.0" ,
}
destDir := mkdir ( filepath . Join ( cache . rootDir , "blobs" , ".build" ) )
tmpFile , err := chartutil . Save ( ch , destDir )
defer os . Remove ( tmpFile )
if err != nil {
return nil , errors . Wrap ( err , "failed to save" )
r := CacheRefSummary {
Name : ref . FullName ( ) ,
Repo : ref . Repo ,
Tag : ref . Tag ,
Chart : ch ,
}
contentRaw , err := ioutil . ReadFile ( tmpFile )
existing , _ := cache . FetchReference ( ref )
r . Exists = existing . Exists
config , _ , err := cache . saveChartConfig ( ch )
if err != nil {
return nil , err
return & r , err
}
contentLayer := cache . store . Add ( HelmChartContentFileName , HelmChartContentLayerMediaType , contentRaw )
// Set annotations
contentLayer . Annotations [ HelmChartNameAnnotation ] = name
contentLayer . Annotations [ HelmChartVersionAnnotation ] = version
layers := [ ] ocispec . Descriptor { metaLayer , contentLayer }
return layers , nil
}
func ( cache * filesystemCache ) LoadReference ( ref * Reference ) ( [ ] ocispec . Descriptor , error ) {
tagDir := filepath . Join ( cache . rootDir , "refs" , escape ( ref . Repo ) , "tags" , ref . Tag )
// add meta layer
metaJSONRaw , err := getSymlinkDestContent ( filepath . Join ( tagDir , "meta" ) )
r . Config = config
contentLayer , _ , err := cache . saveChartContentLayer ( ch )
if err != nil {
return nil , err
return & r , err
}
metaLayer := cache . store . Add ( HelmChartMetaFileName , HelmChartMetaLayerMediaType , metaJSONRaw )
// add content layer
contentRaw , err := getSymlinkDestContent ( filepath . Join ( tagDir , "content" ) )
r . ContentLayer = contentLayer
info , err := cache . ociStore . Info ( ctx ( cache . out , cache . debug ) , contentLayer . Digest )
if err != nil {
return nil , err
return & r , err
}
contentLayer := cache . store . Add ( HelmChartContentFileName , HelmChartContentLayerMediaType , contentRaw )
// set annotations on content layer (chart name and version)
err = setLayerAnnotationsFromChartLink ( contentLayer , filepath . Join ( tagDir , "chart" ) )
r . Size = info . Size
r . Digest = info . Digest
r . CreatedAt = info . CreatedAt
manifest , _ , err := cache . saveChartManifest ( config , contentLayer )
if err != nil {
return nil , err
return & r , err
}
printChartSummary ( cache . out , metaLayer , contentLayer )
layers := [ ] ocispec . Descriptor { metaLayer , contentLayer }
return layers , nil
r . Manifest = manifest
return & r , nil
}
func ( cache * filesystemCache ) StoreReference ( ref * Reference , layers [ ] ocispec . Descriptor ) ( bool , error ) {
tagDir := mkdir ( filepath . Join ( cache . rootDir , "refs" , escape ( ref . Repo ) , "tags" , ref . Tag ) )
// Retrieve just the meta and content layers
metaLayer , contentLayer , err := extractLayers ( layers )
if err != nil {
return false , err
}
// Extract chart name and version
name , version , err := extractChartNameVersionFromLayer ( contentLayer )
if err != nil {
return false , err
// DeleteReference deletes a chart ref from cache
// TODO: garbage collection, only manifest removed
func ( cache * Cache ) DeleteReference ( ref * Reference ) ( * CacheRefSummary , error ) {
if err := cache . init ( ) ; err != nil {
return nil , err
}
// Create chart file
chartPath , err := createChartFile ( filepath . Join ( cache . rootDir , "charts" ) , name , version )
if err != nil {
return false , err
r , err := cache . FetchReference ( ref )
if err != nil || ! r . Exists {
return r , err
}
cache . ociStore . DeleteReference ( r . Name )
err = cache . ociStore . SaveIndex ( )
return r , err
}
// Create chart symlink
err = createSymlink ( chartPath , filepath . Join ( tagDir , "chart" ) )
if err != nil {
return false , err
// ListReferences lists all chart refs in a cache
func ( cache * Cache ) ListReferences ( ) ( [ ] * CacheRefSummary , error ) {
if err := cache . init ( ) ; err != nil {
return nil , err
}
// Save meta blob
metaExists , metaPath := digestPath ( filepath . Join ( cache . rootDir , "blobs" ) , metaLayer . Digest )
if ! metaExists {
fmt . Fprintf ( cache . out , "%s: Saving meta (%s)\n" ,
shortDigest ( metaLayer . Digest . Hex ( ) ) , byteCountBinary ( metaLayer . Size ) )
_ , metaJSONRaw , ok := cache . store . Get ( metaLayer )
if ! ok {
return false , errors . New ( "error retrieving meta layer" )
var rr [ ] * CacheRefSummary
for _ , desc := range cache . ociStore . ListReferences ( ) {
name := desc . Annotations [ ocispec . AnnotationRefName ]
if name == "" {
if cache . debug {
fmt . Fprintf ( cache . out , "warning: found manifest without name: %s" , desc . Digest . Hex ( ) )
}
continue
}
err = writeFile ( metaPath , metaJSONRaw )
ref, err := ParseReference ( name )
if err != nil {
return false , err
return rr , err
}
}
// Create meta symlink
err = createSymlink ( metaPath , filepath . Join ( tagDir , "meta" ) )
if err != nil {
return false , err
}
// Save content blob
contentExists , contentPath := digestPath ( filepath . Join ( cache . rootDir , "blobs" ) , contentLayer . Digest )
if ! contentExists {
fmt . Fprintf ( cache . out , "%s: Saving content (%s)\n" ,
shortDigest ( contentLayer . Digest . Hex ( ) ) , byteCountBinary ( contentLayer . Size ) )
_ , contentRaw , ok := cache . store . Get ( contentLayer )
if ! ok {
return false , errors . New ( "error retrieving content layer" )
}
err = writeFile ( contentPath , contentRaw )
r , err := cache . FetchReference ( ref )
if err != nil {
return false , err
return rr , err
}
rr = append ( rr , r )
}
// Create content symlink
err = createSymlink ( contentPath , filepath . Join ( tagDir , "content" ) )
if err != nil {
return false , err
}
printChartSummary ( cache . out , metaLayer , contentLayer )
return metaExists && contentExists , nil
return rr , nil
}
func ( cache * filesystemCache ) DeleteReference ( ref * Reference ) error {
tagDir := filepath . Join ( cache . rootDir , "refs" , escape ( ref . Repo ) , "tags" , ref . Tag )
if _, err := os . Stat ( tagDir ) ; os . IsNotExist ( err ) {
return err ors. New ( "ref not found" )
// AddManifest provides a manifest to the cache index.json
func ( cache * Cache ) AddManifest ( ref * Reference , manifest * ocispec . Descriptor ) error {
if err := cache . init ( ) ; err != nil {
return err
}
return os . RemoveAll ( tagDir )
}
func ( cache * filesystemCache ) TableRows ( ) ( [ ] [ ] interface { } , error ) {
return getRefsSorted ( filepath . Join ( cache . rootDir , "refs" ) )
cache . ociStore . AddReference ( ref . FullName ( ) , * manifest )
err := cache . ociStore . SaveIndex ( )
return err
}
// escape sanitizes a registry URL to remove characters such as ":"
// which are illegal on windows
func escape ( s string ) string {
return strings . ReplaceAll ( s , ":" , "_" )
// Provider provides a valid containerd Provider
func ( cache * Cache ) Provider ( ) content . Provider {
return content . Provider ( cache . ociStore )
}
// escape reverses escape
func unescape ( s string ) string {
return strings. ReplaceAll ( s , "_" , ":" )
// Ingester provides a valid containerd Ingester
func ( cache * Cache ) Ingester ( ) content . Ingester {
return content. Ingester ( cache . ociStore )
}
// printChartSummary prints details about a chart layers
func printChartSummary ( out io . Writer , metaLayer ocispec . Descriptor , contentLayer ocispec . Descriptor ) {
fmt . Fprintf ( out , "Name: %s\n" , contentLayer . Annotations [ HelmChartNameAnnotation ] )
fmt . Fprintf ( out , "Version: %s\n" , contentLayer . Annotations [ HelmChartVersionAnnotation ] )
fmt . Fprintf ( out , "Meta: %s\n" , metaLayer . Digest )
fmt . Fprintf ( out , "Content: %s\n" , contentLayer . Digest )
// ProvideIngester provides a valid oras ProvideIngester
func ( cache * Cache ) ProvideIngester ( ) orascontent . ProvideIngester {
return orascontent . ProvideIngester ( cache . ociStore )
}
// fileExists determines if a file exists
func fileExists ( path string ) bool {
if _ , err := os . Stat ( path ) ; os . IsNotExist ( err ) {
return false
// init creates files needed necessary for OCI layout store
func ( cache * Cache ) init ( ) error {
if cache . ociStore == nil {
ociStore , err := orascontent . NewOCIStore ( cache . rootDir )
if err != nil {
return err
}
cache . ociStore = ociStore
cache . memoryStore = orascontent . NewMemoryStore ( )
}
return true
}
// mkdir will create a directory (no error check) and return the path
func mkdir ( dir string ) string {
os . MkdirAll ( dir , 0755 )
return dir
}
// createSymlink creates a symbolic link, deleting existing one if exists
func createSymlink ( src string , dest string ) error {
os . Remove ( dest )
err := os . Symlink ( src , dest )
return err
return nil
}
// getSymlinkDestContent returns the file contents of a symlink's destination
func getSymlinkDestContent ( linkPath string ) ( [ ] byte , error ) {
src, err := os . Readlink ( linkPath )
// saveChartConfig stores the Chart.yaml as json blob and returns a descriptor
func ( cache * Cache ) saveChartConfig ( ch * chart . Chart ) ( * ocispec . Descriptor , bool , error ) {
configBytes , err := json . Marshal ( ch . Metadata )
if err != nil {
return nil , err
return nil , false , err
}
return ioutil . ReadFile ( src )
configExists , err := cache . storeBlob ( configBytes )
if err != nil {
return nil , configExists , err
}
descriptor := cache . memoryStore . Add ( "" , HelmChartConfigMediaType , configBytes )
return & descriptor , configExists , nil
}
// setLayerAnnotationsFromChartLink will set chart name/version annotations on a layer
// based on the path of the chart link destination
func setLayerAnnotationsFromChartLink ( layer ocispec . Descriptor , chartLinkPath string ) error {
src , err := os . Readlink ( chartLinkPath )
// saveChartContentLayer stores the chart as tarball blob and returns a descriptor
func ( cache * Cache ) saveChartContentLayer ( ch * chart . Chart ) ( * ocispec . Descriptor , bool , error ) {
destDir := filepath . Join ( cache . rootDir , ".build" )
os . MkdirAll ( destDir , 0755 )
tmpFile , err := chartutil . Save ( ch , destDir )
defer os . Remove ( tmpFile )
if err != nil {
return err
return nil , false , err ors. Wrap ( err , "failed to save" )
}
// example path: /some/path/charts/mychart/versions/1.2.0
chartName := filepath . Base ( filepath . Dir ( filepath . Dir ( src ) ) )
chartVersion := filepath . Base ( src )
layer . Annotations [ HelmChartNameAnnotation ] = chartName
layer . Annotations [ HelmChartVersionAnnotation ] = chartVersion
return nil
contentBytes , err := ioutil . ReadFile ( tmpFile )
if err != nil {
return nil , false , err
}
contentExists , err := cache . storeBlob ( contentBytes )
if err != nil {
return nil , contentExists , err
}
descriptor := cache . memoryStore . Add ( "" , HelmChartContentLayerMediaType , contentBytes )
return & descriptor , contentExists , nil
}
// extractLayers obtains the meta and content layers from a list of layers
func extractLayers ( layers [ ] ocispec . Descriptor ) ( ocispec . Descriptor , ocispec . Descriptor , error ) {
var metaLayer , contentLayer ocispec . Descriptor
if len ( layers ) != 2 {
return metaLayer , contentLayer , errors . New ( "manifest does not contain exactly 2 layers" )
// saveChartManifest stores the chart manifest as json blob and returns a descriptor
func ( cache * Cache ) saveChartManifest ( config * ocispec . Descriptor , contentLayer * ocispec . Descriptor ) ( * ocispec . Descriptor , bool , error ) {
manifest := ocispec . Manifest {
Versioned : specs . Versioned { SchemaVersion : 2 } ,
Config : * config ,
Layers : [ ] ocispec . Descriptor { * contentLayer } ,
}
for _ , layer := range layers {
switch layer . MediaType {
case HelmChartMetaLayerMediaType :
metaLayer = layer
case HelmChartContentLayerMediaType :
contentLayer = layer
}
manifestBytes , err := json . Marshal ( manifest )
if err != nil {
return nil , false , err
}
if metaLayer. Size == 0 {
return metaLayer , contentLayer , errors . New ( "manifest does not contain a Helm chart meta layer" )
manifestExists , err := cache . storeBlob ( manifestBytes )
if err != nil {
return nil , manifestExists , err
}
if contentLayer . Size == 0 {
return metaLayer , contentLayer , errors . New ( "manifest does not contain a Helm chart content layer" )
descriptor := ocispec . Descriptor {
MediaType : ocispec . MediaTypeImageManifest ,
Digest : digest . FromBytes ( manifestBytes ) ,
Size : int64 ( len ( manifestBytes ) ) ,
}
return metaLayer , contentLayer , nil
return & descriptor , manifestExists , nil
}
// extractChartNameVersionFromLayer retrieves the chart name and version from layer annotations
func extractChartNameVersionFromLayer ( layer ocispec . Descriptor ) ( string , string , error ) {
name , ok := layer . Annotations [ HelmChartNameAnnotation ]
if ! ok {
return "" , "" , errors . New ( "could not find chart name in annotations" )
// storeBlob stores a blob on filesystem
func ( cache * Cache ) storeBlob ( blobBytes [ ] byte ) ( bool , error ) {
var exists bool
writer , err := cache . ociStore . Store . Writer ( ctx ( cache . out , cache . debug ) ,
content . WithRef ( digest . FromBytes ( blobBytes ) . Hex ( ) ) )
if err != nil {
return exists , err
}
version , ok := layer . Annotations [ HelmChartVersionAnnotation ]
if ! ok {
return "" , "" , errors . New ( "could not find chart version in annotations" )
_, err = writer . Write ( blobBytes )
if err != nil {
return exists , err
}
return name , version , nil
}
// createChartFile creates a file under "<chartsdir>" dir which is linked to by ref
func createChartFile ( chartsRootDir string , name string , version string ) ( string , error ) {
chartPathDir := filepath . Join ( chartsRootDir , name , "versions" )
chartPath := filepath . Join ( chartPathDir , version )
if _ , err := os . Stat ( chartPath ) ; err != nil && os . IsNotExist ( err ) {
os . MkdirAll ( chartPathDir , 0755 )
err := ioutil . WriteFile ( chartPath , [ ] byte ( "-" ) , 0644 )
if err != nil {
return "" , err
err = writer . Commit ( ctx ( cache . out , cache . debug ) , 0 , writer . Digest ( ) )
if err != nil {
if ! errdefs . IsAlreadyExists ( err ) {
return exists , err
}
exists = true
}
return chartPath , nil
}
// digestPath returns the path to addressable content, and whether the file exists
func digestPath ( rootDir string , digest checksum . Digest ) ( bool , string ) {
path := filepath . Join ( rootDir , "sha256" , digest . Hex ( ) )
exists := fileExists ( path )
return exists , path
}
// writeFile creates a path, ensuring parent directory
func writeFile ( path string , c [ ] byte ) error {
os . MkdirAll ( filepath . Dir ( path ) , 0755 )
return ioutil . WriteFile ( path , c , 0644 )
}
// byteCountBinary produces a human-readable file size
func byteCountBinary ( b int64 ) string {
const unit = 1024
if b < unit {
return fmt . Sprintf ( "%d B" , b )
}
div , exp := int64 ( unit ) , 0
for n := b / unit ; n >= unit ; n /= unit {
div *= unit
exp ++
}
return fmt . Sprintf ( "%.1f %ciB" , float64 ( b ) / float64 ( div ) , "KMGTPE" [ exp ] )
err = writer . Close ( )
return exists , err
}
// shortDigest returns first 7 characters of a sha256 digest
func shortDigest ( digest string ) string {
if len ( digest ) == 64 {
return digest [ : 7 ]
}
return digest
}
// getRefsSorted returns a map of all refs stored in a refsRootDir
func getRefsSorted ( refsRootDir string ) ( [ ] [ ] interface { } , error ) {
refsMap := map [ string ] map [ string ] string { }
// Walk the storage dir, check for symlinks under "refs" dir pointing to valid files in "blobs/" and "charts/"
err := filepath . Walk ( refsRootDir , func ( path string , fileInfo os . FileInfo , fileError error ) error {
// Check if this file is a symlink
linkPath , err := os . Readlink ( path )
if err == nil {
destFileInfo , err := os . Stat ( linkPath )
if err == nil {
tagDir := filepath . Dir ( path )
// Determine the ref
repo := unescape ( strings . TrimLeft (
strings . TrimPrefix ( filepath . Dir ( filepath . Dir ( tagDir ) ) , refsRootDir ) , "/\\" ) )
tag := filepath . Base ( tagDir )
ref := fmt . Sprintf ( "%s:%s" , repo , tag )
// Init hashmap entry if does not exist
if _ , ok := refsMap [ ref ] ; ! ok {
refsMap [ ref ] = map [ string ] string { }
}
// Add data to entry based on file name (symlink name)
base := filepath . Base ( path )
switch base {
case "chart" :
refsMap [ ref ] [ "name" ] = filepath . Base ( filepath . Dir ( filepath . Dir ( linkPath ) ) )
refsMap [ ref ] [ "version" ] = destFileInfo . Name ( )
case "content" :
// Make sure the filename looks like a sha256 digest (64 chars)
digest := destFileInfo . Name ( )
if len ( digest ) == 64 {
refsMap [ ref ] [ "digest" ] = shortDigest ( digest )
refsMap [ ref ] [ "size" ] = byteCountBinary ( destFileInfo . Size ( ) )
refsMap [ ref ] [ "created" ] = units . HumanDuration ( time . Now ( ) . UTC ( ) . Sub ( destFileInfo . ModTime ( ) ) )
}
}
}
}
return nil
} )
// Filter out any refs that are incomplete (do not have all required fields)
for k , ref := range refsMap {
allKeysFound := true
for _ , v := range tableHeaders {
if _ , ok := ref [ v ] ; ! ok {
allKeysFound = false
break
}
}
if ! allKeysFound {
delete ( refsMap , k )
}
// fetchBlob retrieves a blob from filesystem
func ( cache * Cache ) fetchBlob ( desc * ocispec . Descriptor ) ( [ ] byte , error ) {
reader , err := cache . ociStore . ReaderAt ( ctx ( cache . out , cache . debug ) , * desc )
if err != nil {
return nil , err
}
// Sort and convert to format expected by uitable
refs := make ( [ ] [ ] interface { } , len ( refsMap ) )
keys := make ( [ ] string , 0 , len ( refsMap ) )
for key := range refsMap {
keys = append ( keys , key )
}
sort . Strings ( keys )
for i , key := range keys {
refs [ i ] = make ( [ ] interface { } , len ( tableHeaders ) + 1 )
refs [ i ] [ 0 ] = key
ref := refsMap [ key ]
for j , k := range tableHeaders {
refs [ i ] [ j + 1 ] = ref [ k ]
}
bytes := make ( [ ] byte , desc . Size )
_ , err = reader . ReadAt ( bytes , 0 )
if err != nil {
return nil , err
}
return refs , err
return bytes , nil
}