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

Add credentials to HTTP resolver #7315

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
4 changes: 4 additions & 0 deletions docs/git-resolver.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,10 @@ Note that not all `go-scm` implementations have been tested with the `git` resol
* BitBucket Server
* BitBucket Cloud

Fetching from multiple Git providers with different configuration is not
supported. You can use the [http resolver](./http-resolver.md) to fetch URL
from another provider with different credentials.

#### Task Resolution

```yaml
Expand Down
31 changes: 28 additions & 3 deletions docs/http-resolver.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<!--
---

linkTitle: "HTTP Resolver"
weight: 311
---
Expand All @@ -11,9 +12,12 @@ This resolver responds to type `http`.

## Parameters

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

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

Expand Down Expand Up @@ -54,6 +58,27 @@ spec:
value: https://raw.githubusercontent.com/tektoncd-catalog/git-clone/main/task/git-clone/git-clone.yaml
```

### Task Resolution with Basic Auth

```yaml
apiVersion: tekton.dev/v1beta1
kind: TaskRun
metadata:
name: remote-task-reference
spec:
taskRef:
resolver: http
params:
- name: url
value: https://raw.githubusercontent.com/owner/private-repo/main/task/task.yaml
- name: http-username
value: git
- name: http-password-secret
value: git-secret
- name: http-password-secret-key
value: git-token
```

### Pipeline Resolution

