Files
zeekayandHanzo Dev aa87442fdd Record upstream authorship centrally in NOTICE instead of in every file header
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]>
2026-09-10 17:50:35 -07:00

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
}