Skip to content

Commit

Permalink
[TT-1218] add 3 new tiny tools for the compatibility pipeline (#987)
Browse files Browse the repository at this point in the history
  • Loading branch information
Tofel committed Jun 12, 2024
1 parent 8fb5196 commit 5607dff
Show file tree
Hide file tree
Showing 25 changed files with 1,141 additions and 9 deletions.
6 changes: 6 additions & 0 deletions .github/workflows/lint.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@ jobs:
path: ./tools/gotestloghelper/
- name: k8s-test-runner
path: ./k8s-test-runner/
- name: testlistgenerator
path: ./tools/testlistgenerator/
- name: ecrimagefetcher
path: ./tools/ecrimagefetcher/
- name: ghlatestreleasechecker
path: ./tools/ghlatestreleasechecker/
steps:
- name: Check out Code
uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
Expand Down
8 changes: 7 additions & 1 deletion .github/workflows/release-tools.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,13 @@ jobs:
tools:
strategy:
matrix:
tool: [tools/gotestloghelper]
tool:
[
tools/gotestloghelper,
tools/testlistgenerator,
tools/ecrimagefetcher,
tools/ghlatestreleasechecker,
]
name: Release ${{ matrix.tool }}
runs-on: ubuntu-latest
steps:
Expand Down
6 changes: 6 additions & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ jobs:
path: ./
- name: gotestloghelper
path: ./tools/gotestloghelper/
- name: testlistgenerator
path: ./tools/testlistgenerator/
- name: ecrimagefetcher
path: ./tools/ecrimagefetcher/
- name: ghlatestreleasechecker
path: ./tools/ghlatestreleasechecker/
runs-on: ubuntu-latest
name: ${{ matrix.project.name }} unit tests
steps:
Expand Down
21 changes: 13 additions & 8 deletions concurrency/executor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,21 +142,29 @@ func TestExecute(t *testing.T) {
func TestExecuteFailFast(t *testing.T) {

type tc struct {
name string
executor *concurrency.ConcurrentExecutor[int, result, config]
failFast bool
name string
executor *concurrency.ConcurrentExecutor[int, result, config]
processorFn func(resultCh chan result, errCh chan error, keyNum int, payload config)
failFast bool
}

tcs := []tc{
{
name: "fail fast enabled",
executor: concurrency.NewConcurrentExecutor[int, result, config](logging.GetTestLogger(t)),
failFast: true,
processorFn: func(resultCh chan result, errCh chan error, keyNum int, payload config) {
time.Sleep(10 * time.Millisecond)
errCh <- errors.New("always fail, fail fast enabled")
},
},
{
name: "fail fast disabled",
executor: concurrency.NewConcurrentExecutor(logging.GetTestLogger(t), concurrency.WithoutFailFast[int, result, config]()),
failFast: false,
processorFn: func(resultCh chan result, errCh chan error, keyNum int, payload config) {
errCh <- errors.New("always fail, fail fast disabled")
},
},
}

Expand All @@ -165,21 +173,18 @@ func TestExecuteFailFast(t *testing.T) {
tc := tc
t.Parallel()

processorFn := func(resultCh chan result, errCh chan error, keyNum int, payload config) {
errCh <- errors.New("always fail")
}

expectedExecutions := 1000

configs := []config{}
for i := 0; i < expectedExecutions; i++ {
configs = append(configs, struct{}{})
}

results, err := tc.executor.Execute(expectedExecutions, configs, processorFn)
results, err := tc.executor.Execute(100, configs, tc.processorFn)
require.Error(t, err, "No error returned when executing concurrently")
require.Len(t, results, 0, "Expected no results")
if tc.failFast {
fmt.Println(len(tc.executor.GetErrors()))
require.Less(t, len(tc.executor.GetErrors()), expectedExecutions, "With fail fast enabled not all tasks should be executed")
} else {
require.Equal(t, len(tc.executor.GetErrors()), expectedExecutions, "With fail fast disabled all tasks should be executed")
Expand Down
5 changes: 5 additions & 0 deletions tools/ecrimagefetcher/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
lint:
golangci-lint --color=always run ./... --fix -v

test_unit:
go test -timeout 5m -json -cover -covermode=count -coverprofile=unit-test-coverage.out ./... 2>&1 | tee /tmp/gotest.log
61 changes: 61 additions & 0 deletions tools/ecrimagefetcher/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# AWS ECR Image Fetcher

This Go script fetches the latest image tags from an AWS ECR repository based on specified criteria such as a grep string and optional semantic version constraints. It sorts and filters the tags and returns a specified number of the latest ones (according to semantic versioning).

## Usage

```bash
go run main.go <repository_name> <grep_string> <count> [<semver_constraints>]
```

### Example

```bash
go run main.go 'my-repo' '^v[0-9]+\.[0-9]+\.[0-9]+$' 5 '>=1.0.0, <2.0.0'
```

## Command Line Arguments

- `<repository_name>`: The name of the ECR repository.
- `<grep_string>`: A regex string to filter the tags.
- `<count>`: The number of latest tags to return.
- `[<semver_constraints>]`: Optional semantic version constraints to filter tags further.

## Output

- The script prints the specified number of latest image tags from the repository that match the given criteria, formatted as `repository_name:tag`, e.g.`hyperledger/besu:24.3,hyperledger/besu:24.2`.
- If the number of matching tags is less than the requested count, a warning is printed to stderr, and the available tags are returned.

## Error Handling

The script will panic and display error messages in the following scenarios:

- Insufficient command line arguments.
- Empty or invalid `repository_name`, `grep_string`, or `count`.
- Invalid regex for `grep_string`.
- Invalid integer value for `count`.
- Invalid semantic version constraints.
- Errors in fetching or parsing the image details from AWS ECR.

## Detailed Steps

1. **Argument Parsing and Validation**:
- The script checks that at least 4 command line arguments are provided.
- Validates the format and values of `repository_name`, `grep_string`, and `count`.
- Optionally validates semantic version constraints if provided.
2. **Fetch Image Details**:
- Constructs and executes an AWS CLI command to describe images in the specified ECR repository.
- Parses the JSON output to extract image tags.
3. **Filter and Sort Tags**:
- Filters tags based on the `grep_string` regex.
- Optionally filters tags based on semantic version constraints.
- Sorts the tags in descending order.
4. **Return Latest Tags**:
- Constructs the output of the latest tags formatted as `repository_name:tag`.
- Prints the result to stdout.

## Notes

- Ensure AWS CLI is configured and you have the necessary permissions to access the ECR repository.
- The environment variable `AWS_REGION` must be set to the appropriate AWS region.
- Semantic version constraints are optional and can be used to further filter the tags.
16 changes: 16 additions & 0 deletions tools/ecrimagefetcher/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
module github.com/smartcontractkit/chainlink-testing-framework/tools/latestecrimages

go 1.21.7

require (
github.com/Masterminds/semver/v3 v3.2.1
github.com/itchyny/gojq v0.12.16
github.com/stretchr/testify v1.9.0
)

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/itchyny/timefmt-go v0.1.6 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
16 changes: 16 additions & 0 deletions tools/ecrimagefetcher/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/itchyny/gojq v0.12.16 h1:yLfgLxhIr/6sJNVmYfQjTIv0jGctu6/DgDoivmxTr7g=
github.com/itchyny/gojq v0.12.16/go.mod h1:6abHbdC2uB9ogMS38XsErnfqJ94UlngIJGlRAIj4jTM=
github.com/itchyny/timefmt-go v0.1.6 h1:ia3s54iciXDdzWzwaVKXZPbiXzxxnv1SPGFfM/myJ5Q=
github.com/itchyny/timefmt-go v0.1.6/go.mod h1:RRDZYC5s9ErkjQvTvvU7keJjxUYzIISJGxm9/mAERQg=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
176 changes: 176 additions & 0 deletions tools/ecrimagefetcher/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
package main

import (
"encoding/json"
"errors"
"fmt"
"os"
"os/exec"
"regexp"
"sort"
"strconv"
"strings"

"github.com/Masterminds/semver/v3"

"github.com/itchyny/gojq"
)

func fetchImageDetails(repositoryName string) ([]byte, error) {
// #nosec G204
cmd := exec.Command("aws", "ecr", "describe-images", "--repository-name", repositoryName, "--region", os.Getenv("AWS_REGION"), "--output", "json", "--query", "imageDetails[?imageTags!=`null` && imageTags!=`[]`]")
return cmd.Output()
}

func parseImageTags(output []byte, grepString string, constraints *semver.Constraints) ([]string, error) {
var imageDetails []interface{}
if err := json.Unmarshal(output, &imageDetails); err != nil {
return nil, fmt.Errorf("failed to unmarshal JSON: %w", err)
}

query, err := gojq.Parse(".[] | .imageTags[0]")
if err != nil {
return nil, fmt.Errorf("failed to parse gojq query: %w", err)
}

var tags []string
iter := query.Run(imageDetails)
for {
tag, ok := iter.Next()
if !ok {
break
}
if tagStr, ok := tag.(string); ok {
tags = append(tags, tagStr)
} else if err, ok := tag.(error); ok {
return nil, fmt.Errorf("failed to run gojq query: %w", err)
}
}

sort.Sort(sort.Reverse(sort.StringSlice(tags)))

re, err := regexp.Compile(grepString)
if err != nil {
return nil, fmt.Errorf("failed to compile regex: %w", err)
}

var filteredTags []string
for _, tag := range tags {
if re.MatchString(tag) {
ignore := false
if constraints != nil {
version, err := semver.NewVersion(tag)
if err != nil {
return nil, fmt.Errorf("failed to parse version: %w", err)
}
if !constraints.Check(version) {
ignore = true
}
}

if !ignore {
filteredTags = append(filteredTags, tag)
}
}
}

return filteredTags, nil
}

func getLatestImages(fetchFunc func(string) ([]byte, error), repositoryName, grepString string, count int, constraints *semver.Constraints) (string, error) {
output, err := fetchFunc(repositoryName)
if err != nil {
return "", fmt.Errorf("failed to describe images: %w", err)
}

tags, err := parseImageTags(output, grepString, constraints)
if err != nil {
return "", fmt.Errorf("failed to parse image tags: %w", err)
}

constraintsText := "(none)"
if constraints != nil {
constraintsText = constraints.String()
}

if len(tags) == 0 {
panic(fmt.Errorf("error: no tags found for repository '%s' given the version constraints '%s'", repositoryName, constraintsText))
}

if count > len(tags) {
_, _ = fmt.Fprintf(os.Stderr, "Warning: failed to find %d tags given the version constraints '%s'. Found only %d tag(s)\n", count, constraintsText, len(tags))
count = len(tags)
}

var imagesArr []string
for i := 0; i < count; i++ {
imagesArr = append(imagesArr, fmt.Sprintf("%s:%s", repositoryName, tags[i]))
}

images := strings.Join(imagesArr, ",")
return images, nil
}

func main() {
if err := validateInputs(); err != nil {
panic(err)
}

repositoryName := os.Args[1]
grepString := os.Args[2]
count, err := strconv.Atoi(os.Args[3])
if err != nil {
panic(fmt.Errorf("error: count must be an integer, but %s is not an integer", os.Args[3]))
}

var constraints *semver.Constraints
if len(os.Args) == 5 {
constraints, err = semver.NewConstraint(os.Args[4])
if err != nil {
panic(fmt.Errorf("error: invalid semver constraint: %v", err))
}
}

images, err := getLatestImages(fetchImageDetails, repositoryName, grepString, count, constraints)
if err != nil {
panic(fmt.Errorf("error getting latest images: %v", err))
}

fmt.Println(images)
}

func validateInputs() error {
if len(os.Args) < 4 {
return errors.New("usage: <repository_name> <grep_string> <count> [<ignored_tags>]")
}

if os.Args[1] == "" {
return errors.New("error: repository_name cannot be empty")
}

if os.Args[2] == "" {
return errors.New("error: grep_string cannot be empty")
}

if _, err := regexp.Compile(os.Args[2]); err != nil {
return errors.New("error: grep_string is not a valid regex")
}

if _, err := strconv.Atoi(os.Args[3]); err != nil {
return fmt.Errorf("error: count must be an integer, but %s is not an integer", os.Args[3])
}

if len(os.Args) == 5 && os.Args[4] != "" {
for _, semVerConstraint := range strings.Split(os.Args[4], ",") {
if semVerConstraint == "" {
return errors.New("error: semver constraint cannot be empty")
}
_, err := semver.NewConstraint(semVerConstraint)
if err != nil {
return fmt.Errorf("error: invalid semver constraint: %v", err)
}
}
}

return nil
}
Loading

0 comments on commit 5607dff

Please sign in to comment.