Files
codit/backend/internal/docker/layout.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)
}