288 lines
6.2 KiB
Go
288 lines
6.2 KiB
Go
package docker
|
|
|
|
import "encoding/json"
|
|
import "errors"
|
|
import "io/fs"
|
|
import "os"
|
|
import "path/filepath"
|
|
import "strings"
|
|
|
|
type TagInfo struct {
|
|
Tag string `json:"tag"`
|
|
Digest string `json:"digest"`
|
|
Size int64 `json:"size"`
|
|
MediaType string `json:"media_type"`
|
|
}
|
|
|
|
type ManifestDetail struct {
|
|
Reference string `json:"reference"`
|
|
Digest string `json:"digest"`
|
|
MediaType string `json:"media_type"`
|
|
Size int64 `json:"size"`
|
|
Config ImageConfig `json:"config"`
|
|
Layers []ociDescriptor `json:"layers"`
|
|
}
|
|
|
|
type ImageConfig struct {
|
|
Created string `json:"created"`
|
|
Architecture string `json:"architecture"`
|
|
OS string `json:"os"`
|
|
}
|
|
|
|
type manifestPayload struct {
|
|
SchemaVersion int `json:"schemaVersion"`
|
|
MediaType string `json:"mediaType"`
|
|
Config ociDescriptor `json:"config"`
|
|
Layers []ociDescriptor `json:"layers"`
|
|
}
|
|
|
|
func ListTags(repoPath string) ([]TagInfo, error) {
|
|
var tags []string
|
|
var err error
|
|
var list []TagInfo
|
|
var i int
|
|
var tag string
|
|
var desc ociDescriptor
|
|
tags, err = listTags(repoPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
list = make([]TagInfo, 0, len(tags))
|
|
for i = 0; i < len(tags); i++ {
|
|
tag = tags[i]
|
|
desc, err = resolveTag(repoPath, tag)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
list = append(list, TagInfo{
|
|
Tag: tag,
|
|
Digest: desc.Digest,
|
|
Size: desc.Size,
|
|
MediaType: desc.MediaType,
|
|
})
|
|
}
|
|
return list, nil
|
|
}
|
|
|
|
func DeleteTag(repoPath string, tag string) error {
|
|
var idx ociIndex
|
|
var err error
|
|
var updated []ociDescriptor
|
|
var i int
|
|
var desc ociDescriptor
|
|
var keep bool
|
|
idx, err = loadIndex(repoPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
updated = make([]ociDescriptor, 0, len(idx.Manifests))
|
|
for i = 0; i < len(idx.Manifests); i++ {
|
|
desc = idx.Manifests[i]
|
|
keep = true
|
|
if desc.Annotations != nil && desc.Annotations[ociTagAnnotation] == tag {
|
|
keep = false
|
|
}
|
|
if keep {
|
|
updated = append(updated, desc)
|
|
}
|
|
}
|
|
idx.Manifests = updated
|
|
return saveIndex(repoPath, idx)
|
|
}
|
|
|
|
func DeleteImage(repoPath string, image string) error {
|
|
var imagePath string
|
|
imagePath = ImagePath(repoPath, image)
|
|
return os.RemoveAll(imagePath)
|
|
}
|
|
|
|
func RenameTag(repoPath string, from string, to string) error {
|
|
var idx ociIndex
|
|
var err error
|
|
var i int
|
|
var desc ociDescriptor
|
|
var hasFrom bool
|
|
var hasTo bool
|
|
if from == "" || to == "" {
|
|
return errors.New("tag required")
|
|
}
|
|
if from == to {
|
|
return errors.New("tag unchanged")
|
|
}
|
|
idx, err = loadIndex(repoPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for i = 0; i < len(idx.Manifests); i++ {
|
|
desc = idx.Manifests[i]
|
|
if desc.Annotations == nil {
|
|
continue
|
|
}
|
|
if desc.Annotations[ociTagAnnotation] == to {
|
|
hasTo = true
|
|
}
|
|
}
|
|
if hasTo {
|
|
return errors.New("tag already exists")
|
|
}
|
|
for i = 0; i < len(idx.Manifests); i++ {
|
|
desc = idx.Manifests[i]
|
|
if desc.Annotations == nil {
|
|
continue
|
|
}
|
|
if desc.Annotations[ociTagAnnotation] == from {
|
|
desc.Annotations[ociTagAnnotation] = to
|
|
idx.Manifests[i] = desc
|
|
hasFrom = true
|
|
break
|
|
}
|
|
}
|
|
if !hasFrom {
|
|
return ErrNotFound
|
|
}
|
|
return saveIndex(repoPath, idx)
|
|
}
|
|
|
|
func RenameImage(repoPath string, from string, to string) error {
|
|
var srcPath string
|
|
var destPath string
|
|
var err error
|
|
var info os.FileInfo
|
|
var dir string
|
|
if from == to {
|
|
return errors.New("image unchanged")
|
|
}
|
|
if IsReservedImagePath(to) {
|
|
return errors.New("invalid image name")
|
|
}
|
|
srcPath = ImagePath(repoPath, from)
|
|
destPath = ImagePath(repoPath, to)
|
|
info, err = os.Stat(srcPath)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return ErrNotFound
|
|
}
|
|
return err
|
|
}
|
|
if info == nil || !info.IsDir() {
|
|
return errors.New("source is not a directory")
|
|
}
|
|
_, err = os.Stat(destPath)
|
|
if err == nil {
|
|
return errors.New("target already exists")
|
|
}
|
|
if err != nil && !os.IsNotExist(err) {
|
|
return err
|
|
}
|
|
dir = filepath.Dir(destPath)
|
|
err = os.MkdirAll(dir, 0o755)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return os.Rename(srcPath, destPath)
|
|
}
|
|
|
|
func ListImages(repoPath string) ([]string, error) {
|
|
var images []string
|
|
var err error
|
|
var seen map[string]struct{}
|
|
var root string
|
|
var ok bool
|
|
var dummy struct{}
|
|
seen = map[string]struct{}{}
|
|
root = filepath.Clean(repoPath)
|
|
err = filepath.WalkDir(root, func(path string, entry fs.DirEntry, walkErr error) error {
|
|
var base string
|
|
var rel string
|
|
var dir string
|
|
if walkErr != nil {
|
|
return walkErr
|
|
}
|
|
if entry.IsDir() {
|
|
base = entry.Name()
|
|
if base == "blobs" || base == "uploads" {
|
|
return filepath.SkipDir
|
|
}
|
|
return nil
|
|
}
|
|
if entry.Name() != "oci-layout" {
|
|
return nil
|
|
}
|
|
dir = filepath.Dir(path)
|
|
rel, err = filepath.Rel(root, dir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if rel == "." {
|
|
rel = ""
|
|
}
|
|
rel = filepath.ToSlash(rel)
|
|
if rel == ".root" {
|
|
rel = ""
|
|
}
|
|
dummy, ok = seen[rel]
|
|
_ = dummy
|
|
if !ok {
|
|
seen[rel] = struct{}{}
|
|
images = append(images, rel)
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil && !os.IsNotExist(err) {
|
|
return nil, err
|
|
}
|
|
return images, nil
|
|
}
|
|
|
|
func ImagePath(repoPath string, image string) string {
|
|
var cleaned string
|
|
var normalized string
|
|
cleaned = strings.Trim(image, "/")
|
|
if cleaned == "" {
|
|
return filepath.Join(repoPath, ".root")
|
|
}
|
|
normalized = strings.ReplaceAll(cleaned, "\\", "/")
|
|
if normalized == ".root" {
|
|
return filepath.Join(repoPath, ".root")
|
|
}
|
|
return filepath.Join(repoPath, filepath.FromSlash(cleaned))
|
|
}
|
|
|
|
func GetManifestDetail(repoPath string, reference string) (ManifestDetail, error) {
|
|
var detail ManifestDetail
|
|
var desc ociDescriptor
|
|
var err error
|
|
var data []byte
|
|
var payload manifestPayload
|
|
var configData []byte
|
|
var config ImageConfig
|
|
desc, err = resolveManifest(repoPath, reference)
|
|
if err != nil {
|
|
return detail, err
|
|
}
|
|
data, err = ReadBlob(repoPath, desc.Digest)
|
|
if err != nil {
|
|
return detail, err
|
|
}
|
|
err = json.Unmarshal(data, &payload)
|
|
if err != nil {
|
|
return detail, err
|
|
}
|
|
if payload.MediaType != "" {
|
|
desc.MediaType = payload.MediaType
|
|
}
|
|
configData, err = ReadBlob(repoPath, payload.Config.Digest)
|
|
if err == nil {
|
|
_ = json.Unmarshal(configData, &config)
|
|
}
|
|
detail = ManifestDetail{
|
|
Reference: reference,
|
|
Digest: desc.Digest,
|
|
MediaType: desc.MediaType,
|
|
Size: int64(len(data)),
|
|
Config: config,
|
|
Layers: payload.Layers,
|
|
}
|
|
return detail, nil
|
|
}
|