summaryrefslogtreecommitdiffstats
path: root/registry/provider.go
diff options
context:
space:
mode:
authorColin Wilk <colin@wilk.cx>2026-10-30 10:04:02 +0100
committerColin Wilk <colin@wilk.cx>2026-10-30 10:07:43 +0100
commitb03b00869391cd70ef1e40561f72dffbd2f47b29 (patch)
treefd1941854bea58ec464b81ac8b0d8627cbed2e74 /registry/provider.go
downloadcr-web-b03b00869391cd70ef1e40561f72dffbd2f47b29.tar.gz
cr-web-b03b00869391cd70ef1e40561f72dffbd2f47b29.zip
Initial Commit
Diffstat (limited to 'registry/provider.go')
-rw-r--r--registry/provider.go268
1 files changed, 268 insertions, 0 deletions
diff --git a/registry/provider.go b/registry/provider.go
new file mode 100644
index 0000000..0c90bec
--- /dev/null
+++ b/registry/provider.go
@@ -0,0 +1,268 @@
+package registry
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "strings"
+ "time"
+)
+
+type HTTPProvider struct {
+ BaseURL string
+ Client *http.Client
+}
+
+// Compile time verify that HTTPProvider implements Provider interface
+var _ Provider = (*HTTPProvider)(nil)
+
+func (p *HTTPProvider) Repositories(ctx context.Context) ([]Repository, error) {
+ url := strings.TrimRight(p.BaseURL, "/") + "/v2/_zot/ext/search"
+ body := `{"query":"{ RepoListWithNewestImage { Results { Name LastUpdated } } }"}`
+ req, err := http.NewRequestWithContext(ctx, "POST", url, strings.NewReader(body))
+ if err != nil {
+ return nil, err
+ }
+ req.Header.Set("Content-Type", "application/json")
+
+ resp, err := p.Client.Do(req)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode == http.StatusNotFound {
+ return nil, ErrNotFound
+ }
+
+ if resp.StatusCode != http.StatusOK {
+ return nil, fmt.Errorf("registry returned %s", resp.Status)
+ }
+
+ var result struct {
+ Data struct {
+ RepoListWithNewestImage struct {
+ Results []struct {
+ Name string
+ LastUpdated time.Time
+ }
+ }
+ }
+ Errors []struct {
+ Message string
+ }
+ }
+
+ if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
+ return nil, err
+ }
+ if len(result.Errors) > 0 {
+ return nil, fmt.Errorf("registry search: %s", result.Errors[0].Message)
+ }
+
+ repositories := make([]Repository, 0, len(result.Data.RepoListWithNewestImage.Results))
+ for _, repo := range result.Data.RepoListWithNewestImage.Results {
+ repositories = append(repositories, Repository{
+ Name: repo.Name,
+ UpdatedAt: repo.LastUpdated,
+ Tags: []Tag{},
+ })
+ }
+
+ return repositories, nil
+}
+
+func (p *HTTPProvider) Repository(ctx context.Context, name string) (Repository, error) {
+ body, err := json.Marshal(map[string]string{
+ "query": fmt.Sprintf("{ ImageList(repo: %q) { Results { Tag Digest LastUpdated Size } } }", name),
+ })
+ if err != nil {
+ return Repository{}, err
+ }
+
+ url := strings.TrimRight(p.BaseURL, "/") + "/v2/_zot/ext/search"
+ req, err := http.NewRequestWithContext(ctx, "POST", url, strings.NewReader(string(body)))
+ if err != nil {
+ return Repository{}, err
+ }
+ req.Header.Set("Content-Type", "application/json")
+
+ resp, err := p.Client.Do(req)
+ if err != nil {
+ return Repository{}, err
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode == http.StatusNotFound {
+ return Repository{}, ErrNotFound
+ }
+
+ if resp.StatusCode != http.StatusOK {
+ return Repository{}, fmt.Errorf("registry returned %s", resp.Status)
+ }
+
+ var result struct {
+ Data struct {
+ ImageList struct {
+ Results []struct {
+ Tag string
+ Digest string
+ LastUpdated time.Time
+ Size int64 `json:"Size,string"`
+ }
+ }
+ }
+ Errors []struct {
+ Message string
+ }
+ }
+
+ if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
+ return Repository{}, err
+ }
+ if len(result.Errors) > 0 {
+ return Repository{}, fmt.Errorf("registry search: %s", result.Errors[0].Message)
+ }
+
+ repo := Repository{
+ Name: name,
+ Tags: make([]Tag, 0, len(result.Data.ImageList.Results)),
+ }
+
+ for _, image := range result.Data.ImageList.Results {
+ repo.Tags = append(repo.Tags, Tag{
+ Name: image.Tag,
+ ManifestDigest: image.Digest,
+ UpdatedAt: image.LastUpdated,
+ SizeBytes: image.Size,
+ })
+ if image.LastUpdated.After(repo.UpdatedAt) {
+ repo.UpdatedAt = image.LastUpdated
+ }
+ }
+
+ return repo, nil
+}
+
+func (p *HTTPProvider) Manifest(ctx context.Context, repository, digest string) (Manifest, error) {
+ body, err := json.Marshal(map[string]string{
+ "query": fmt.Sprintf(`{ ImageList(repo: %q) { Results {
+ Tag Digest MediaType Size LastUpdated
+ Manifests { Digest Size Platform { Os Arch } Layers { Digest Size } }
+ } } }`, repository),
+ })
+ if err != nil {
+ return Manifest{}, err
+ }
+
+ url := strings.TrimRight(p.BaseURL, "/") + "/v2/_zot/ext/search"
+ req, err := http.NewRequestWithContext(ctx, "POST", url, strings.NewReader(string(body)))
+ if err != nil {
+ return Manifest{}, err
+ }
+ req.Header.Set("Content-Type", "application/json")
+
+ resp, err := p.Client.Do(req)
+ if err != nil {
+ return Manifest{}, err
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode == http.StatusNotFound {
+ return Manifest{}, ErrNotFound
+ }
+ if resp.StatusCode != http.StatusOK {
+ return Manifest{}, fmt.Errorf("registry returned %s", resp.Status)
+ }
+
+ type manifestSummary struct {
+ Digest string
+ Size int64 `json:"Size,string"`
+ Platform struct {
+ Os string
+ Arch string
+ }
+ Layers []struct {
+ Digest string
+ Size int64 `json:"Size,string"`
+ }
+ }
+ var result struct {
+ Data struct {
+ ImageList struct {
+ Results []struct {
+ Tag string
+ Digest string
+ MediaType string
+ Size int64 `json:"Size,string"`
+ LastUpdated time.Time
+ Manifests []manifestSummary
+ }
+ }
+ }
+ Errors []struct {
+ Message string
+ }
+ }
+ if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
+ return Manifest{}, err
+ }
+ if len(result.Errors) > 0 {
+ return Manifest{}, fmt.Errorf("registry search: %s", result.Errors[0].Message)
+ }
+
+ manifest := Manifest{Repository: repository, Digest: digest}
+ for _, image := range result.Data.ImageList.Results {
+ var summary *manifestSummary
+ for i := range image.Manifests {
+ if image.Manifests[i].Digest == digest {
+ summary = &image.Manifests[i]
+ break
+ }
+ }
+ if image.Digest != digest && summary == nil {
+ continue
+ }
+
+ // A platform manifest also lists tags referencing its parent index.
+ manifest.Tags = append(manifest.Tags, TagReference{
+ Name: image.Tag,
+ UpdatedAt: image.LastUpdated,
+ ManifestDigest: image.Digest,
+ })
+ if manifest.Kind != "" {
+ continue
+ }
+
+ manifest.Kind = "image"
+ if image.Digest == digest {
+ manifest.SizeBytes = image.Size
+ if image.MediaType == "application/vnd.oci.image.index.v1+json" ||
+ image.MediaType == "application/vnd.docker.distribution.manifest.list.v2+json" {
+ manifest.Kind = "index"
+ for _, platform := range image.Manifests {
+ manifest.Platforms = append(manifest.Platforms, PlatformManifest{
+ Platform: strings.Trim(platform.Platform.Os+"/"+platform.Platform.Arch, "/"),
+ SizeBytes: platform.Size,
+ Digest: platform.Digest,
+ })
+ }
+ }
+ }
+ if manifest.Kind == "image" && summary != nil {
+ manifest.SizeBytes = summary.Size
+ for _, layer := range summary.Layers {
+ manifest.Layers = append(manifest.Layers, Layer{
+ Digest: layer.Digest,
+ SizeBytes: layer.Size,
+ })
+ }
+ }
+ }
+ if manifest.Kind == "" {
+ return Manifest{}, ErrNotFound
+ }
+
+ return manifest, nil
+}