Skip to content

Commit e9fe4a9

Browse files
committed
Add credentials to HTTP resolver
This adds the ability to pass credentials to the HTTP resolver when fetching the URL. This let's for example to fetch tasks from SCM private repositories on other SCM providers than configured with the git resolver. Fixes #7296 Signed-off-by: Chmouel Boudjnah <chmouel@redhat.com>
1 parent 7069889 commit e9fe4a9

File tree

7 files changed

+427
-27
lines changed

7 files changed

+427
-27
lines changed

docs/git-resolver.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,10 @@ Note that not all `go-scm` implementations have been tested with the `git` resol
112112
* BitBucket Server
113113
* BitBucket Cloud
114114

115+
Fetching from multiple Git providers with different configuration is not
116+
supported. You can use the [http resolver](./http-resolver.md) to fetch URL
117+
from another provider with different credentials.
118+
115119
#### Task Resolution
116120

117121
```yaml

docs/http-resolver.md

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,12 @@ This resolver responds to type `http`.
1111

1212
## Parameters
1313

14-
| Param Name | Description | Example Value |
15-
|------------------|-------------------------------------------------------------------------------|------------------------------------------------------------|
16-
| `url` | The URL to fetch from | https://raw.githubusercontent.com/tektoncd-catalog/git-clone/main/task/git-clone/git-clone.yaml |
14+
| Param Name | Description | Example Value | |
15+
|----------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------|---|
16+
| `url` | The URL to fetch from | https://raw.githubusercontent.com/tektoncd-catalog/git-clone/main/task/git-clone/git-clone.yaml | |
17+
| `http-username` | An optional username when fetching a task with credentials (need to be used in conjunction with `http-password-secret`) | `git` | |
18+
| `http-password-secret` | An optional secret in the PipelineRun directory with a reference to a password when fetching a task with credentials (need to be used in conjunction with `http-username`) | `http-password` | |
19+
| `http-password-secret-key` | An optional key in the `http-password-secret` to be used when fetching a task with credentials | Default: `password` | |
1720

1821
A valid URL must be provided. Only HTTP or HTTPS URLs are supported.
1922

@@ -54,6 +57,27 @@ spec:
5457
value: https://raw.githubusercontent.com/tektoncd-catalog/git-clone/main/task/git-clone/git-clone.yaml
5558
```
5659
60+
### Task Resolution with Basic Auth
61+
62+
```yaml
63+
apiVersion: tekton.dev/v1beta1
64+
kind: TaskRun
65+
metadata:
66+
name: remote-task-reference
67+
spec:
68+
taskRef:
69+
resolver: http
70+
params:
71+
- name: url
72+
value: https://raw.githubusercontent.com/owner/private-repo/main/task/task.yaml
73+
- name: http-username
74+
value: git
75+
- name: http-password-secret
76+
value: git-secret
77+
- name: http-password-secret-key
78+
value: git-token
79+
```
80+
5781
### Pipeline Resolution
5882
5983
```yaml
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
---
2+
apiVersion: tekton.dev/v1
3+
kind: PipelineRun
4+
metadata:
5+
generateName: http-resolver-
6+
spec:
7+
pipelineSpec:
8+
tasks:
9+
- name: http-resolver
10+
taskRef:
11+
resolver: http
12+
params:
13+
# This will uses a username and password to access the URL.
14+
#
15+
# http-password-secret is a Kubernetes secret containing the
16+
# password in the same namespace where this PipelineRun runs.
17+
#
18+
# In this case it's a GitHub token to be able to fetch the task from
19+
# a private GitHub repository.
20+
- name: url
21+
value: https://raw.githubusercontent.com/owner/private-repository/main/private-task.yaml
22+
- name: http-username
23+
value: git
24+
- name: http-password-secret
25+
value: my-github-token
26+
- name: http-password-secret-key
27+
value: token
28+
params:
29+
- name: ARGS
30+
value: ["version"]

pkg/resolution/resolver/framework/testing/fakecontroller.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,14 +71,14 @@ func RunResolverReconcileTest(ctx context.Context, t *testing.T, d test.Data, re
7171
err := testAssets.Controller.Reconciler.Reconcile(testAssets.Ctx, getRequestName(request))
7272
if expectedErr != nil {
7373
if err == nil {
74-
t.Fatalf("expected to get error %v, but got nothing", expectedErr)
74+
t.Fatalf("expected to get error: `%v`, but got nothing", expectedErr)
7575
}
7676
if expectedErr.Error() != err.Error() {
77-
t.Fatalf("expected to get error %v, but got %v", expectedErr, err)
77+
t.Fatalf("expected to get error `%v`, but got `%v`", expectedErr, err)
7878
}
7979
} else if err != nil {
8080
if ok, _ := controller.IsRequeueKey(err); !ok {
81-
t.Fatalf("did not expect an error, but got %v", err)
81+
t.Fatalf("did not expect an error, but got `%v`", err)
8282
}
8383
}
8484

pkg/resolution/resolver/http/params.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,15 @@ limitations under the License.
1414
package http
1515

1616
const (
17-
// urlParam is the url to fetch the task from
18-
urlParam string = "url"
17+
// urlParamType is the URL to fetch the task from
18+
urlParamType string = "url"
19+
20+
// httpBasicAuthUsername is the user name to use for basic auth
21+
httpBasicAuthUsername string = "http-username"
22+
23+
// httpBasicAuthSecret is the reference to a secret in the PipelineRun namespace to use for basic auth
24+
httpBasicAuthSecret string = "http-password-secret"
25+
26+
// httpBasicAuthSecretKey is the key in the httpBasicAuthSecret secret to use for basic auth
27+
httpBasicAuthSecretKey string = "http-password-secret-key"
1928
)

pkg/resolution/resolver/http/resolver.go

Lines changed: 85 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ package http
1616
import (
1717
"context"
1818
"crypto/sha256"
19+
"encoding/base64"
1920
"encoding/hex"
2021
"errors"
2122
"fmt"
@@ -29,6 +30,12 @@ import (
2930
pipelinev1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1"
3031
"github.com/tektoncd/pipeline/pkg/resolution/common"
3132
"github.com/tektoncd/pipeline/pkg/resolution/resolver/framework"
33+
"go.uber.org/zap"
34+
apierrors "k8s.io/apimachinery/pkg/api/errors"
35+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
36+
"k8s.io/client-go/kubernetes"
37+
kubeclient "knative.dev/pkg/client/injection/kube/client"
38+
"knative.dev/pkg/logging"
3239
)
3340

3441
const (
@@ -41,17 +48,25 @@ const (
4148
// httpResolverName The name of the resolver
4249
httpResolverName = "Http"
4350

44-
// ConfigMapName is the http resolver's config map
51+
// configMapName is the http resolver's config map
4552
configMapName = "http-resolver-config"
4653

4754
// default Timeout value when fetching http resources in seconds
4855
defaultHttpTimeoutValue = "1m"
56+
57+
// default key in the HTTP password secret
58+
defaultBasicAuthSecretKey = "password"
4959
)
5060

5161
// Resolver implements a framework.Resolver that can fetch files from an HTTP URL
52-
type Resolver struct{}
62+
type Resolver struct {
63+
kubeClient kubernetes.Interface
64+
logger *zap.SugaredLogger
65+
}
5366

54-
func (r *Resolver) Initialize(context.Context) error {
67+
func (r *Resolver) Initialize(ctx context.Context) error {
68+
r.kubeClient = kubeclient.Get(ctx)
69+
r.logger = logging.FromContext(ctx)
5570
return nil
5671
}
5772

@@ -95,7 +110,7 @@ func (r *Resolver) Resolve(ctx context.Context, oParams []pipelinev1.Param) (fra
95110
return nil, err
96111
}
97112

98-
return fetchHttpResource(ctx, params)
113+
return r.fetchHttpResource(ctx, params)
99114
}
100115

101116
func (r *Resolver) isDisabled(ctx context.Context) bool {
@@ -144,15 +159,33 @@ func populateDefaultParams(ctx context.Context, params []pipelinev1.Param) (map[
144159

145160
var missingParams []string
146161

147-
if _, ok := paramsMap[urlParam]; !ok {
148-
missingParams = append(missingParams, urlParam)
162+
if _, ok := paramsMap[urlParamType]; !ok {
163+
missingParams = append(missingParams, urlParamType)
149164
} else {
150-
u, err := url.ParseRequestURI(paramsMap[urlParam])
165+
u, err := url.ParseRequestURI(paramsMap[urlParamType])
151166
if err != nil {
152-
return nil, fmt.Errorf("cannot parse url %s: %w", paramsMap[urlParam], err)
167+
return nil, fmt.Errorf("cannot parse url %s: %w", paramsMap[urlParamType], err)
153168
}
154169
if u.Scheme != "http" && u.Scheme != "https" {
155-
return nil, fmt.Errorf("url %s is not a valid http(s) url", paramsMap[urlParam])
170+
return nil, fmt.Errorf("url %s is not a valid http(s) url", paramsMap[urlParamType])
171+
}
172+
}
173+
174+
if username, ok := paramsMap[httpBasicAuthUsername]; ok {
175+
if _, ok := paramsMap[httpBasicAuthSecret]; !ok {
176+
return nil, fmt.Errorf("missing required param %s when using %s", httpBasicAuthSecret, httpBasicAuthUsername)
177+
}
178+
if username == "" {
179+
return nil, fmt.Errorf("value %s cannot be empty", httpBasicAuthUsername)
180+
}
181+
}
182+
183+
if secret, ok := paramsMap[httpBasicAuthSecret]; ok {
184+
if _, ok := paramsMap[httpBasicAuthUsername]; !ok {
185+
return nil, fmt.Errorf("missing required param %s when using %s", httpBasicAuthUsername, httpBasicAuthSecret)
186+
}
187+
if secret == "" {
188+
return nil, fmt.Errorf("value %s cannot be empty", httpBasicAuthSecret)
156189
}
157190
}
158191

@@ -178,7 +211,7 @@ func makeHttpClient(ctx context.Context) (*http.Client, error) {
178211
}, nil
179212
}
180213

181-
func fetchHttpResource(ctx context.Context, params map[string]string) (framework.ResolvedResource, error) {
214+
func (r *Resolver) fetchHttpResource(ctx context.Context, params map[string]string) (framework.ResolvedResource, error) {
182215
var targetURL string
183216
var ok bool
184217

@@ -187,15 +220,24 @@ func fetchHttpResource(ctx context.Context, params map[string]string) (framework
187220
return nil, err
188221
}
189222

190-
if targetURL, ok = params[urlParam]; !ok {
191-
return nil, fmt.Errorf("missing required params: %s", urlParam)
223+
if targetURL, ok = params[urlParamType]; !ok {
224+
return nil, fmt.Errorf("missing required params: %s", urlParamType)
192225
}
193226

194227
req, err := http.NewRequestWithContext(ctx, http.MethodGet, targetURL, nil)
195228
if err != nil {
196229
return nil, fmt.Errorf("constructing request: %w", err)
197230
}
198231

232+
// NOTE(chmouel): We already made sure that username and secret was specified by the user
233+
if secret, ok := params[httpBasicAuthSecret]; ok && secret != "" {
234+
if encodedSecret, err := r.getBasicAuthSecret(ctx, params); err != nil {
235+
return nil, err
236+
} else {
237+
req.Header.Set("Authorization", encodedSecret)
238+
}
239+
}
240+
199241
resp, err := httpClient.Do(req)
200242
if err != nil {
201243
return nil, fmt.Errorf("error fetching URL: %w", err)
@@ -216,3 +258,34 @@ func fetchHttpResource(ctx context.Context, params map[string]string) (framework
216258
URL: targetURL,
217259
}, nil
218260
}
261+
262+
func (r *Resolver) getBasicAuthSecret(ctx context.Context, params map[string]string) (string, error) {
263+
secretName := params[httpBasicAuthSecret]
264+
userName := params[httpBasicAuthUsername]
265+
tokenSecretKey := defaultBasicAuthSecretKey
266+
if v, ok := params[httpBasicAuthSecretKey]; ok {
267+
if v != "" {
268+
tokenSecretKey = v
269+
}
270+
}
271+
secretNS := common.RequestNamespace(ctx)
272+
secret, err := r.kubeClient.CoreV1().Secrets(secretNS).Get(ctx, secretName, metav1.GetOptions{})
273+
if err != nil {
274+
if apierrors.IsNotFound(err) {
275+
notFoundErr := fmt.Errorf("cannot get API token, secret %s not found in namespace %s", secretName, secretNS)
276+
r.logger.Info(notFoundErr)
277+
return "", notFoundErr
278+
}
279+
wrappedErr := fmt.Errorf("error reading API token from secret %s in namespace %s: %w", secretName, secretNS, err)
280+
r.logger.Info(wrappedErr)
281+
return "", wrappedErr
282+
}
283+
secretVal, ok := secret.Data[tokenSecretKey]
284+
if !ok {
285+
err := fmt.Errorf("cannot get API token, key %s not found in secret %s in namespace %s", tokenSecretKey, secretName, secretNS)
286+
r.logger.Info(err)
287+
return "", err
288+
}
289+
return fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString(
290+
[]byte(fmt.Sprintf("%s:%s", userName, secretVal)))), nil
291+
}

0 commit comments

Comments
 (0)