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

Hub resolver: add version constraints #7257

Merged
merged 1 commit into from
Oct 24, 2023
Merged
Show file tree
Hide file tree
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
14 changes: 5 additions & 9 deletions cmd/resolvers/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ import (

func main() {
ctx := filteredinformerfactory.WithSelectors(signals.NewContext(), v1alpha1.ManagedByLabelKey)
tektonHubURL := buildHubURL(os.Getenv("TEKTON_HUB_API"), "", hub.TektonHubYamlEndpoint)
artifactHubURL := buildHubURL(os.Getenv("ARTIFACT_HUB_API"), hub.DefaultArtifactHubURL, hub.ArtifactHubYamlEndpoint)
tektonHubURL := buildHubURL(os.Getenv("TEKTON_HUB_API"), "")
artifactHubURL := buildHubURL(os.Getenv("ARTIFACT_HUB_API"), hub.DefaultArtifactHubURL)

sharedmain.MainWithContext(ctx, "controller",
framework.NewController(ctx, &git.Resolver{}),
Expand All @@ -43,16 +43,12 @@ func main() {
framework.NewController(ctx, &cluster.Resolver{}))
}

func buildHubURL(configAPI, defaultURL, yamlEndpoint string) string {
func buildHubURL(configAPI, defaultURL string) string {
var hubURL string
if configAPI == "" {
hubURL = defaultURL
} else {
if !strings.HasSuffix(configAPI, "/") {
configAPI += "/"
}
hubURL = configAPI + yamlEndpoint
hubURL = configAPI
}

return hubURL
return strings.TrimSuffix(hubURL, "/")
}
40 changes: 40 additions & 0 deletions cmd/resolvers/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package main

import "testing"

func TestBuildHubURL(t *testing.T) {
testCases := []struct {
name string
configAPI string
defaultURL string
expected string
}{
{
name: "configAPI empty",
configAPI: "",
defaultURL: "https://tekton.dev",
expected: "https://tekton.dev",
},
{
name: "configAPI not empty",
configAPI: "https://myhub.com",
defaultURL: "https://foo.com",
expected: "https://myhub.com",
},
{
name: "defaultURL ends with slash",
configAPI: "",
defaultURL: "https://bar.com/",
expected: "https://bar.com",
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
actual := buildHubURL(tc.configAPI, tc.defaultURL)
if actual != tc.expected {
t.Errorf("expected %s, but got %s", tc.expected, actual)
}
})
}
}
34 changes: 33 additions & 1 deletion docs/hub-resolver.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Use resolver type `hub`.
| `type` | The type of Hub from where to pull the resource (Optional). Either `artifact` or `tekton` | Default: `artifact` |
| `kind` | Either `task` or `pipeline` (Optional) | Default: `task` |
| `name` | The name of the task or pipeline to fetch from the hub | `golang-build` |
| `version` | Version of task or pipeline to pull in from hub. Wrap the number in quotes! | `"0.5.0"` |
| `version` | Version or a Constraint (see [below](#version-constraint) of a task or a pipeline to pull in from. Wrap the number in quotes! | `"0.5.0"`, `">= 0.5.0"` |

The Catalogs in the Artifact Hub follows the semVer (i.e.` <major-version>.<minor-version>.0`) and the Catalogs in the Tekton Hub follows the simplified semVer (i.e. `<major-version>.<minor-version>`). Both full and simplified semantic versioning will be accepted by the `version` parameter. The Hub Resolver will map the version to the format expected by the target Hub `type`.

Expand Down Expand Up @@ -126,6 +126,38 @@ spec:
# overall will not succeed without those parameters.
```

### Version constraint

Instead of a version you can specify a constraint to choose from. The constraint is a string as documented in the [go-version](https://github.com/hashicorp/go-version) library.

Some examples:

```yaml
params:
- name: name
value: git-clone
- name: version
value: ">=0.7.0"
```

Will only choose the git-clone task that is greater than version `0.7.0`

```yaml
params:
- name: name
value: git-clone
- name: version
value: ">=0.7.0, < 2.0.0"
```

Will select the **latest** git-clone task that is greater than version `0.7.0` and
less than version `2.0.0`, so if the latest task is the version `0.9.0` it will
be selected.

Other operators for selection are available for comparisons, see the
[go-version](https://github.com/hashicorp/go-version/blob/644291d14038339745c2d883a1a114488e30b702/constraint.go#L40C2-L48)
source code.

---

Except as otherwise noted, the content of this page is licensed under the
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ require (
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/hashicorp/go-version v1.6.0 // indirect
github.com/hashicorp/go-version v1.6.0
github.com/imdario/mergo v0.3.13 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
Expand Down
8 changes: 7 additions & 1 deletion pkg/resolution/resolver/hub/params.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,20 @@ limitations under the License.
package hub

// DefaultArtifactHubURL is the default url for the Artifact hub api
const DefaultArtifactHubURL = "https://artifacthub.io/api/v1/packages/tekton-%s/%s/%s/%s"
const DefaultArtifactHubURL = "https://artifacthub.io"

// TektonHubYamlEndpoint is the suffix for a private custom Tekton hub instance
const TektonHubYamlEndpoint = "v1/resource/%s/%s/%s/%s/yaml"

// DefaultTektonHubListTasksEndpoint
const TektonHubListTasksEndpoint = "v1/resource/%s/%s/%s"

// ArtifactHubYamlEndpoint is the suffix for a private custom Artifact hub instance
const ArtifactHubYamlEndpoint = "api/v1/packages/tekton-%s/%s/%s/%s"

// ArtifactHubListTasksEndpoint
const ArtifactHubListTasksEndpoint = "api/v1/packages/tekton-%s/%s/%s"

// ParamName is the parameter defining what the layer name in the bundle
// image is.
const ParamName = "name"
Expand Down
98 changes: 95 additions & 3 deletions pkg/resolution/resolver/hub/resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"net/http"
"strings"

goversion "github.com/hashicorp/go-version"
resolverconfig "github.com/tektoncd/pipeline/pkg/apis/config/resolver"
pipelinev1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1"
"github.com/tektoncd/pipeline/pkg/resolution/common"
Expand Down Expand Up @@ -121,6 +122,14 @@ func (r *Resolver) Resolve(ctx context.Context, params []pipelinev1.Param) (fram
return nil, fmt.Errorf("failed to validate params: %w", err)
}

if constraint, err := goversion.NewConstraint(paramsMap[ParamVersion]); err == nil {
chosen, err := r.resolveVersionConstraint(ctx, paramsMap, constraint)
if err != nil {
return nil, err
}
paramsMap[ParamVersion] = chosen.String()
}

resVer, err := resolveVersion(paramsMap[ParamVersion], paramsMap[ParamType])
if err != nil {
return nil, err
Expand All @@ -130,7 +139,8 @@ func (r *Resolver) Resolve(ctx context.Context, params []pipelinev1.Param) (fram
// call hub API
switch paramsMap[ParamType] {
case ArtifactHubType:
url := fmt.Sprintf(r.ArtifactHubURL, paramsMap[ParamKind], paramsMap[ParamCatalog], paramsMap[ParamName], paramsMap[ParamVersion])
url := fmt.Sprintf(fmt.Sprintf("%s/%s", r.ArtifactHubURL, ArtifactHubYamlEndpoint),
paramsMap[ParamKind], paramsMap[ParamCatalog], paramsMap[ParamName], paramsMap[ParamVersion])
resp := artifactHubResponse{}
if err := fetchHubResource(ctx, url, &resp); err != nil {
return nil, fmt.Errorf("fail to fetch Artifact Hub resource: %w", err)
Expand All @@ -140,7 +150,8 @@ func (r *Resolver) Resolve(ctx context.Context, params []pipelinev1.Param) (fram
Content: []byte(resp.Data.YAML),
}, nil
case TektonHubType:
url := fmt.Sprintf(r.TektonHubURL, paramsMap[ParamCatalog], paramsMap[ParamKind], paramsMap[ParamName], paramsMap[ParamVersion])
url := fmt.Sprintf(fmt.Sprintf("%s/%s", r.TektonHubURL, TektonHubYamlEndpoint),
paramsMap[ParamCatalog], paramsMap[ParamKind], paramsMap[ParamName], paramsMap[ParamVersion])
resp := tektonHubResponse{}
if err := fetchHubResource(ctx, url, &resp); err != nil {
return nil, fmt.Errorf("fail to fetch Tekton Hub resource: %w", err)
Expand Down Expand Up @@ -218,7 +229,6 @@ func fetchHubResource(ctx context.Context, apiEndpoint string, v interface{}) er
if err != nil {
return fmt.Errorf("error unmarshalling json response: %w", err)
}

return nil
}

Expand Down Expand Up @@ -256,6 +266,88 @@ func resolveCatalogName(paramsMap, conf map[string]string) (string, error) {
return paramsMap[ParamCatalog], nil
}

type artifactHubavailableVersionsResults struct {
Version string `json:"version"`
Prerelease bool `json:"prerelease"`
}

type artifactHubListResult struct {
AvailableVersions []artifactHubavailableVersionsResults `json:"available_versions"`
Version string `json:"version"`
}

type tektonHubListResultVersion struct {
Version string `json:"version"`
}

type tektonHubListDataResult struct {
Versions []tektonHubListResultVersion `json:"versions"`
}

type tektonHubListResult struct {
Data tektonHubListDataResult `json:"data"`
}

func (r *Resolver) resolveVersionConstraint(ctx context.Context, paramsMap map[string]string, constraint goversion.Constraints) (*goversion.Version, error) {
var ret *goversion.Version
if paramsMap[ParamType] == ArtifactHubType {
allVersionsURL := fmt.Sprintf("%s/%s", r.ArtifactHubURL, fmt.Sprintf(
ArtifactHubListTasksEndpoint,
paramsMap[ParamKind], paramsMap[ParamCatalog], paramsMap[ParamName]))
resp := artifactHubListResult{}
if err := fetchHubResource(ctx, allVersionsURL, &resp); err != nil {
return nil, fmt.Errorf("fail to fetch Artifact Hub resource: %w", err)
}
for _, vers := range resp.AvailableVersions {
if vers.Prerelease {
continue
}
checkV, err := goversion.NewVersion(vers.Version)
if err != nil {
return nil, fmt.Errorf("fail to parse version %s from %s: %w", ArtifactHubType, vers.Version, err)
}
if checkV == nil {
continue
}
if constraint.Check(checkV) {
if ret != nil && ret.GreaterThan(checkV) {
continue
}
// TODO(chmouel): log constraint result in controller
ret = checkV
}
}
} else if paramsMap[ParamType] == TektonHubType {
allVersionsURL := fmt.Sprintf("%s/%s", r.TektonHubURL,
fmt.Sprintf(TektonHubListTasksEndpoint,
paramsMap[ParamCatalog], paramsMap[ParamKind], paramsMap[ParamName]))
resp := tektonHubListResult{}
if err := fetchHubResource(ctx, allVersionsURL, &resp); err != nil {
return nil, fmt.Errorf("fail to fetch Tekton Hub resource: %w", err)
}
for _, vers := range resp.Data.Versions {
checkV, err := goversion.NewVersion(vers.Version)
if err != nil {
return nil, fmt.Errorf("fail to parse version %s from %s: %w", TektonHubType, vers, err)
}
if checkV == nil {
continue
}
if constraint.Check(checkV) {
if ret != nil && ret.GreaterThan(checkV) {
continue
}
// TODO(chmouel): log constraint result in controller
ret = checkV
}
}
}
if ret == nil {
return nil, fmt.Errorf("no version found for constraint %s", paramsMap[ParamVersion])
}
return ret, nil
}

// the Artifact Hub follows the semVer (i.e. <major-version>.<minor-version>.0)
// the Tekton Hub follows the simplified semVer (i.e. <major-version>.<minor-version>)
// for resolution request with "artifact" type, we append ".0" suffix if the input version is simplified semVer
Expand Down
Loading
Loading