mirror of
https://github.com/hanzoai/forge.git
synced 2026-10-30 15:00:39 +00:00
Source files keep their SPDX-License-Identifier and Hanzo copyright line; the Gitea, Gogs and LFS Test Server copyrights they used to repeat now live once in NOTICE, alongside LICENSE. The header lint accepts any copyright line above the SPDX line, so it no longer demands an upstream one. Co-authored-by: Hanzo Dev <[email protected]>
189 lines
5.6 KiB
Go
189 lines
5.6 KiB
Go
// Copyright 2026 Hanzo AI, Inc. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package container
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"strings"
|
|
|
|
"github.com/hanzoai/git/modules/json"
|
|
"github.com/hanzoai/git/modules/packages/container/helm"
|
|
"github.com/hanzoai/git/modules/validation"
|
|
|
|
oci "github.com/opencontainers/image-spec/specs-go/v1"
|
|
)
|
|
|
|
const (
|
|
PropertyRepository = "container.repository"
|
|
PropertyDigest = "container.digest"
|
|
PropertyMediaType = "container.mediatype"
|
|
PropertyManifestTagged = "container.manifest.tagged"
|
|
PropertyManifestReference = "container.manifest.reference"
|
|
|
|
DefaultPlatform = "linux/amd64"
|
|
|
|
labelLicenses = "org.opencontainers.image.licenses"
|
|
labelURL = "org.opencontainers.image.url"
|
|
labelSource = "org.opencontainers.image.source"
|
|
labelDocumentation = "org.opencontainers.image.documentation"
|
|
labelDescription = "org.opencontainers.image.description"
|
|
labelAuthors = "org.opencontainers.image.authors"
|
|
)
|
|
|
|
type ImageType string
|
|
|
|
const (
|
|
TypeOCI ImageType = "oci"
|
|
TypeHelm ImageType = "helm"
|
|
)
|
|
|
|
// Name gets the name of the image type
|
|
func (it ImageType) Name() string {
|
|
switch it {
|
|
case TypeHelm:
|
|
return "Helm Chart"
|
|
default:
|
|
return "OCI / Docker"
|
|
}
|
|
}
|
|
|
|
// Metadata represents the metadata of a Container package
|
|
type Metadata struct {
|
|
Type ImageType `json:"type"`
|
|
IsTagged bool `json:"is_tagged"`
|
|
Platform string `json:"platform,omitempty"`
|
|
Description string `json:"description,omitempty"`
|
|
Authors []string `json:"authors,omitempty"`
|
|
Licenses string `json:"license,omitempty"`
|
|
ProjectURL string `json:"project_url,omitempty"`
|
|
RepositoryURL string `json:"repository_url,omitempty"`
|
|
DocumentationURL string `json:"documentation_url,omitempty"`
|
|
Labels map[string]string `json:"labels,omitempty"`
|
|
ImageLayers []string `json:"layer_creation,omitempty"`
|
|
Manifests []*Manifest `json:"manifests,omitempty"`
|
|
}
|
|
|
|
type Manifest struct {
|
|
Platform string `json:"platform"`
|
|
Digest string `json:"digest"`
|
|
Size int64 `json:"size"`
|
|
}
|
|
|
|
func IsMediaTypeValid(mt string) bool {
|
|
return strings.HasPrefix(mt, "application/vnd.docker.") || strings.HasPrefix(mt, "application/vnd.oci.")
|
|
}
|
|
|
|
func IsMediaTypeImageManifest(mt string) bool {
|
|
return strings.EqualFold(mt, oci.MediaTypeImageManifest) || strings.EqualFold(mt, "application/vnd.docker.distribution.manifest.v2+json")
|
|
}
|
|
|
|
func IsMediaTypeImageIndex(mt string) bool {
|
|
return strings.EqualFold(mt, oci.MediaTypeImageIndex) || strings.EqualFold(mt, "application/vnd.docker.distribution.manifest.list.v2+json")
|
|
}
|
|
|
|
// ParseImageConfig parses the metadata of an image config
|
|
func ParseImageConfig(mediaType string, r io.Reader) (*Metadata, error) {
|
|
if strings.EqualFold(mediaType, helm.ConfigMediaType) {
|
|
return parseHelmConfig(r)
|
|
}
|
|
|
|
// fallback to OCI Image Config
|
|
// FIXME: this fallback is not right, we should strictly check the media type in the future
|
|
metadata, err := parseOCIImageConfig(r)
|
|
if err != nil {
|
|
if !IsMediaTypeImageManifest(mediaType) {
|
|
return &Metadata{Platform: "unknown/unknown"}, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
return metadata, nil
|
|
}
|
|
|
|
func parseOCIImageConfig(r io.Reader) (*Metadata, error) {
|
|
var image oci.Image
|
|
// FIXME: JSON-KEY-CASE: here seems a abuse of the case-insensitive decoding feature, spec is case-sensitive
|
|
// https://github.com/opencontainers/image-spec/blob/main/schema/config-schema.json
|
|
if err := json.NewDecoderCaseInsensitive(r).Decode(&image); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
platform := DefaultPlatform
|
|
if image.OS != "" && image.Architecture != "" {
|
|
platform = fmt.Sprintf("%s/%s", image.OS, image.Architecture)
|
|
if image.Variant != "" {
|
|
platform = fmt.Sprintf("%s/%s", platform, image.Variant)
|
|
}
|
|
}
|
|
|
|
imageLayers := make([]string, 0, len(image.History))
|
|
for _, history := range image.History {
|
|
cmd := history.CreatedBy
|
|
if i := strings.Index(cmd, "#(nop) "); i != -1 {
|
|
cmd = strings.TrimSpace(cmd[i+7:])
|
|
}
|
|
if cmd != "" {
|
|
imageLayers = append(imageLayers, cmd)
|
|
}
|
|
}
|
|
|
|
metadata := &Metadata{
|
|
Type: TypeOCI,
|
|
Platform: platform,
|
|
Licenses: image.Config.Labels[labelLicenses],
|
|
ProjectURL: image.Config.Labels[labelURL],
|
|
RepositoryURL: image.Config.Labels[labelSource],
|
|
DocumentationURL: image.Config.Labels[labelDocumentation],
|
|
Description: image.Config.Labels[labelDescription],
|
|
Labels: image.Config.Labels,
|
|
ImageLayers: imageLayers,
|
|
}
|
|
|
|
if authors, ok := image.Config.Labels[labelAuthors]; ok {
|
|
metadata.Authors = []string{authors}
|
|
}
|
|
|
|
if !validation.IsValidURL(metadata.ProjectURL) {
|
|
metadata.ProjectURL = ""
|
|
}
|
|
if !validation.IsValidURL(metadata.RepositoryURL) {
|
|
metadata.RepositoryURL = ""
|
|
}
|
|
if !validation.IsValidURL(metadata.DocumentationURL) {
|
|
metadata.DocumentationURL = ""
|
|
}
|
|
|
|
return metadata, nil
|
|
}
|
|
|
|
func parseHelmConfig(r io.Reader) (*Metadata, error) {
|
|
var config helm.Metadata
|
|
if err := json.NewDecoder(r).Decode(&config); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
metadata := &Metadata{
|
|
Type: TypeHelm,
|
|
Description: config.Description,
|
|
ProjectURL: config.Home,
|
|
}
|
|
|
|
if len(config.Maintainers) > 0 {
|
|
authors := make([]string, 0, len(config.Maintainers))
|
|
for _, maintainer := range config.Maintainers {
|
|
authors = append(authors, maintainer.Name)
|
|
}
|
|
metadata.Authors = authors
|
|
}
|
|
|
|
if len(config.Sources) > 0 && validation.IsValidURL(config.Sources[0]) {
|
|
metadata.RepositoryURL = config.Sources[0]
|
|
}
|
|
if !validation.IsValidURL(metadata.ProjectURL) {
|
|
metadata.ProjectURL = ""
|
|
}
|
|
|
|
return metadata, nil
|
|
}
|