diff --git a/.gitignore b/.gitignore
index a804790..1ff899b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
testdata/.push-output
.envrc
tags
+.idea
diff --git a/README.md b/README.md
index 1966289..1623848 100644
--- a/README.md
+++ b/README.md
@@ -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:
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
-
Note if used, this will override all Semver constraints and features
+
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`.
+
variant (Optional) |
@@ -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
diff --git a/check_test.go b/check_test.go
index 92102bb..b0fd04c 100644
--- a/check_test.go
+++ b/check_test.go
@@ -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(),
diff --git a/commands/check.go b/commands/check.go
index 2ce27c6..67f8ce0 100644
--- a/commands/check.go
+++ b/commands/check.go
@@ -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: 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
diff --git a/types.go b/types.go
index 350fd44..5177130 100644
--- a/types.go
+++ b/types.go
@@ -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
|