```yaml
Expand Down
35 changes: 35 additions & 0 deletions examples/v1/pipelineruns/beta/http-resolver-credentials.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# This http resolver example will uses a username and password to access the
# URL.
#
# http-password-secret is a Kubernetes secret containing the
# password in the same namespace where this PipelineRun runs.
---
kind: Secret
apiVersion: v1
metadata:
name: my-secret
stringData:
token: "token"
---
apiVersion: tekton.dev/v1
kind: PipelineRun
metadata:
generateName: http-resolver-
spec:
pipelineSpec:
tasks:
- name: http-resolver
taskRef:
resolver: http
params:
- name: url
value: https://api.hub.tekton.dev/v1/resource/tekton/task/tkn/0.4/raw
Yongxuanzhang marked this conversation as resolved.
Show resolved Hide resolved
- name: http-username
value: git
- name: http-password-secret
value: my-secret
- name: http-password-secret-key
value: token
params:
- name: ARGS
value: ["version"]
6 changes: 3 additions & 3 deletions pkg/resolution/resolver/framework/testing/fakecontroller.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,14 +71,14 @@ func RunResolverReconcileTest(ctx context.Context, t *testing.T, d test.Data, re
err := testAssets.Controller.Reconciler.Reconcile(testAssets.Ctx, getRequestName(request))
if expectedErr != nil {
if err == nil {
t.Fatalf("expected to get error %v, but got nothing", expectedErr)
t.Fatalf("expected to get error: `%v`, but got nothing", expectedErr)
}
if expectedErr.Error() != err.Error() {
t.Fatalf("expected to get error %v, but got %v", expectedErr, err)
t.Fatalf("expected to get error `%v`, but got `%v`", expectedErr, err)
}
} else if err != nil {
if ok, _ := controller.IsRequeueKey(err); !ok {
t.Fatalf("did not expect an error, but got %v", err)
t.Fatalf("did not expect an error, but got `%v`", err)
}
}

Expand Down
11 changes: 10 additions & 1 deletion pkg/resolution/resolver/http/params.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,15 @@ limitations under the License.
package http

const (
// urlParam is the url to fetch the task from
// urlParam is the URL to fetch the task from
urlParam string = "url"

// httpBasicAuthUsername is the user name to use for basic auth
httpBasicAuthUsername string = "http-username"

// httpBasicAuthSecret is the reference to a secret in the PipelineRun or TaskRun namespace to use for basic auth
httpBasicAuthSecret string = "http-password-secret"

// httpBasicAuthSecretKey is the key in the httpBasicAuthSecret secret to use for basic auth
httpBasicAuthSecretKey string = "http-password-secret-key"
)
83 changes: 78 additions & 5 deletions pkg/resolution/resolver/http/resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package http
import (
"context"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"errors"
"fmt"
Expand All @@ -29,6 +30,12 @@ import (
pipelinev1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1"
"github.com/tektoncd/pipeline/pkg/resolution/common"
"github.com/tektoncd/pipeline/pkg/resolution/resolver/framework"
"go.uber.org/zap"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
kubeclient "knative.dev/pkg/client/injection/kube/client"
"knative.dev/pkg/logging"
)

const (
Expand All @@ -41,17 +48,25 @@ const (
// httpResolverName The name of the resolver
httpResolverName = "Http"

// ConfigMapName is the http resolver's config map
// configMapName is the http resolver's config map
configMapName = "http-resolver-config"

// default Timeout value when fetching http resources in seconds
defaultHttpTimeoutValue = "1m"

// default key in the HTTP password secret
defaultBasicAuthSecretKey = "password"
)

// Resolver implements a framework.Resolver that can fetch files from an HTTP URL
type Resolver struct{}
type Resolver struct {
kubeClient kubernetes.Interface
logger *zap.SugaredLogger
}

func (r *Resolver) Initialize(context.Context) error {
func (r *Resolver) Initialize(ctx context.Context) error {
r.kubeClient = kubeclient.Get(ctx)
r.logger = logging.FromContext(ctx)
return nil
}

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

return fetchHttpResource(ctx, params)
return r.fetchHttpResource(ctx, params)
}

func (r *Resolver) isDisabled(ctx context.Context) bool {
Expand Down Expand Up @@ -156,6 +171,24 @@ func populateDefaultParams(ctx context.Context, params []pipelinev1.Param) (map[
}
}

if username, ok := paramsMap[httpBasicAuthUsername]; ok {
if _, ok := paramsMap[httpBasicAuthSecret]; !ok {
return nil, fmt.Errorf("missing required param %s when using %s", httpBasicAuthSecret, httpBasicAuthUsername)
}
if username == "" {
return nil, fmt.Errorf("value %s cannot be empty", httpBasicAuthUsername)
}
}

if secret, ok := paramsMap[httpBasicAuthSecret]; ok {
if _, ok := paramsMap[httpBasicAuthUsername]; !ok {
return nil, fmt.Errorf("missing required param %s when using %s", httpBasicAuthUsername, httpBasicAuthSecret)
}
if secret == "" {
return nil, fmt.Errorf("value %s cannot be empty", httpBasicAuthSecret)
Yongxuanzhang marked this conversation as resolved.
Show resolved Hide resolved
}
}

if len(missingParams) > 0 {
return nil, fmt.Errorf("missing required http resolver params: %s", strings.Join(missingParams, ", "))
}
Expand All @@ -178,7 +211,7 @@ func makeHttpClient(ctx context.Context) (*http.Client, error) {
}, nil
}

func fetchHttpResource(ctx context.Context, params map[string]string) (framework.ResolvedResource, error) {
func (r *Resolver) fetchHttpResource(ctx context.Context, params map[string]string) (framework.ResolvedResource, error) {
var targetURL string
var ok bool

Expand All @@ -196,6 +229,15 @@ func fetchHttpResource(ctx context.Context, params map[string]string) (framework
return nil, fmt.Errorf("constructing request: %w", err)
}

// NOTE(chmouel): We already made sure that username and secret was specified by the user
if secret, ok := params[httpBasicAuthSecret]; ok && secret != "" {
if encodedSecret, err := r.getBasicAuthSecret(ctx, params); err != nil {
return nil, err
} else {
req.Header.Set("Authorization", encodedSecret)
}
}

resp, err := httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("error fetching URL: %w", err)
Expand All @@ -216,3 +258,34 @@ func fetchHttpResource(ctx context.Context, params map[string]string) (framework
URL: targetURL,
}, nil
}

func (r *Resolver) getBasicAuthSecret(ctx context.Context, params map[string]string) (string, error) {
secretName := params[httpBasicAuthSecret]
userName := params[httpBasicAuthUsername]
tokenSecretKey := defaultBasicAuthSecretKey
if v, ok := params[httpBasicAuthSecretKey]; ok {
if v != "" {
tokenSecretKey = v
}
}
secretNS := common.RequestNamespace(ctx)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The namespace is the same as the one with TaskRun/PiplineRun or from the default? I think we need to clarify which namespace we're using in the docs&docstrings

secret, err := r.kubeClient.CoreV1().Secrets(secretNS).Get(ctx, secretName, metav1.GetOptions{})
if err != nil {
if apierrors.IsNotFound(err) {
notFoundErr := fmt.Errorf("cannot get API token, secret %s not found in namespace %s", secretName, secretNS)
r.logger.Info(notFoundErr)
return "", notFoundErr
}
wrappedErr := fmt.Errorf("error reading API token from secret %s in namespace %s: %w", secretName, secretNS, err)
r.logger.Info(wrappedErr)
return "", wrappedErr
}
secretVal, ok := secret.Data[tokenSecretKey]
if !ok {
err := fmt.Errorf("cannot get API token, key %s not found in secret %s in namespace %s", tokenSecretKey, secretName, secretNS)
r.logger.Info(err)
return "", err
}
return fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString(
[]byte(fmt.Sprintf("%s:%s", userName, secretVal)))), nil
}
Loading
Loading