diff options
Diffstat (limited to 'registry/provider.go')
| -rw-r--r-- | registry/provider.go | 268 |
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 +} |