package artifacts import ( "context" "encoding/json" "errors" "fmt" "io" "net/http" "strings" ) // Listing and letting go of tagged documents: the check builds the scratch namespace keeps (novox/hq ADR 0350 // §5). A check build is found by its tag, and the build seat's holder lets each go when its pull request closes // or it is seven days old, so the store keeps a week of check builds and no more. // Tags is every tag repository holds, in the store's order. None for a repository the store does not hold. func (s Store) Tags(ctx context.Context, repository string) ([]string, error) { if s.Address == "" { return nil, fmt.Errorf("this mesh has no artifact store on its network to list %s in", repository) } var tags []string next := "http://" + s.Address + "/v2/" + repository + "/tags/list?n=1000" for next != "" { request, err := http.NewRequestWithContext(ctx, http.MethodGet, next, nil) if err != nil { return nil, err } response, err := s.client().Do(request) if err != nil { return nil, fmt.Errorf("cannot reach the artifact store at %s: %w", s.Address, err) } switch response.StatusCode { case http.StatusOK: case http.StatusNotFound: response.Body.Close() return tags, nil default: response.Body.Close() return nil, fmt.Errorf("the artifact store answered %s for the tags of %s", response.Status, repository) } var page struct { Tags []string `json:"tags"` } err = json.NewDecoder(io.LimitReader(response.Body, 8<<20)).Decode(&page) link := response.Header.Get("Link") response.Body.Close() if err != nil { return nil, fmt.Errorf("the tags of %s could not be read: %w", repository, err) } tags = append(tags, page.Tags...) next = nextPage(link, s.Address) } return tags, nil } // nextPage is the registry's `Link: ; rel="next"`, made whole; empty when there is no next page. func nextPage(link, address string) string { if !strings.Contains(link, `rel="next"`) { return "" } start, end := strings.Index(link, "<"), strings.Index(link, ">") if start < 0 || end <= start+1 { return "" } target := link[start+1 : end] if strings.HasPrefix(target, "/") { return "http://" + address + target } return target } // DeleteTagged lets go of the document tag names in repository: its manifest, by digest, so the next // collection takes its layer. Gone when the tag names nothing. func (s Store) DeleteTagged(ctx context.Context, repository, tag string) error { if s.Address == "" { return fmt.Errorf("this mesh has no artifact store on its network to let go of %s:%s in", repository, tag) } digest, err := s.manifestDigest(ctx, repository, tag) if err != nil { return err } if digest == "" { return Gone } err = s.remove(ctx, s.url(repository, "manifests", digest), repository+":"+tag) if errors.Is(err, Gone) { return Gone } return err }