326 lines
6.9 KiB
Go
326 lines
6.9 KiB
Go
package docker
|
|
|
|
import "crypto/sha256"
|
|
import "encoding/hex"
|
|
import "encoding/json"
|
|
import "errors"
|
|
import "io"
|
|
import "os"
|
|
import "path/filepath"
|
|
import "strings"
|
|
|
|
var ErrNotFound error = errors.New("not found")
|
|
|
|
type ociLayout struct {
|
|
ImageLayoutVersion string `json:"imageLayoutVersion"`
|
|
}
|
|
|
|
type ociIndex struct {
|
|
SchemaVersion int `json:"schemaVersion"`
|
|
MediaType string `json:"mediaType,omitempty"`
|
|
Manifests []ociDescriptor `json:"manifests"`
|
|
}
|
|
|
|
type ociDescriptor struct {
|
|
MediaType string `json:"mediaType"`
|
|
Digest string `json:"digest"`
|
|
Size int64 `json:"size"`
|
|
Annotations map[string]string `json:"annotations,omitempty"`
|
|
}
|
|
|
|
const ociIndexMediaType string = "application/vnd.oci.image.index.v1+json"
|
|
const ociLayoutVersion string = "1.0.0"
|
|
const ociTagAnnotation string = "org.opencontainers.image.ref.name"
|
|
|
|
func EnsureLayout(repoPath string) error {
|
|
var err error
|
|
var layoutPath string
|
|
var indexPath string
|
|
var blobsDir string
|
|
err = os.MkdirAll(repoPath, 0o755)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
blobsDir = filepath.Join(repoPath, "blobs", "sha256")
|
|
err = os.MkdirAll(blobsDir, 0o755)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
layoutPath = filepath.Join(repoPath, "oci-layout")
|
|
_, err = os.Stat(layoutPath)
|
|
if err != nil {
|
|
if !os.IsNotExist(err) {
|
|
return err
|
|
}
|
|
err = writeJSONFile(layoutPath, ociLayout{ImageLayoutVersion: ociLayoutVersion})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
indexPath = filepath.Join(repoPath, "index.json")
|
|
_, err = os.Stat(indexPath)
|
|
if err != nil {
|
|
if !os.IsNotExist(err) {
|
|
return err
|
|
}
|
|
err = writeJSONFile(indexPath, ociIndex{SchemaVersion: 2, MediaType: ociIndexMediaType, Manifests: []ociDescriptor{}})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func BlobPath(repoPath string, digest string) (string, bool) {
|
|
var algo string
|
|
var hexPart string
|
|
var ok bool
|
|
algo, hexPart, ok = parseDigest(digest)
|
|
if !ok {
|
|
return "", false
|
|
}
|
|
if algo != "sha256" {
|
|
return "", false
|
|
}
|
|
return filepath.Join(repoPath, "blobs", "sha256", hexPart), true
|
|
}
|
|
|
|
func HasBlob(repoPath string, digest string) (bool, error) {
|
|
var path string
|
|
var ok bool
|
|
var err error
|
|
path, ok = BlobPath(repoPath, digest)
|
|
if !ok {
|
|
return false, nil
|
|
}
|
|
_, err = os.Stat(path)
|
|
if err == nil {
|
|
return true, nil
|
|
}
|
|
if os.IsNotExist(err) {
|
|
return false, nil
|
|
}
|
|
return false, err
|
|
}
|
|
|
|
func ReadBlob(repoPath string, digest string) ([]byte, error) {
|
|
var path string
|
|
var ok bool
|
|
var data []byte
|
|
var err error
|
|
path, ok = BlobPath(repoPath, digest)
|
|
if !ok {
|
|
return nil, ErrNotFound
|
|
}
|
|
data, err = os.ReadFile(path)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return nil, ErrNotFound
|
|
}
|
|
return nil, err
|
|
}
|
|
return data, nil
|
|
}
|
|
|
|
func WriteBlob(repoPath string, digest string, data []byte) error {
|
|
var path string
|
|
var ok bool
|
|
var err error
|
|
var dir string
|
|
path, ok = BlobPath(repoPath, digest)
|
|
if !ok {
|
|
return errors.New("invalid digest")
|
|
}
|
|
dir = filepath.Dir(path)
|
|
err = os.MkdirAll(dir, 0o755)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = os.WriteFile(path, data, 0o644)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func ComputeDigest(data []byte) string {
|
|
var sum [32]byte
|
|
sum = sha256.Sum256(data)
|
|
return "sha256:" + hex.EncodeToString(sum[:])
|
|
}
|
|
|
|
func parseDigest(digest string) (string, string, bool) {
|
|
var parts []string
|
|
var algo string
|
|
var hexPart string
|
|
parts = strings.SplitN(digest, ":", 2)
|
|
if len(parts) != 2 {
|
|
return "", "", false
|
|
}
|
|
algo = parts[0]
|
|
hexPart = parts[1]
|
|
if algo == "" || hexPart == "" {
|
|
return "", "", false
|
|
}
|
|
return algo, hexPart, true
|
|
}
|
|
|
|
func loadIndex(repoPath string) (ociIndex, error) {
|
|
var idx ociIndex
|
|
var path string
|
|
var data []byte
|
|
var err error
|
|
path = filepath.Join(repoPath, "index.json")
|
|
data, err = os.ReadFile(path)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return idx, ErrNotFound
|
|
}
|
|
return idx, err
|
|
}
|
|
err = json.Unmarshal(data, &idx)
|
|
if err != nil {
|
|
return idx, err
|
|
}
|
|
if idx.Manifests == nil {
|
|
idx.Manifests = []ociDescriptor{}
|
|
}
|
|
return idx, nil
|
|
}
|
|
|
|
func saveIndex(repoPath string, idx ociIndex) error {
|
|
var path string
|
|
path = filepath.Join(repoPath, "index.json")
|
|
return writeJSONFile(path, idx)
|
|
}
|
|
|
|
func updateTag(repoPath string, tag string, desc ociDescriptor) error {
|
|
var idx ociIndex
|
|
var err error
|
|
var updated []ociDescriptor
|
|
var i int
|
|
var existing ociDescriptor
|
|
if desc.Annotations == nil {
|
|
desc.Annotations = map[string]string{}
|
|
}
|
|
desc.Annotations[ociTagAnnotation] = tag
|
|
idx, err = loadIndex(repoPath)
|
|
if err != nil {
|
|
if err == ErrNotFound {
|
|
idx = ociIndex{SchemaVersion: 2, MediaType: ociIndexMediaType, Manifests: []ociDescriptor{}}
|
|
} else {
|
|
return err
|
|
}
|
|
}
|
|
updated = make([]ociDescriptor, 0, len(idx.Manifests))
|
|
for i = 0; i < len(idx.Manifests); i++ {
|
|
existing = idx.Manifests[i]
|
|
if existing.Annotations != nil && existing.Annotations[ociTagAnnotation] == tag {
|
|
continue
|
|
}
|
|
updated = append(updated, existing)
|
|
}
|
|
updated = append(updated, desc)
|
|
idx.Manifests = updated
|
|
return saveIndex(repoPath, idx)
|
|
}
|
|
|
|
func resolveTag(repoPath string, tag string) (ociDescriptor, error) {
|
|
var idx ociIndex
|
|
var err error
|
|
var i int
|
|
var desc ociDescriptor
|
|
idx, err = loadIndex(repoPath)
|
|
if err != nil {
|
|
return desc, err
|
|
}
|
|
for i = 0; i < len(idx.Manifests); i++ {
|
|
desc = idx.Manifests[i]
|
|
if desc.Annotations != nil && desc.Annotations[ociTagAnnotation] == tag {
|
|
return desc, nil
|
|
}
|
|
}
|
|
return desc, ErrNotFound
|
|
}
|
|
|
|
func listTags(repoPath string) ([]string, error) {
|
|
var idx ociIndex
|
|
var err error
|
|
var tags []string
|
|
var i int
|
|
var desc ociDescriptor
|
|
var tag string
|
|
idx, err = loadIndex(repoPath)
|
|
if err != nil {
|
|
if err == ErrNotFound {
|
|
return []string{}, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
tags = []string{}
|
|
for i = 0; i < len(idx.Manifests); i++ {
|
|
desc = idx.Manifests[i]
|
|
if desc.Annotations == nil {
|
|
continue
|
|
}
|
|
tag = desc.Annotations[ociTagAnnotation]
|
|
if tag != "" {
|
|
tags = append(tags, tag)
|
|
}
|
|
}
|
|
return tags, nil
|
|
}
|
|
|
|
func writeJSONFile(path string, value interface{}) error {
|
|
var data []byte
|
|
var err error
|
|
var temp string
|
|
data, err = json.MarshalIndent(value, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
temp = path + ".tmp"
|
|
err = os.WriteFile(temp, data, 0o644)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return os.Rename(temp, path)
|
|
}
|
|
|
|
func computeDigestFromReader(reader io.Reader) (string, int64, error) {
|
|
var hash hashWriter
|
|
var size int64
|
|
var err error
|
|
hash = newHashWriter()
|
|
size, err = io.Copy(&hash, reader)
|
|
if err != nil {
|
|
return "", 0, err
|
|
}
|
|
return hash.Digest(), size, nil
|
|
}
|
|
|
|
type hashWriter struct {
|
|
hasher hashState
|
|
}
|
|
|
|
type hashState interface {
|
|
Write([]byte) (int, error)
|
|
Sum([]byte) []byte
|
|
}
|
|
|
|
func newHashWriter() hashWriter {
|
|
var h hashWriter
|
|
h.hasher = sha256.New()
|
|
return h
|
|
}
|
|
|
|
func (h *hashWriter) Write(p []byte) (int, error) {
|
|
return h.hasher.Write(p)
|
|
}
|
|
|
|
func (h *hashWriter) Digest() string {
|
|
var sum []byte
|
|
sum = h.hasher.Sum(nil)
|
|
return "sha256:" + hex.EncodeToString(sum)
|
|
}
|