Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(created_at_sort): add support for sorting by created #354

Merged
merged 2 commits into from
Mar 29, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
testdata/.push-output
.envrc
tags
.idea
17 changes: 14 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -10,7 +10,9 @@ to, and `out` will always push to the specified tag. This is to be used in
simpler cases where no real versioning exists.

With `tag_regex` specified, `check` will instead detect tags based on the regex
provided
provided. If `created_at_sort` is set to `true`, the tags will be sorted in descending order by the creation time.
This is useful when you want to get the latest tag based on the regex (see Docker registry issue
[here](https://github.com/docker/hub-feedback/issues/185)).

With `tag` and `tag_regex` both omitted, `check` will instead detect tags based on semver versions
(e.g. `1.2.3`) and return them in semver order. With `variant` included,
@@ -89,9 +91,17 @@ differences:
<br>The syntax of the regular expressions accepted is the same
general syntax used by Perl, Python, and other languages. More precisely,
it is the syntax accepted by RE2 and described at https://golang.org/s/re2syntax
<br>Note if used, this will override all Semver constraints and features
<br>Note if used, this will override all Semver constraints and features.
By default, order of tags is not guaranteed. If you want to sort the tags in descending order, set `created_at_sort` to `true`.
</td>
</tr>
<tr>
<td><code>created_at_sort</code> <em>(Optional)<br>Default: false</em></td>
<td>
If set to `true`, the tags will be sorted in descending order using the creation time from the image history.
This is useful when you want to get the latest tag based on the tag_regex.
</td>
</tr>
<tr>
<td><code>variant</code> <em>(Optional)</em></td>
<td>
@@ -352,7 +362,8 @@ Reports the current digest that the registry has for the tag configured in
### `check` Step (`check` script) with `tag_regex`: discover tags matching regex

Reports the current digest that the registry has for tags matching the regex
configured in `source`. They will be returned in the same order that the source repository lists them.
configured in `source`. They will be returned in the same order that the source repository lists them unless `created_at_sort`
is set to `true`.

### `check` Step (`check` script) without `tag` or `tag_regex`: discover semver tags

93 changes: 87 additions & 6 deletions check_test.go
Original file line number Diff line number Diff line change
@@ -2,12 +2,15 @@ package resource_test

import (
"bytes"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"encoding/pem"
"fmt"
"net/http"
"os/exec"
"strconv"
"time"

"github.com/google/go-containerregistry/pkg/name"
v1 "github.com/google/go-containerregistry/pkg/v1"
@@ -806,8 +809,43 @@ var _ = DescribeTable("tracking semver tags",
ImageName: "random-4",
},
},
Regex: "gr(a|e)y",
Versions: []string{"gray", "grey"},
Regex: "gr(a|e)y",
CreatedAtSort: false,
Versions: []string{"gray", "grey"},
},
),
Entry("simple tag regex where sorted is true",
SemverOrRegexTagCheckExample{
Tags: []testTag{
{
Tag: "1.0.0",
ImageName: "random-1",
},
{
Tag: "non-semver-tag",
ImageName: "random-2",
},
{
Tag: "gem-1338-git-4bd8a5e1a244",
ImageName: "random-3",
},
{
Tag: "gem-182-git-6bd8a5e1a2b3",
ImageName: "random-4",
},
{
Tag: "gem-1337-git-4bd8a5e1a244",
ImageName: "random-5",
},
},
TagsToTime: map[string]time.Time{
"gem-1338-git-4bd8a5e1a244": time.Date(2024, 1, 4, 5, 0, 0, 0, time.UTC),
"gem-182-git-6bd8a5e1a2b3": time.Date(2024, 1, 4, 0, 0, 0, 0, time.UTC),
"gem-1337-git-4bd8a5e1a244": time.Date(2024, 1, 4, 4, 0, 0, 0, time.UTC),
},
Regex: "gem-(\\d+)-git-([a-f0-9]{12})",
CreatedAtSort: true,
Versions: []string{"gem-182-git-6bd8a5e1a2b3", "gem-1337-git-4bd8a5e1a244", "gem-1338-git-4bd8a5e1a244"},
},
),
Entry("regex override semver constraint",
@@ -888,8 +926,9 @@ var _ = DescribeTable("tracking semver tags",
ImageName: "random-5",
},
},
Regex: "^[0-9a-f]{7}-dev$",
Versions: []string{"3bd8a5e-dev", "67e3c33-dev"},
Regex: "^[0-9a-f]{7}-dev$",
CreatedAtSort: false,
Versions: []string{"3bd8a5e-dev", "67e3c33-dev"},
},
),
Entry("semver tag ordering",
@@ -1488,12 +1527,14 @@ type testTag struct {
}

type SemverOrRegexTagCheckExample struct {
Tags []testTag
Tags []testTag
TagsToTime map[string]time.Time

PreReleases bool
Variant string

Regex string
Regex string
CreatedAtSort bool

SemverConstraint string

@@ -1534,6 +1575,7 @@ func (example SemverOrRegexTagCheckExample) Run() {
Variant: example.Variant,
SemverConstraint: example.SemverConstraint,
Regex: example.Regex,
CreatedAtSort: example.CreatedAtSort,
},
}

@@ -1612,6 +1654,45 @@ func (example SemverOrRegexTagCheckExample) Run() {
)
}

