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 }