Files
tiny-forge/internal/registry/registry.go
T
alexei.dolgolyov 37e251da85 feat: auto-discover container images from registries
- Add ListImages() to registry interface, implement for Gitea
- Add owner field to registry config (needed for Gitea packages API)
- GET /api/registries/:id/images endpoint
- "Browse Images" button on Projects and Quick Deploy pages
- Image dropdown with registry grouping and search
- i18n support (EN/RU) for all new UI strings
2026-03-28 14:04:11 +03:00

90 lines
2.9 KiB
Go

package registry
import (
"context"
"fmt"
"path"
"sort"
"strings"
)
// RegistryImage represents a container image discovered from a registry.
type RegistryImage struct {
Name string `json:"name"`
Owner string `json:"owner"`
FullRef string `json:"full_ref"` // e.g., "git.example.com/owner/my-app"
}
// Client defines the interface for interacting with a container image registry.
type Client interface {
// ListTags returns all available tags for the given image.
ListTags(ctx context.Context, image string) ([]string, error)
// GetLatestTag returns the most recently created tag that matches the given
// glob pattern. Returns an empty string and no error if no tags match.
GetLatestTag(ctx context.Context, image string, pattern string) (string, error)
// ListImages returns all container images available in the registry for the
// given owner. Returns an error if the registry does not support image listing.
ListImages(ctx context.Context, owner string) ([]RegistryImage, error)
}
// DeployTriggerer is called by the poller when a new tag is detected for a
// stage with auto_deploy enabled. This decouples the registry package from the
// deployer implementation.
type DeployTriggerer interface {
TriggerDeploy(ctx context.Context, projectID, stageID, imageTag string) error
}
// MatchTags filters a list of tags, returning only those that match the given
// glob pattern. Pattern matching uses path.Match semantics (*, ?, []).
// Returns an error if the pattern is malformed.
func MatchTags(tags []string, pattern string) ([]string, error) {
if pattern == "" || pattern == "*" {
result := make([]string, len(tags))
copy(result, tags)
return result, nil
}
// Validate pattern once before iterating.
if _, err := path.Match(pattern, ""); err != nil {
return nil, fmt.Errorf("invalid tag pattern %q: %w", pattern, err)
}
var matched []string
for _, tag := range tags {
ok, _ := path.Match(pattern, tag)
if ok {
matched = append(matched, tag)
}
}
return matched, nil
}
// LatestTag returns the last element of a sorted tag list that matches the
// pattern. Tags are sorted lexicographically; the "latest" is the last in sort
// order. Returns empty string if no tags match. Returns an error if the pattern
// is malformed.
func LatestTag(tags []string, pattern string) (string, error) {
matched, err := MatchTags(tags, pattern)
if err != nil {
return "", err
}
if len(matched) == 0 {
return "", nil
}
sort.Strings(matched)
return matched[len(matched)-1], nil
}
// NewClient creates a registry Client based on the registry type string.
// Supported types: "gitea". Future: "github", "dockerhub".
func NewClient(registryType, baseURL, token string) (Client, error) {
switch strings.ToLower(registryType) {
case "gitea":
return NewGiteaClient(baseURL, token), nil
default:
return nil, fmt.Errorf("unsupported registry type: %s", registryType)
}
}