// if SortByCreatedAt is set, we need to return the created date for each tag when the manifest is requested
if example.CreatedAtSort {
manifestRef, err := image.Manifest()
Expect(err).ToNot(HaveOccurred())
// Mutate ConfigFile such that created at is set to the tag name
expectedTime := example.TagsToTime[tag.Tag]
config, err := image.ConfigFile()
Expect(err).ToNot(HaveOccurred())
config.Created = v1.Time{Time: expectedTime}
configBytes, err := json.Marshal(config)
Expect(err).ToNot(HaveOccurred())

// Take the SHA256 of config and set to mutatedManifest object
configHash := sha256.Sum256(configBytes)
Expect(err).ToNot(HaveOccurred())
manifestRef.Config.Digest = v1.Hash{Algorithm: "sha256", Hex: hex.EncodeToString(configHash[:])}
manifestDigest := manifestRef.Config.Digest
mutatedManifest, err := json.Marshal(manifestRef)
Expect(err).ToNot(HaveOccurred())

registryServer.RouteToHandler(
"GET",
"/v2/"+repo.RepositoryStr()+"/manifests/"+tag.Tag,
ghttp.RespondWith(http.StatusOK, mutatedManifest, http.Header{
"Content-Type": {string(mediaType)},
"Content-Length": {strconv.Itoa(len(mutatedManifest))},
"Docker-Content-Digest": {digest.String()},
}),
)

registryServer.RouteToHandler(
"GET",
"/v2/"+repo.RepositoryStr()+"/blobs/"+manifestDigest.String(),
ghttp.RespondWith(http.StatusOK, configBytes, http.Header{
"Content-Length": {strconv.Itoa(len(configBytes))},
}),
)
}

tagVersions[tag.Tag] = resource.Version{
Tag: tag.Tag,
Digest: digest.String(),
24 changes: 24 additions & 0 deletions commands/check.go
Original file line number Diff line number Diff line change
@@ -8,6 +8,7 @@ import (
"regexp"
"sort"
"strings"
"time"

"github.com/Masterminds/semver/v3"
resource "github.com/concourse/registry-image-resource"
@@ -280,6 +281,7 @@ func checkRepositoryRegex(repo name.Repository, source resource.Source, from *re
}

tagDigests := map[string]string{}
tagToTimeDigests := map[string]time.Time{}
matchedTags := make([]string, 0)

for _, identifier := range tags {
@@ -300,11 +302,33 @@ func checkRepositoryRegex(repo name.Repository, source resource.Source, from *re
continue
}

if source.CreatedAtSort {
// Call Get to get the Image and History of the tag
img, err := remote.Image(tagRef, opts...)
if err != nil {
return resource.CheckResponse{}, fmt.Errorf("get remote image: %w", err)
}

// This calls /blobs/sha256:<digest> to get the config file
configFile, err := img.ConfigFile()
if err != nil {
return resource.CheckResponse{}, fmt.Errorf("get remote image config file: %w", err)
}
tagToTimeDigests[identifier] = configFile.Created.Time
}

matchedTags = append(matchedTags, identifier)

tagDigests[identifier] = digest.String()
}

// If CreatedAtSort is true, sort the matchedTags in descending order by looking up Time in tagToTimeDigests
if source.CreatedAtSort {
sort.Slice(matchedTags, func(i, j int) bool {
return tagToTimeDigests[matchedTags[i]].Before(tagToTimeDigests[matchedTags[j]])
})
}

response := resource.CheckResponse{}

// Using matchedTags here maintains the order of the response to the list tags call
3 changes: 2 additions & 1 deletion types.go
Original file line number Diff line number Diff line change
@@ -95,7 +95,8 @@ type Source struct {

Tag Tag `json:"tag,omitempty"`

Regex string `json:"tag_regex,omitempty"`
Regex string `json:"tag_regex,omitempty"`
CreatedAtSort bool `json:"created_at_sort,omitempty"`

BasicCredentials
AwsCredentials