Skip to content

Commit ffdf7b3

Browse files
authored
Merge pull request #191 from timofurrer/gitlab-deploy-token
Implement client for Deploy Tokens
2 parents c232b5d + b79b5d5 commit ffdf7b3

12 files changed

+636
-13
lines changed

github/resource_repository.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,10 @@ func (r *userRepository) DeployKeys() gitprovider.DeployKeyClient {
129129
return r.deployKeys
130130
}
131131

132+
func (r *userRepository) DeployTokens() (gitprovider.DeployTokenClient, error) {
133+
return nil, gitprovider.ErrNoProviderSupport
134+
}
135+
132136
func (r *userRepository) Commits() gitprovider.CommitClient {
133137
return r.commits
134138
}
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
/*
2+
Copyright 2023 The Flux CD contributors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package gitlab
18+
19+
import (
20+
"context"
21+
"errors"
22+
"fmt"
23+
24+
"github.com/fluxcd/go-git-providers/gitprovider"
25+
"github.com/xanzy/go-gitlab"
26+
)
27+
28+
// DeployTokenClient implements the gitprovider.DeployTokenClient interface.
29+
var _ gitprovider.DeployTokenClient = &DeployTokenClient{}
30+
31+
// DeployTokenClient operates on the access deploy token list for a specific repository.
32+
type DeployTokenClient struct {
33+
*clientContext
34+
ref gitprovider.RepositoryRef
35+
}
36+
37+
// Get returns the repository at the given path.
38+
//
39+
// ErrNotFound is returned if the resource does not exist.
40+
func (c *DeployTokenClient) Get(_ context.Context, deployTokenName string) (gitprovider.DeployToken, error) {
41+
return c.get(deployTokenName)
42+
}
43+
44+
func (c *DeployTokenClient) get(deployTokenName string) (*deployToken, error) {
45+
deployTokens, err := c.list()
46+
if err != nil {
47+
return nil, err
48+
}
49+
// Loop through deploy tokens once we find one with the right name
50+
for _, dk := range deployTokens {
51+
if dk.k.Name == deployTokenName {
52+
return dk, nil
53+
}
54+
}
55+
return nil, gitprovider.ErrNotFound
56+
}
57+
58+
// List lists all repository deploy tokens of the given deploy token type.
59+
//
60+
// List returns all available repository deploy tokens for the given type,
61+
// using multiple paginated requests if needed.
62+
func (c *DeployTokenClient) List(_ context.Context) ([]gitprovider.DeployToken, error) {
63+
dks, err := c.list()
64+
if err != nil {
65+
return nil, err
66+
}
67+
// Cast to the generic []gitprovider.DeployToken
68+
tokens := make([]gitprovider.DeployToken, 0, len(dks))
69+
for _, dk := range dks {
70+
tokens = append(tokens, dk)
71+
}
72+
return tokens, nil
73+
}
74+
75+
func (c *DeployTokenClient) list() ([]*deployToken, error) {
76+
// GET /repos/{owner}/{repo}/tokens
77+
apiObjs, err := c.c.ListTokens(getRepoPath(c.ref))
78+
if err != nil {
79+
return nil, err
80+
}
81+
82+
// Map the api object to our DeployToken type
83+
tokens := make([]*deployToken, 0, len(apiObjs))
84+
for _, apiObj := range apiObjs {
85+
// apiObj is already validated at ListTokens
86+
tokens = append(tokens, newDeployToken(c, apiObj))
87+
}
88+
89+
return tokens, nil
90+
}
91+
92+
// Create creates a deploy token with the given specifications.
93+
//
94+
// ErrAlreadyExists will be returned if the resource already exists.
95+
func (c *DeployTokenClient) Create(_ context.Context, req gitprovider.DeployTokenInfo) (gitprovider.DeployToken, error) {
96+
apiObj, err := createDeployToken(c.c, c.ref, req)
97+
if err != nil {
98+
return nil, err
99+
}
100+
return newDeployToken(c, apiObj), nil
101+
}
102+
103+
// Reconcile makes sure the given desired state (req) becomes the actual state in the backing Git provider.
104+
//
105+
// If req doesn't exist under the hood, it is created (actionTaken == true).
106+
// If req doesn't equal the actual state, the resource will be deleted and recreated (actionTaken == true).
107+
// If req is already the actual state, this is a no-op (actionTaken == false).
108+
func (c *DeployTokenClient) Reconcile(ctx context.Context, req gitprovider.DeployTokenInfo) (gitprovider.DeployToken, bool, error) {
109+
// First thing, validate and default the request to ensure a valid and fully-populated object
110+
// (to minimize any possible diffs between desired and actual state)
111+
if err := gitprovider.ValidateAndDefaultInfo(&req); err != nil {
112+
return nil, false, err
113+
}
114+
115+
// Get the token with the desired name
116+
actual, err := c.Get(ctx, req.Name)
117+
if err != nil {
118+
// Create if not found
119+
if errors.Is(err, gitprovider.ErrNotFound) {
120+
resp, err := c.Create(ctx, req)
121+
return resp, true, err
122+
}
123+
124+
// Unexpected path, Get should succeed or return NotFound
125+
return nil, false, err
126+
}
127+
128+
actionTaken, err := actual.Reconcile(ctx)
129+
if err != nil {
130+
return nil, false, err
131+
}
132+
133+
return actual, actionTaken, nil
134+
//
135+
// // If the desired matches the actual state, just return the actual state
136+
// if req.Equals(actual.Get()) {
137+
// return actual, false, nil
138+
// }
139+
//
140+
// // Populate the desired state to the current-actual object
141+
// if err := actual.Set(req); err != nil {
142+
// return actual, false, err
143+
// }
144+
// // Apply the desired state by running Update
145+
// return actual, true, actual.Update(ctx)
146+
}
147+
148+
func createDeployToken(c gitlabClient, ref gitprovider.RepositoryRef, req gitprovider.DeployTokenInfo) (*gitlab.DeployToken, error) {
149+
// First thing, validate and default the request to ensure a valid and fully-populated object
150+
// (to minimize any possible diffs between desired and actual state)
151+
if err := gitprovider.ValidateAndDefaultInfo(&req); err != nil {
152+
return nil, err
153+
}
154+
155+
return c.CreateToken(fmt.Sprintf("%s/%s", ref.GetIdentity(), ref.GetRepository()), deployTokenToAPI(&req))
156+
}

gitlab/gitlabclient.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,18 @@ type gitlabClient interface {
8888
// This function handles HTTP error wrapping.
8989
DeleteKey(projectName string, keyID int) error
9090

91+
// Deploy token methods
92+
93+
// ListTokens is a wrapper for "GET /projects/{project}/deploy_tokens".
94+
// This function handles pagination, HTTP error wrapping, and validates the server result.
95+
ListTokens(projectName string) ([]*gitlab.DeployToken, error)
96+
// CreateProjectKey is a wrapper for "POST /projects/{project}/deploy_tokens".
97+
// This function handles HTTP error wrapping, and validates the server result.
98+
CreateToken(projectName string, req *gitlab.DeployToken) (*gitlab.DeployToken, error)
99+
// DeleteKey is a wrapper for "DELETE /projects/{project}/deploy_tokens/{key_id}".
100+
// This function handles HTTP error wrapping.
101+
DeleteToken(projectName string, keyID int) error
102+
91103
// Team related methods
92104

93105
// ShareGroup is a wrapper for ""
@@ -373,6 +385,54 @@ func (c *gitlabClientImpl) DeleteKey(projectName string, keyID int) error {
373385
return handleHTTPError(err)
374386
}
375387

388+
func (c *gitlabClientImpl) ListTokens(projectName string) ([]*gitlab.DeployToken, error) {
389+
apiObjs := []*gitlab.DeployToken{}
390+
opts := &gitlab.ListProjectDeployTokensOptions{}
391+
err := allDeployTokenPages(opts, func() (*gitlab.Response, error) {
392+
// GET /projects/{project}/deploy_tokens
393+
pageObjs, resp, listErr := c.c.DeployTokens.ListProjectDeployTokens(projectName, opts)
394+
// filter for active tokens
395+
for _, apiObj := range pageObjs {
396+
if !apiObj.Expired && !apiObj.Revoked {
397+
apiObjs = append(apiObjs, apiObj)
398+
}
399+
}
400+
return resp, listErr
401+
})
402+
if err != nil {
403+
return nil, err
404+
}
405+
406+
for _, apiObj := range apiObjs {
407+
if err := validateDeployTokenAPI(apiObj); err != nil {
408+
return nil, err
409+
}
410+
}
411+
return apiObjs, nil
412+
}
413+
414+
func (c *gitlabClientImpl) CreateToken(projectName string, req *gitlab.DeployToken) (*gitlab.DeployToken, error) {
415+
opts := &gitlab.CreateProjectDeployTokenOptions{
416+
Name: &req.Name,
417+
Scopes: &[]string{"read_repository"},
418+
}
419+
// POST /projects/{project}/deploy_tokens
420+
apiObj, _, err := c.c.DeployTokens.CreateProjectDeployToken(projectName, opts)
421+
if err != nil {
422+
return nil, handleHTTPError(err)
423+
}
424+
if err := validateDeployTokenAPI(apiObj); err != nil {
425+
return nil, err
426+
}
427+
return apiObj, nil
428+
}
429+
430+
func (c *gitlabClientImpl) DeleteToken(projectName string, keyID int) error {
431+
// DELETE /projects/{project}/deploy_tokens/{deploy_token_id}
432+
_, err := c.c.DeployTokens.DeleteProjectDeployToken(projectName, keyID)
433+
return handleHTTPError(err)
434+
}
435+
376436
func (c *gitlabClientImpl) ShareProject(projectName string, groupIDObj, groupAccessObj int) error {
377437
groupAccess := gitlab.AccessLevel(gitlab.AccessLevelValue(groupAccessObj))
378438
groupID := &groupIDObj

0 commit comments

Comments
 (0)