real init
This commit is contained in:
@@ -0,0 +1,198 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.dcntrlized.org/Dcntrlized/Maestro/lib/consts"
|
||||
"git.dcntrlized.org/Dcntrlized/Maestro/lib/logger"
|
||||
|
||||
"github.com/google/go-containerregistry/pkg/authn"
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
"github.com/google/go-containerregistry/pkg/v1/remote"
|
||||
"golang.org/x/mod/semver"
|
||||
)
|
||||
|
||||
// RegistryContainer represents a clean API payload specifically for container registry items.
|
||||
type RegistryContainer struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
Image string `json:"image"`
|
||||
Title string `json:"title,omitempty"`
|
||||
Vendor string `json:"vendor,omitempty"`
|
||||
Author string `json:"author,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Source string `json:"source,omitempty"`
|
||||
CreatedAt string `json:"created_at,omitempty"`
|
||||
}
|
||||
|
||||
type RawPackage struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
HTMLURL string `json:"html_url"`
|
||||
Repository *struct {
|
||||
HTMLURL string `json:"html_url"`
|
||||
} `json:"repository"`
|
||||
}
|
||||
|
||||
func GetRegistry(w http.ResponseWriter, r *http.Request) {
|
||||
// 1. Query Gitea REST API using the exact Org name
|
||||
apiURL := fmt.Sprintf("https://git.dcntrlized.org/api/v1/packages/%s?type=container", consts.GITEA_ORG)
|
||||
|
||||
req, err := http.NewRequestWithContext(r.Context(), http.MethodGet, apiURL, nil)
|
||||
if err != nil {
|
||||
logger.Error("Failed to create request: %v", err)
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", "token "+consts.GITEA_RW_PACKAGE_TOKEN)
|
||||
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
logger.Error("Failed to reach Gitea package API: %v", err)
|
||||
http.Error(w, "Failed to connect to upstream registry", http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
logger.Error("Registry API returned non-200 status code: %d", resp.StatusCode)
|
||||
http.Error(w, fmt.Sprintf("Registry API error: %s", resp.Status), resp.StatusCode)
|
||||
return
|
||||
}
|
||||
|
||||
var rawPackages []RawPackage
|
||||
if err := json.NewDecoder(resp.Body).Decode(&rawPackages); err != nil {
|
||||
logger.Error("Failed to decode registry response: %v", err)
|
||||
http.Error(w, "Failed to decode registry response", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// 2. Group by Name and filter out "latest" & "sha256:"
|
||||
groupedPackages := make(map[string][]RawPackage)
|
||||
for _, pkg := range rawPackages {
|
||||
if pkg.Version == "latest" || strings.HasPrefix(pkg.Version, "sha256:") {
|
||||
continue
|
||||
}
|
||||
groupedPackages[pkg.Name] = append(groupedPackages[pkg.Name], pkg)
|
||||
}
|
||||
|
||||
// Helper function to normalize version strings for semver package (requires 'v' prefix)
|
||||
formatSemver := func(v string) string {
|
||||
if !strings.HasPrefix(v, "v") {
|
||||
return "v" + v
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// 3. For each container name, find the package entry with the highest semver
|
||||
var latestPackages []RawPackage
|
||||
for _, pkgs := range groupedPackages {
|
||||
if len(pkgs) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
sort.Slice(pkgs, func(i, j int) bool {
|
||||
vI := formatSemver(pkgs[i].Version)
|
||||
vJ := formatSemver(pkgs[j].Version)
|
||||
|
||||
// Compare using semver package
|
||||
if semver.IsValid(vI) && semver.IsValid(vJ) {
|
||||
return semver.Compare(vI, vJ) > 0
|
||||
}
|
||||
// Fall back to string comparison if not valid semver
|
||||
return pkgs[i].Version > pkgs[j].Version
|
||||
})
|
||||
|
||||
// First item after descending sort is the highest version
|
||||
latestPackages = append(latestPackages, pkgs[0])
|
||||
}
|
||||
|
||||
ociAuth := remote.WithAuth(&authn.Basic{
|
||||
Username: consts.GITEA_USER,
|
||||
Password: consts.GITEA_RW_PACKAGE_TOKEN,
|
||||
})
|
||||
|
||||
ociOrg := strings.ToLower(consts.GITEA_ORG)
|
||||
var registryItems []RegistryContainer
|
||||
|
||||
// 4. Fetch OCI metadata only for the highest version of each package
|
||||
for _, pkg := range latestPackages {
|
||||
pkgNameLower := strings.ToLower(pkg.Name)
|
||||
imageRef := fmt.Sprintf("git.dcntrlized.org/%s/%s:%s", ociOrg, pkgNameLower, pkg.Version)
|
||||
|
||||
source := pkg.HTMLURL
|
||||
if pkg.Repository != nil && pkg.Repository.HTMLURL != "" {
|
||||
source = pkg.Repository.HTMLURL
|
||||
}
|
||||
|
||||
item := RegistryContainer{
|
||||
Name: pkg.Name,
|
||||
Version: pkg.Version,
|
||||
Image: imageRef,
|
||||
Source: source,
|
||||
CreatedAt: pkg.CreatedAt,
|
||||
}
|
||||
|
||||
ref, err := name.ParseReference(imageRef)
|
||||
if err != nil {
|
||||
logger.Error("Failed to parse image reference %s: %v", imageRef, err)
|
||||
registryItems = append(registryItems, item)
|
||||
continue
|
||||
}
|
||||
|
||||
img, err := remote.Image(ref, ociAuth, remote.WithContext(r.Context()))
|
||||
if err != nil {
|
||||
logger.Error("Failed to fetch OCI metadata for %s: %v", imageRef, err)
|
||||
registryItems = append(registryItems, item)
|
||||
continue
|
||||
}
|
||||
|
||||
// Extract annotations from OCI manifest
|
||||
if manifest, err := img.Manifest(); err == nil && manifest != nil {
|
||||
if title, ok := manifest.Annotations["org.opencontainers.image.title"]; ok {
|
||||
item.Title = title
|
||||
}
|
||||
if vendor, ok := manifest.Annotations["org.opencontainers.image.vendor"]; ok {
|
||||
item.Vendor = vendor
|
||||
}
|
||||
if author, ok := manifest.Annotations["org.opencontainers.image.authors"]; ok {
|
||||
item.Author = author
|
||||
}
|
||||
if desc, ok := manifest.Annotations["org.opencontainers.image.description"]; ok {
|
||||
item.Description = desc
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to Image Config labels
|
||||
if configFile, err := img.ConfigFile(); err == nil && configFile != nil {
|
||||
labels := configFile.Config.Labels
|
||||
if item.Title == "" {
|
||||
item.Title = labels["org.opencontainers.image.title"]
|
||||
}
|
||||
if item.Vendor == "" {
|
||||
item.Vendor = labels["org.opencontainers.image.vendor"]
|
||||
}
|
||||
if item.Author == "" {
|
||||
item.Author = labels["org.opencontainers.image.authors"]
|
||||
}
|
||||
if item.Description == "" {
|
||||
item.Description = labels["org.opencontainers.image.description"]
|
||||
}
|
||||
}
|
||||
|
||||
registryItems = append(registryItems, item)
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(w).Encode(registryItems); err != nil {
|
||||
logger.Error("Failed to write response: %v", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user