Skip to content

Commit 5af937d

Browse files
committed
feat: add support for authenticated git clone
Added support for authenticated git cloning using `gitToken` and `gitTokenKey` parameters. Updated documentation to reflect the new parameters and their usage. Modified resolver functions to handle authentication when cloning repositories. Added tests to verify the functionality of authenticated git cloning. The differences between the two modes are: - The `git clone` method support anonymous cloning and authenticated cloning. - Depending of the Git provider `git clone` has a lower rate limit (if none) than the authenticated API. - The authenticated API supports private repositories and fetches only the file at the specified path rather than doing a full clone.
1 parent 0d698a7 commit 5af937d

File tree

7 files changed

+138
-35
lines changed

7 files changed

+138
-35
lines changed

docs/git-resolver.md

Lines changed: 41 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,19 @@ This Resolver responds to type `git`.
1313

1414
## Parameters
1515

16-
| Param Name | Description | Example Value |
17-
|--------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------|
18-
| `url` | URL of the repo to fetch and clone anonymously. Either `url`, or `repo` (with `org`) must be specified, but not both. | `https://github.com/tektoncd/catalog.git` |
19-
| `repo` | The repository to find the resource in. Either `url`, or `repo` (with `org`) must be specified, but not both. | `pipeline`, `test-infra` |
20-
| `org` | The organization to find the repository in. Default can be set in [configuration](#configuration). | `tektoncd`, `kubernetes` |
21-
| `token` | An optional secret name in the `PipelineRun` namespace to fetch the token from. Defaults to empty, meaning it will try to use the configuration from the global configmap. | `secret-name`, (empty) |
22-
| `tokenKey` | An optional key in the token secret name in the `PipelineRun` namespace to fetch the token from. Defaults to `token`. | `token` |
23-
| `revision` | Git revision to checkout a file from. This can be commit SHA, branch or tag. | `aeb957601cf41c012be462827053a21a420befca` `main` `v0.38.2` |
24-
| `pathInRepo` | Where to find the file in the repo. | `task/golang-build/0.3/golang-build.yaml` |
25-
| `serverURL` | An optional server URL (that includes the https:// prefix) to connect for API operations | `https:/github.mycompany.com` |
26-
| `scmType` | An optional SCM type to use for API operations | `github`, `gitlab`, `gitea` |
16+
| Param Name | Description | Example Value |
17+
|---------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------|
18+
| `url` | URL of the repo to fetch and clone anonymously. Either `url`, or `repo` (with `org`) must be specified, but not both. | `https://github.com/tektoncd/catalog.git` |
19+
| `repo` | The repository to find the resource in. Either `url`, or `repo` (with `org`) must be specified, but not both. | `pipeline`, `test-infra` |
20+
| `org` | The organization to find the repository in. Default can be set in [configuration](#configuration). | `tektoncd`, `kubernetes` |
21+
| `token` | An optional secret name in the `PipelineRun` namespace to fetch the token from. Defaults to empty, meaning it will try to use the configuration from the global configmap. | `secret-name`, (empty) |
22+
| `tokenKey` | An optional key in the token secret name in the `PipelineRun` namespace to fetch the token from. Defaults to `token`. | `token` |
23+
| `gitToken` | An optional secret name in the `PipelineRun` namespace to fetch the token from when doing opration with the `git clone`. When empty it will use anonymous cloning.
24+
| `gitTokenKey` | An optional key in the token secret name in the `PipelineRun` namespace to fetch the token from when using the `git clone`. Defaults to `token`. | `token` |
25+
| `revision` | Git revision to checkout a file from. This can be commit SHA, branch or tag. | `aeb957601cf41c012be462827053a21a420befca` `main` `v0.38.2` |
26+
| `pathInRepo` | Where to find the file in the repo. | `task/golang-build/0.3/golang-build.yaml` |
27+
| `serverURL` | An optional server URL (that includes the https:// prefix) to connect for API operations | `https:/github.mycompany.com` |
28+
| `scmType` | An optional SCM type to use for API operations | `github`, `gitlab`, `gitea` |
2729

2830
## Requirements
2931

@@ -55,11 +57,22 @@ for the name, namespace and defaults that the resolver ships with.
5557

5658
## Usage
5759

58-
The `git` resolver has two modes: cloning a repository anonymously, or fetching individual files via an SCM provider's API using an API token.
60+
The `git` resolver has two modes: cloning a repository with `git clone` (with
61+
the `go-git` library), or fetching individual files via an SCM provider's API
62+
using an API token.
5963

60-
### Anonymous Cloning
64+
The differences between the two modes are:
6165

62-
Anonymous cloning is supported only for public repositories. This mode clones the full git repo.
66+
- The `git clone` method support anonymous cloning and authenticated cloning.
67+
- Depending of the Git provider `git clone` has a lower rate limit
68+
than the authenticated API.
69+
- The authenticated API supports private repositories and fetches only the file
70+
at the specified path rather than doing a full clone.
71+
72+
### Git Clone with git clone
73+
74+
Git clone with `git clone` is supported for anonymous and
75+
authenticated cloning.
6376

6477
#### Task Resolution
6578

@@ -78,6 +91,11 @@ spec:
7891
value: main
7992
- name: pathInRepo
8093
value: task/git-clone/0.6/git-clone.yaml
94+
# Uncomment the following lines to use a secret with a token
95+
# - name: gitToken
96+
# value: "secret-with-token"
97+
# - name: gitTokenKey (optional, defaults to "token")
98+
# value: "token"
8199
```
82100

83101
#### Pipeline resolution
@@ -97,6 +115,11 @@ spec:
97115
value: main
98116
- name: pathInRepo
99117
value: pipeline/simple/0.1/simple.yaml
118+
# Uncomment the following lines to use a secret with a token
119+
# - name: gitToken
120+
# value: "secret-with-token"
121+
# - name: gitTokenKey (optional, defaults to "token")
122+
# value: "token"
100123
params:
101124
- name: name
102125
value: Ranni
@@ -193,11 +216,11 @@ spec:
193216

194217
### Specifying Configuration for Multiple Git Providers
195218

196-
It is possible to specify configurations for multiple providers and even multiple configurations for same provider to use in
219+
It is possible to specify configurations for multiple providers and even multiple configurations for same provider to use in
197220
different tekton resources. Firstly, details need to be added in configmap with the unique identifier key prefix.
198-
To use them in tekton resources, pass the unique key mentioned in configmap as an extra param to resolver with key
199-
`configKey` and value will be the unique key. If no `configKey` param is passed, `default` will be used. Default
200-
configuration to be used for git resolver can be specified in configmap by either mentioning no unique identifier or
221+
To use them in tekton resources, pass the unique key mentioned in configmap as an extra param to resolver with key
222+
`configKey` and value will be the unique key. If no `configKey` param is passed, `default` will be used. Default
223+
configuration to be used for git resolver can be specified in configmap by either mentioning no unique identifier or
201224
using identifier `default`
202225

203226
**Note**: `configKey` should not contain `.` while specifying configurations in configmap

pkg/remoteresolution/resolver/git/resolver.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ func (r *Resolver) Resolve(ctx context.Context, req *v1beta1.ResolutionRequestSp
120120
}
121121

122122
if params[git.UrlParam] != "" {
123-
return git.ResolveAnonymousGit(ctx, params)
123+
return git.ResolveAnonymousGit(ctx, params, r.kubeClient, r.logger, r.cache, r.ttl)
124124
}
125125

126126
return git.ResolveAPIGit(ctx, params, r.kubeClient, r.logger, r.cache, r.ttl, r.clientFunc)

pkg/resolution/resolver/git/config.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ type ScmConfig struct {
6262
Org string `json:"default-org"`
6363
ServerURL string `json:"server-url"`
6464
SCMType string `json:"scm-type"`
65+
GitToken string `json:"git-token"`
6566
APISecretName string `json:"api-token-secret-name"`
6667
APISecretKey string `json:"api-token-secret-key"`
6768
APISecretNamespace string `json:"api-token-secret-namespace"`

pkg/resolution/resolver/git/params.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ const (
3333
TokenParam string = "token"
3434
// TokenKeyParam is an optional reference to a key in the TokenParam secret for SCM API authentication
3535
TokenKeyParam string = "tokenKey"
36+
// GitTokenParam is an optional reference to a secret name when using go-git for git authentication
37+
GitTokenParam string = "gitToken"
38+
// GitTokenParam is an optional reference to a secret name when using go-git for git authentication
39+
GitTokenKeyParam string = "gitTokenKey"
3640
// DefaultTokenKeyParam is the default key in the TokenParam secret for SCM API authentication
3741
DefaultTokenKeyParam string = "token"
3842
// scmTypeParam is an optional string overriding the scm-type configuration (ie: github, gitea, gitlab etc..)

pkg/resolution/resolver/git/resolver.go

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ import (
3131
"github.com/go-git/go-git/v5"
3232
gitcfg "github.com/go-git/go-git/v5/config"
3333
"github.com/go-git/go-git/v5/plumbing"
34+
plumbTransport "github.com/go-git/go-git/v5/plumbing/transport"
35+
"github.com/go-git/go-git/v5/plumbing/transport/http"
3436
"github.com/go-git/go-git/v5/storage/memory"
3537
"github.com/jenkins-x/go-scm/scm"
3638
"github.com/jenkins-x/go-scm/scm/factory"
@@ -130,7 +132,7 @@ func (r *Resolver) Resolve(ctx context.Context, origParams []pipelinev1.Param) (
130132
}
131133

132134
if params[UrlParam] != "" {
133-
return ResolveAnonymousGit(ctx, params)
135+
return ResolveAnonymousGit(ctx, params, r.kubeClient, r.logger, r.cache, r.ttl)
134136
}
135137

136138
return ResolveAPIGit(ctx, params, r.kubeClient, r.logger, r.cache, r.ttl, r.clientFunc)
@@ -157,7 +159,7 @@ func validateRepoURL(url string) bool {
157159
return re.MatchString(url)
158160
}
159161

160-
func ResolveAnonymousGit(ctx context.Context, params map[string]string) (framework.ResolvedResource, error) {
162+
func ResolveAnonymousGit(ctx context.Context, params map[string]string, kubeclient kubernetes.Interface, logger *zap.SugaredLogger, cache *cache.LRUExpireCache, ttl time.Duration) (framework.ResolvedResource, error) {
161163
conf, err := GetScmConfigForParamConfigKey(ctx, params)
162164
if err != nil {
163165
return nil, err
@@ -180,6 +182,33 @@ func ResolveAnonymousGit(ctx context.Context, params map[string]string) (framewo
180182
cloneOpts := &git.CloneOptions{
181183
URL: repo,
182184
}
185+
186+
secretRef := &secretCacheKey{
187+
name: params[GitTokenParam],
188+
key: params[GitTokenKeyParam],
189+
}
190+
if secretRef.name != "" {
191+
if secretRef.key == "" {
192+
secretRef.key = DefaultTokenKeyParam
193+
}
194+
secretRef.ns = common.RequestNamespace(ctx)
195+
} else {
196+
secretRef = nil
197+
}
198+
199+
auth := plumbTransport.AuthMethod(nil)
200+
if secretRef != nil {
201+
gitToken, err := getAPIToken(ctx, secretRef, kubeclient, logger, cache, ttl, params, GitTokenKeyParam)
202+
if err != nil {
203+
return nil, err
204+
}
205+
auth = &http.BasicAuth{
206+
Username: "git",
207+
Password: string(gitToken),
208+
}
209+
cloneOpts.Auth = auth
210+
}
211+
183212
filesystem := memfs.New()
184213
repository, err := git.Clone(memory.NewStorage(), filesystem, cloneOpts)
185214
if err != nil {
@@ -190,6 +219,7 @@ func ResolveAnonymousGit(ctx context.Context, params map[string]string) (framewo
190219
refSpec := gitcfg.RefSpec(fmt.Sprintf("+refs/heads/%s:refs/remotes/%s", revision, revision))
191220
err = repository.Fetch(&git.FetchOptions{
192221
RefSpecs: []gitcfg.RefSpec{refSpec},
222+
Auth: auth,
193223
})
194224
if err != nil {
195225
var fetchErr git.NoMatchingRefSpecError
@@ -405,7 +435,7 @@ func ResolveAPIGit(ctx context.Context, params map[string]string, kubeclient kub
405435
} else {
406436
secretRef = nil
407437
}
408-
apiToken, err := getAPIToken(ctx, secretRef, kubeclient, logger, cache, ttl, params)
438+
apiToken, err := getAPIToken(ctx, secretRef, kubeclient, logger, cache, ttl, params, APISecretNameKey)
409439
if err != nil {
410440
return nil, err
411441
}
@@ -449,7 +479,7 @@ func ResolveAPIGit(ctx context.Context, params map[string]string, kubeclient kub
449479
}, nil
450480
}
451481

452-
func getAPIToken(ctx context.Context, apiSecret *secretCacheKey, kubeclient kubernetes.Interface, logger *zap.SugaredLogger, cache *cache.LRUExpireCache, ttl time.Duration, params map[string]string) ([]byte, error) {
482+
func getAPIToken(ctx context.Context, apiSecret *secretCacheKey, kubeclient kubernetes.Interface, logger *zap.SugaredLogger, cache *cache.LRUExpireCache, ttl time.Duration, params map[string]string, key string) ([]byte, error) {
453483
conf, err := GetScmConfigForParamConfigKey(ctx, params)
454484
if err != nil {
455485
return nil, err
@@ -467,7 +497,7 @@ func getAPIToken(ctx context.Context, apiSecret *secretCacheKey, kubeclient kube
467497
if apiSecret.name == "" {
468498
apiSecret.name = conf.APISecretName
469499
if apiSecret.name == "" {
470-
err := fmt.Errorf("cannot get API token, required when specifying '%s' param, '%s' not specified in config", RepoParam, APISecretNameKey)
500+
err := fmt.Errorf("cannot get API token, required when specifying '%s' param, '%s' not specified in config", RepoParam, key)
471501
logger.Info(err)
472502
return nil, err
473503
}

pkg/resolution/resolver/git/resolver_test.go

Lines changed: 51 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -280,17 +280,19 @@ func TestResolveNotEnabled(t *testing.T) {
280280
}
281281

282282
type params struct {
283-
url string
284-
revision string
285-
pathInRepo string
286-
org string
287-
repo string
288-
token string
289-
tokenKey string
290-
namespace string
291-
serverURL string
292-
scmType string
293-
configKey string
283+
url string
284+
revision string
285+
pathInRepo string
286+
org string
287+
repo string
288+
token string
289+
tokenKey string
290+
namespace string
291+
serverURL string
292+
scmType string
293+
configKey string
294+
gitToken string
295+
gitTokenKey string
294296
}
295297

296298
func TestResolve(t *testing.T) {
@@ -419,6 +421,26 @@ func TestResolve(t *testing.T) {
419421
url: anonFakeRepoURL,
420422
},
421423
expectedErr: createError(`error opening file "foo/non-exist": file does not exist`),
424+
}, {
425+
name: "clone: secret for git clone",
426+
args: &params{
427+
pathInRepo: "./released",
428+
url: anonFakeRepoURL,
429+
gitToken: "token-secret",
430+
gitTokenKey: "token",
431+
namespace: "foo",
432+
},
433+
expectedCommitSHA: commitSHAsInAnonRepo[2],
434+
expectedStatus: resolution.CreateResolutionRequestStatusWithData([]byte("released content in main branch and in tag v1")),
435+
}, {
436+
name: "clone: secret for git clone does not exist",
437+
args: &params{
438+
pathInRepo: "./released",
439+
url: anonFakeRepoURL,
440+
gitToken: "non-existent",
441+
gitTokenKey: "token",
442+
},
443+
expectedErr: createError(`cannot get API token, secret non-existent not found in namespace foo`),
422444
}, {
423445
name: "clone: revision does not exist",
424446
args: &params{
@@ -721,9 +743,14 @@ func TestResolve(t *testing.T) {
721743
if tc.config[tc.configIdentifer+APISecretNameKey] != "" && tc.config[tc.configIdentifer+APISecretNamespaceKey] != "" && tc.config[tc.configIdentifer+APISecretKeyKey] != "" && tc.apiToken != "" {
722744
secretName, secretNameKey, secretNamespace = tc.config[tc.configIdentifer+APISecretNameKey], tc.config[tc.configIdentifer+APISecretKeyKey], tc.config[tc.configIdentifer+APISecretNamespaceKey]
723745
}
746+
724747
if tc.args.token != "" && tc.args.namespace != "" && tc.args.tokenKey != "" {
725748
secretName, secretNameKey, secretNamespace = tc.args.token, tc.args.tokenKey, tc.args.namespace
726749
}
750+
751+
if tc.args.gitToken != "" && tc.args.gitTokenKey != "" && tc.args.namespace != "" {
752+
secretName, secretNameKey, secretNamespace = tc.args.gitToken, tc.args.gitTokenKey, tc.args.namespace
753+
}
727754
if secretName == "" || secretNameKey == "" || secretNamespace == "" {
728755
return
729756
}
@@ -754,6 +781,9 @@ func createTestRepo(t *testing.T, commits []commitForRepo) (string, []string) {
754781
tempDir := t.TempDir()
755782

756783
repo, err := git.PlainInit(tempDir, false)
784+
if err != nil {
785+
t.Fatalf("couldn't create test repo: %v", err)
786+
}
757787

758788
worktree, err := repo.Worktree()
759789
if err != nil {
@@ -922,6 +952,16 @@ func createRequest(args *params) *v1beta1.ResolutionRequest {
922952
Name: UrlParam,
923953
Value: *pipelinev1.NewStructuredValues(args.url),
924954
})
955+
if args.gitToken != "" {
956+
rr.Spec.Params = append(rr.Spec.Params, pipelinev1.Param{
957+
Name: GitTokenParam,
958+
Value: *pipelinev1.NewStructuredValues(args.gitToken),
959+
})
960+
rr.Spec.Params = append(rr.Spec.Params, pipelinev1.Param{
961+
Name: GitTokenKeyParam,
962+
Value: *pipelinev1.NewStructuredValues(args.gitTokenKey),
963+
})
964+
}
925965
} else {
926966
rr.Spec.Params = append(rr.Spec.Params, pipelinev1.Param{
927967
Name: RepoParam,

0 commit comments

Comments
 (0)