Skip to content

Commit f0c7ed5

Browse files
chmoueltekton-robot
authored andcommitted
Hub resolver: add version constraints
This let the user specify a version constraint to choose from. Version constraint looks like this: ```yaml version: ">= 0.5.0" ``` This will only choose the tasks that are greater than 0.5.0 Additional constraint operators are available, for example: ```yaml version: ">= 0.5.0, < 2.0.0" ``` This will only choose the tasks that are greater than 0.5.0 and less than 2.0.0 Signed-off-by: Chmouel Boudjnah <[email protected]>
1 parent b958eb5 commit f0c7ed5

File tree

7 files changed

+380
-17
lines changed

7 files changed

+380
-17
lines changed

cmd/resolvers/main.go

+5-9
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@ import (
3333

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

3939
sharedmain.MainWithContext(ctx, "controller",
4040
framework.NewController(ctx, &git.Resolver{}),
@@ -43,16 +43,12 @@ func main() {
4343
framework.NewController(ctx, &cluster.Resolver{}))
4444
}
4545

46-
func buildHubURL(configAPI, defaultURL, yamlEndpoint string) string {
46+
func buildHubURL(configAPI, defaultURL string) string {
4747
var hubURL string
4848
if configAPI == "" {
4949
hubURL = defaultURL
5050
} else {
51-
if !strings.HasSuffix(configAPI, "/") {
52-
configAPI += "/"
53-
}
54-
hubURL = configAPI + yamlEndpoint
51+
hubURL = configAPI
5552
}
56-
57-
return hubURL
53+
return strings.TrimSuffix(hubURL, "/")
5854
}

cmd/resolvers/main_test.go

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package main
2+
3+
import "testing"
4+
5+
func TestBuildHubURL(t *testing.T) {
6+
testCases := []struct {
7+
name string
8+
configAPI string
9+
defaultURL string
10+
expected string
11+
}{
12+
{
13+
name: "configAPI empty",
14+
configAPI: "",
15+
defaultURL: "https://tekton.dev",
16+
expected: "https://tekton.dev",
17+
},
18+
{
19+
name: "configAPI not empty",
20+
configAPI: "https://myhub.com",
21+
defaultURL: "https://foo.com",
22+
expected: "https://myhub.com",
23+
},
24+
{
25+
name: "defaultURL ends with slash",
26+
configAPI: "",
27+
defaultURL: "https://bar.com/",
28+
expected: "https://bar.com",
29+
},
30+
}
31+
32+
for _, tc := range testCases {
33+
t.Run(tc.name, func(t *testing.T) {
34+
actual := buildHubURL(tc.configAPI, tc.defaultURL)
35+
if actual != tc.expected {
36+
t.Errorf("expected %s, but got %s", tc.expected, actual)
37+
}
38+
})
39+
}
40+
}

docs/hub-resolver.md

+33-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ Use resolver type `hub`.
1717
| `type` | The type of Hub from where to pull the resource (Optional). Either `artifact` or `tekton` | Default: `artifact` |
1818
| `kind` | Either `task` or `pipeline` (Optional) | Default: `task` |
1919
| `name` | The name of the task or pipeline to fetch from the hub | `golang-build` |
20-
| `version` | Version of task or pipeline to pull in from hub. Wrap the number in quotes! | `"0.5.0"` |
20+
| `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"` |
2121

2222
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`.
2323

@@ -129,6 +129,38 @@ spec:
129129
# overall will not succeed without those parameters.
130130
```
131131

132+
### Version constraint
133+
134+
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.
135+
136+
Some examples:
137+
138+
```yaml
139+
params:
140+
- name: name
141+
value: git-clone
142+
- name: version
143+
value: ">=0.7.0"
144+
```
145+
146+
Will only choose the git-clone task that is greater than version `0.7.0`
147+
148+
```yaml
149+
params:
150+
- name: name
151+
value: git-clone
152+
- name: version
153+
value: ">=0.7.0, < 2.0.0"
154+
```
155+
156+
Will select the **latest** git-clone task that is greater than version `0.7.0` and
157+
less than version `2.0.0`, so if the latest task is the version `0.9.0` it will
158+
be selected.
159+
160+
Other operators for selection are available for comparisons, see the
161+
[go-version](https://github.com/hashicorp/go-version/blob/644291d14038339745c2d883a1a114488e30b702/constraint.go#L40C2-L48)
162+
source code.
163+
132164
---
133165

134166
Except as otherwise noted, the content of this page is licensed under the

pkg/resolution/resolver/hub/params.go

+7-1
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,20 @@ limitations under the License.
1414
package hub
1515

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

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

22+
// DefaultTektonHubListTasksEndpoint
23+
const TektonHubListTasksEndpoint = "v1/resource/%s/%s/%s"
24+
2225
// ArtifactHubYamlEndpoint is the suffix for a private custom Artifact hub instance
2326
const ArtifactHubYamlEndpoint = "api/v1/packages/tekton-%s/%s/%s/%s"
2427

28+
// ArtifactHubListTasksEndpoint
29+
const ArtifactHubListTasksEndpoint = "api/v1/packages/tekton-%s/%s/%s"
30+
2531
// ParamName is the parameter defining what the layer name in the bundle
2632
// image is.
2733
const ParamName = "name"

pkg/resolution/resolver/hub/resolver.go

+95-3
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"net/http"
2525
"strings"
2626

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

125+
if constraint, err := goversion.NewConstraint(paramsMap[ParamVersion]); err == nil {
126+
chosen, err := r.resolveVersionConstraint(ctx, paramsMap, constraint)
127+
if err != nil {
128+
return nil, err
129+
}
130+
paramsMap[ParamVersion] = chosen.String()
131+
}
132+
124133
resVer, err := resolveVersion(paramsMap[ParamVersion], paramsMap[ParamType])
125134
if err != nil {
126135
return nil, err
@@ -130,7 +139,8 @@ func (r *Resolver) Resolve(ctx context.Context, params []pipelinev1.Param) (fram
130139
// call hub API
131140
switch paramsMap[ParamType] {
132141
case ArtifactHubType:
133-
url := fmt.Sprintf(r.ArtifactHubURL, paramsMap[ParamKind], paramsMap[ParamCatalog], paramsMap[ParamName], paramsMap[ParamVersion])
142+
url := fmt.Sprintf(fmt.Sprintf("%s/%s", r.ArtifactHubURL, ArtifactHubYamlEndpoint),
143+
paramsMap[ParamKind], paramsMap[ParamCatalog], paramsMap[ParamName], paramsMap[ParamVersion])
134144
resp := artifactHubResponse{}
135145
if err := fetchHubResource(ctx, url, &resp); err != nil {
136146
return nil, fmt.Errorf("fail to fetch Artifact Hub resource: %w", err)
@@ -140,7 +150,8 @@ func (r *Resolver) Resolve(ctx context.Context, params []pipelinev1.Param) (fram
140150
Content: []byte(resp.Data.YAML),
141151
}, nil
142152
case TektonHubType:
143-
url := fmt.Sprintf(r.TektonHubURL, paramsMap[ParamCatalog], paramsMap[ParamKind], paramsMap[ParamName], paramsMap[ParamVersion])
153+
url := fmt.Sprintf(fmt.Sprintf("%s/%s", r.TektonHubURL, TektonHubYamlEndpoint),
154+
paramsMap[ParamCatalog], paramsMap[ParamKind], paramsMap[ParamName], paramsMap[ParamVersion])
144155
resp := tektonHubResponse{}
145156
if err := fetchHubResource(ctx, url, &resp); err != nil {
146157
return nil, fmt.Errorf("fail to fetch Tekton Hub resource: %w", err)
@@ -218,7 +229,6 @@ func fetchHubResource(ctx context.Context, apiEndpoint string, v interface{}) er
218229
if err != nil {
219230
return fmt.Errorf("error unmarshalling json response: %w", err)
220231
}
221-
222232
return nil
223233
}
224234

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

269+
type artifactHubavailableVersionsResults struct {
270+
Version string `json:"version"`
271+
Prerelease bool `json:"prerelease"`
272+
}
273+
274+
type artifactHubListResult struct {
275+
AvailableVersions []artifactHubavailableVersionsResults `json:"available_versions"`
276+
Version string `json:"version"`
277+
}
278+
279+
type tektonHubListResultVersion struct {
280+
Version string `json:"version"`
281+
}
282+
283+
type tektonHubListDataResult struct {
284+
Versions []tektonHubListResultVersion `json:"versions"`
285+
}
286+
287+
type tektonHubListResult struct {
288+
Data tektonHubListDataResult `json:"data"`
289+
}
290+
291+
func (r *Resolver) resolveVersionConstraint(ctx context.Context, paramsMap map[string]string, constraint goversion.Constraints) (*goversion.Version, error) {
292+
var ret *goversion.Version
293+
if paramsMap[ParamType] == ArtifactHubType {
294+
allVersionsURL := fmt.Sprintf("%s/%s", r.ArtifactHubURL, fmt.Sprintf(
295+
ArtifactHubListTasksEndpoint,
296+
paramsMap[ParamKind], paramsMap[ParamCatalog], paramsMap[ParamName]))
297+
resp := artifactHubListResult{}
298+
if err := fetchHubResource(ctx, allVersionsURL, &resp); err != nil {
299+
return nil, fmt.Errorf("fail to fetch Artifact Hub resource: %w", err)
300+
}
301+
for _, vers := range resp.AvailableVersions {
302+
if vers.Prerelease {
303+
continue
304+
}
305+
checkV, err := goversion.NewVersion(vers.Version)
306+
if err != nil {
307+
return nil, fmt.Errorf("fail to parse version %s from %s: %w", ArtifactHubType, vers.Version, err)
308+
}
309+
if checkV == nil {
310+
continue
311+
}
312+
if constraint.Check(checkV) {
313+
if ret != nil && ret.GreaterThan(checkV) {
314+
continue
315+
}
316+
// TODO(chmouel): log constraint result in controller
317+
ret = checkV
318+
}
319+
}
320+
} else if paramsMap[ParamType] == TektonHubType {
321+
allVersionsURL := fmt.Sprintf("%s/%s", r.TektonHubURL,
322+
fmt.Sprintf(TektonHubListTasksEndpoint,
323+
paramsMap[ParamCatalog], paramsMap[ParamKind], paramsMap[ParamName]))
324+
resp := tektonHubListResult{}
325+
if err := fetchHubResource(ctx, allVersionsURL, &resp); err != nil {
326+
return nil, fmt.Errorf("fail to fetch Tekton Hub resource: %w", err)
327+
}
328+
for _, vers := range resp.Data.Versions {
329+
checkV, err := goversion.NewVersion(vers.Version)
330+
if err != nil {
331+
return nil, fmt.Errorf("fail to parse version %s from %s: %w", TektonHubType, vers, err)
332+
}
333+
if checkV == nil {
334+
continue
335+
}
336+
if constraint.Check(checkV) {
337+
if ret != nil && ret.GreaterThan(checkV) {
338+
continue
339+
}
340+
// TODO(chmouel): log constraint result in controller
341+
ret = checkV
342+
}
343+
}
344+
}
345+
if ret == nil {
346+
return nil, fmt.Errorf("no version found for constraint %s", paramsMap[ParamVersion])
347+
}
348+
return ret, nil
349+
}
350+
259351
// the Artifact Hub follows the semVer (i.e. <major-version>.<minor-version>.0)
260352
// the Tekton Hub follows the simplified semVer (i.e. <major-version>.<minor-version>)
261353
// for resolution request with "artifact" type, we append ".0" suffix if the input version is simplified semVer

0 commit comments

Comments
 (0)