Skip to content

Commit 4bdcb7e

Browse files
authored
test: reuse Gitlab repos (#1923)
1 parent a2eb632 commit 4bdcb7e

File tree

5 files changed

+163
-179
lines changed

5 files changed

+163
-179
lines changed

e2e/nomostest/gitproviders/bitbucket.go

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ package gitproviders
1616

1717
import (
1818
"bytes"
19+
"context"
1920
"encoding/json"
2021
"errors"
2122
"fmt"
@@ -35,7 +36,8 @@ const (
3536
bitbucketProject = "CSCI"
3637

3738
// PrivateSSHKey is secret name of the private SSH key stored in the Cloud Secret Manager.
38-
PrivateSSHKey = "config-sync-ci-ssh-private-key"
39+
PrivateSSHKey = "config-sync-ci-ssh-private-key"
40+
bitbucketRequestTimeout = 10 * time.Second
3941
)
4042

4143
// BitbucketClient is the client that calls the Bitbucket REST APIs.
@@ -59,9 +61,7 @@ func newBitbucketClient(repoSuffix string, logger *testlogger.TestLogger) (*Bitb
5961
logger: logger,
6062
workspace: *e2e.BitbucketWorkspace,
6163
repoSuffix: repoSuffix,
62-
httpClient: &http.Client{
63-
Timeout: 10 * time.Second,
64-
},
64+
httpClient: &http.Client{},
6565
}
6666

6767
var err error
@@ -112,7 +112,9 @@ func (b *BitbucketClient) CreateRepository(name string) (string, error) {
112112
}
113113

114114
// Check if repository already exists
115-
resp, err := b.sendRequest(http.MethodGet, repoURL, accessToken, nil)
115+
getRequestCtx, getRequestCancel := context.WithTimeout(context.Background(), bitbucketRequestTimeout)
116+
defer getRequestCancel()
117+
resp, err := b.sendRequest(getRequestCtx, http.MethodGet, repoURL, accessToken, nil)
116118
if err != nil {
117119
return "", err
118120
}
@@ -135,13 +137,15 @@ func (b *BitbucketClient) CreateRepository(name string) (string, error) {
135137
}
136138

137139
// Create new Bitbucket repository
138-
resp, err = b.sendRequest(http.MethodPost, repoURL, accessToken, bytes.NewReader(jsonPayload))
140+
postRequestCtx, postRequestCancel := context.WithTimeout(context.Background(), bitbucketRequestTimeout)
141+
resp, err = b.sendRequest(postRequestCtx, http.MethodPost, repoURL, accessToken, bytes.NewReader(jsonPayload))
139142

140143
defer func() {
141144
if closeErr := resp.Body.Close(); closeErr != nil {
142145
// Log the error as just printing it might be missed.
143146
b.logger.Infof("failed to close response body: %v\n", closeErr)
144147
}
148+
postRequestCancel()
145149
}()
146150

147151
if err != nil {
@@ -159,8 +163,8 @@ func (b *BitbucketClient) CreateRepository(name string) (string, error) {
159163
}
160164

161165
// sendRequest sends an HTTP request to the Bitbucket API.
162-
func (b *BitbucketClient) sendRequest(method, url, accessToken string, body io.Reader) (*http.Response, error) {
163-
req, err := http.NewRequest(method, url, body)
166+
func (b *BitbucketClient) sendRequest(ctx context.Context, method, url, accessToken string, body io.Reader) (*http.Response, error) {
167+
req, err := http.NewRequestWithContext(ctx, method, url, body)
164168
if err != nil {
165169
return nil, fmt.Errorf("error creating request: %w", err)
166170
}

e2e/nomostest/gitproviders/git-provider.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,8 @@ func NewGitProvider(t testing.NTB, provider, clusterName string, logger *testlog
5858
}
5959
return client
6060
case e2e.GitLab:
61-
client, err := newGitlabClient()
61+
repoSuffix := *e2e.GCPProject + "/" + clusterName
62+
client, err := newGitlabClient(repoSuffix, logger)
6263
if err != nil {
6364
t.Fatal(err)
6465
}

e2e/nomostest/gitproviders/gitlab.go

Lines changed: 81 additions & 167 deletions
Original file line numberDiff line numberDiff line change
@@ -15,32 +15,40 @@
1515
package gitproviders
1616

1717
import (
18+
"context"
1819
"encoding/json"
19-
"errors"
2020
"fmt"
21-
"os/exec"
22-
"strings"
21+
"io"
22+
"net/http"
2323
"time"
2424

2525
"github.com/GoogleContainerTools/config-sync/e2e"
26-
"github.com/google/uuid"
27-
"go.uber.org/multierr"
26+
"github.com/GoogleContainerTools/config-sync/e2e/nomostest/gitproviders/util"
27+
"github.com/GoogleContainerTools/config-sync/e2e/nomostest/testlogger"
2828
)
2929

3030
const (
31-
projectNameMaxLength = 256
3231
groupID = 15698791
3332
groupName = "configsync"
33+
gitlabRequestTimeout = 10 * time.Second
3434
)
3535

3636
// GitlabClient is the client that will call Gitlab REST APIs.
3737
type GitlabClient struct {
3838
privateToken string
39+
logger *testlogger.TestLogger
40+
// repoSuffix is used to avoid overlap
41+
repoSuffix string
42+
httpClient *http.Client
3943
}
4044

4145
// newGitlabClient instantiates a new GitlabClient.
42-
func newGitlabClient() (*GitlabClient, error) {
43-
client := &GitlabClient{}
46+
func newGitlabClient(repoSuffix string, logger *testlogger.TestLogger) (*GitlabClient, error) {
47+
client := &GitlabClient{
48+
logger: logger,
49+
repoSuffix: repoSuffix,
50+
httpClient: &http.Client{},
51+
}
4452

4553
var err error
4654

@@ -50,6 +58,10 @@ func newGitlabClient() (*GitlabClient, error) {
5058
return client, nil
5159
}
5260

61+
func (g *GitlabClient) fullName(name string) string {
62+
return util.SanitizeGitlabRepoName(g.repoSuffix, name)
63+
}
64+
5365
// Type returns the git provider type
5466
func (g *GitlabClient) Type() string {
5567
return e2e.GitLab
@@ -68,190 +80,92 @@ func (g *GitlabClient) SyncURL(name string) string {
6880
// CreateRepository calls the POST API to create a project/repository on Gitlab.
6981
// The remote repo name is unique with a prefix of the local name.
7082
func (g *GitlabClient) CreateRepository(name string) (string, error) {
71-
u, err := uuid.NewRandom()
83+
fullName := g.fullName(name)
84+
repoURL := fmt.Sprintf("https://gitlab.com/api/v4/projects?search=%s", fullName)
85+
86+
// Check if the repository already exists
87+
getRequestCtx, getRequestCancel := context.WithTimeout(context.Background(), gitlabRequestTimeout)
88+
defer getRequestCancel()
89+
resp, err := g.sendRequest(getRequestCtx, http.MethodGet, repoURL, nil)
7290
if err != nil {
73-
return "", fmt.Errorf("failed to generate a new UUID: %w", err)
91+
return "", err
7492
}
7593

76-
repoName := name + "-" + u.String()
77-
// Gitlab create projects API doesn't allow '/' character
78-
// so all instances are replaced with '-'
79-
repoName = strings.ReplaceAll(repoName, "/", "-")
80-
if len(repoName) > projectNameMaxLength {
81-
repoName = repoName[:projectNameMaxLength]
82-
}
94+
if resp.StatusCode == http.StatusOK {
95+
body, err := io.ReadAll(resp.Body)
96+
if err != nil {
97+
return "", fmt.Errorf("error reading response body: %w", err)
98+
}
8399

84-
// Projects created under the `configsync` group (namespaceId: 15698791) has
85-
// no protected branch.
86-
out, err := exec.Command("curl", "-s", "--request", "POST",
87-
fmt.Sprintf("https://gitlab.com/api/v4/projects?name=%s&namespace_id=%d&initialize_with_readme=true", repoName, groupID),
88-
"--header", fmt.Sprintf("PRIVATE-TOKEN: %s", g.privateToken)).CombinedOutput()
100+
var output []map[string]interface{}
101+
if err := json.Unmarshal(body, &output); err != nil {
102+
return "", fmt.Errorf("unmarshalling project search response: %w", err)
103+
}
89104

90-
if err != nil {
91-
return "", fmt.Errorf("%s: %w", string(out), err)
92-
}
93-
if !strings.Contains(string(out), fmt.Sprintf("\"name\":\"%s\"", repoName)) {
94-
return "", errors.New(string(out))
95-
}
105+
// the assumption is that our project name is unique, so we'll get exactly 1 result
106+
if len(output) > 0 {
107+
return fullName, nil
96108

97-
return repoName, nil
98-
}
109+
}
110+
} else {
111+
return "", fmt.Errorf("failed to check if repository exists: status %d", resp.StatusCode)
112+
}
99113

100-
// GetProjectID is a helper function for DeleteRepositories
101-
// since Gitlab API only deletes by id
102-
func GetProjectID(g *GitlabClient, name string) (string, error) {
103-
out, err := exec.Command("curl", "-s", "--request", "GET",
104-
fmt.Sprintf("https://gitlab.com/api/v4/projects?search=%s", name),
105-
"--header", fmt.Sprintf("PRIVATE-TOKEN: %s", g.privateToken)).CombinedOutput()
114+
// Creates repository
115+
repoURL = fmt.Sprintf("https://gitlab.com/api/v4/projects?name=%s&namespace_id=%d&initialize_with_readme=true", fullName, groupID)
116+
postRequestCtx, postRequestCancel := context.WithTimeout(context.Background(), gitlabRequestTimeout)
117+
resp, err = g.sendRequest(postRequestCtx, http.MethodPost, repoURL, nil)
106118

119+
defer func() {
120+
if closeErr := resp.Body.Close(); closeErr != nil {
121+
g.logger.Infof("failed to close response body: %v\n", closeErr)
122+
}
123+
postRequestCancel()
124+
}()
107125
if err != nil {
108-
return "", fmt.Errorf("Failure retrieving id for project %s: %w", name, err)
126+
return "", err
109127
}
110128

111-
var response []interface{}
112-
113-
err = json.Unmarshal(out, &response)
129+
body, err := io.ReadAll(resp.Body)
114130
if err != nil {
115-
return "", fmt.Errorf("%s: %w", string(out), err)
131+
return "", fmt.Errorf("error reading response body: %w", err)
116132
}
117133

118-
var float float64
119-
var ok bool
120-
121-
// the assumption is that our project name is unique, so we'll get exactly 1 result
122-
if len(response) < 1 {
123-
return "", fmt.Errorf("Project with name %s: %w", name, err)
124-
}
125-
if len(response) > 1 {
126-
return "", fmt.Errorf("Project with name %s is not unique: %w", name, err)
134+
if resp.StatusCode != http.StatusCreated {
135+
return "", fmt.Errorf("failed to create repository: status %d: %s", resp.StatusCode, string(body))
127136
}
128-
m := response[0].(map[string]interface{})
129-
if x, found := m["id"]; found {
130-
if float, ok = x.(float64); !ok {
131-
return "", fmt.Errorf("Project id in the respose isn't a float: %w", err)
132-
}
133-
} else {
134-
return "", fmt.Errorf("Project id wasn't found in the response: %w", err)
135-
}
136-
id := fmt.Sprintf("%.0f", float)
137137

138-
return id, nil
138+
return fullName, nil
139139
}
140140

141-
// DeleteRepositories calls the DELETE API to delete the list of project name in Gitlab.
142-
func (g *GitlabClient) DeleteRepositories(names ...string) error {
143-
var errs error
144-
145-
for _, name := range names {
146-
id, err := GetProjectID(g, name)
147-
if err != nil {
148-
errs = multierr.Append(errs, fmt.Errorf("invalid repo name: %w", err))
149-
} else {
150-
out, err := exec.Command("curl", "-s", "--request", "DELETE",
151-
fmt.Sprintf("https://gitlab.com/api/v4/projects/%s", id),
152-
"--header", fmt.Sprintf("PRIVATE-TOKEN: %s", g.privateToken)).CombinedOutput()
153-
154-
if err != nil {
155-
errs = multierr.Append(errs, fmt.Errorf("%s: %w", string(out), err))
156-
}
157-
158-
response := string(out)
159-
// Check for successful deletion (202 Accepted)
160-
if strings.Contains(response, "\"message\":\"202 Accepted\"") {
161-
continue
162-
}
163-
// Check if project is already marked for deletion (this is also a success case)
164-
if strings.Contains(response, "Project has already been marked for deletion") {
165-
continue
166-
}
167-
// Any other response is treated as an error
168-
return errors.New(response)
169-
}
170-
}
171-
return errs
141+
// DeleteRepositories is a no-op because Gitlab repo names are determined by the
142+
// test cluster name and RSync namespace and name, so they can be reset and reused
143+
// across test runs
144+
func (g *GitlabClient) DeleteRepositories(_ ...string) error {
145+
g.logger.Info("[Gitlab] Skip deletion of repos")
146+
return nil
172147
}
173148

174-
// DeleteObsoleteRepos deletes all projects that has been inactive more than 24 hours
149+
// DeleteObsoleteRepos is a no-op because Gitlab repo names are determined by the
150+
// test cluster name and RSync namespace and name, so it can be reused if it
151+
// failed to be deleted after the test.
175152
func (g *GitlabClient) DeleteObsoleteRepos() error {
176-
repos, _ := g.GetObsoleteRepos()
177-
178-
err := g.DeleteRepoByID(repos...)
179-
return err
153+
return nil
180154
}
181155

182-
// DeleteRepoByID calls the DELETE API to delete the list of project id in Gitlab.
183-
func (g *GitlabClient) DeleteRepoByID(ids ...string) error {
184-
var errs error
185-
186-
for _, id := range ids {
187-
out, err := exec.Command("curl", "-s", "--request", "DELETE",
188-
fmt.Sprintf("https://gitlab.com/api/v4/projects/%s", id),
189-
"--header", fmt.Sprintf("PRIVATE-TOKEN: %s", g.privateToken)).CombinedOutput()
190-
191-
if err != nil {
192-
errs = multierr.Append(errs, fmt.Errorf("%s: %w", string(out), err))
193-
}
194-
195-
response := string(out)
196-
// Check for successful deletion (202 Accepted)
197-
if strings.Contains(response, "\"message\":\"202 Accepted\"") {
198-
continue
199-
}
200-
// Check if project is already marked for deletion (this is also a success case)
201-
if strings.Contains(response, "Project has already been marked for deletion") {
202-
continue
203-
}
204-
// Any other response is treated as an error
205-
return fmt.Errorf("unexpected response in DeleteRepoByID: %s", response)
156+
// sendRequest sends an HTTP request to the Gitlab API.
157+
func (g *GitlabClient) sendRequest(ctx context.Context, method, url string, body io.Reader) (*http.Response, error) {
158+
req, err := http.NewRequestWithContext(ctx, method, url, body)
159+
if err != nil {
160+
return nil, fmt.Errorf("error creating request: %w", err)
206161
}
207-
return errs
208-
}
209-
210-
// GetObsoleteRepos is a helper function to get all project ids that has been inactive more than 24 hours
211-
func (g *GitlabClient) GetObsoleteRepos() ([]string, error) {
212-
var result []string
213-
pageNum := 1
214-
cutOffDate := time.Now().AddDate(0, 0, -1)
215-
formattedDate := fmt.Sprintf("%d-%02d-%02dT%02d:%02d:%02d",
216-
cutOffDate.Year(), cutOffDate.Month(), cutOffDate.Day(),
217-
cutOffDate.Hour(), cutOffDate.Minute(), cutOffDate.Second())
162+
req.Header.Add("PRIVATE-TOKEN", g.privateToken)
163+
req.Header.Set("Content-Type", "application/json")
218164

219-
for {
220-
out, err := exec.Command("curl", "-s", "--request", "GET",
221-
fmt.Sprintf("https://gitlab.com/api/v4/projects?last_activity_before=%s&owned=yes&simple=yes&page=%d", formattedDate, pageNum),
222-
"--header", fmt.Sprintf("PRIVATE-TOKEN: %s", g.privateToken)).CombinedOutput()
223-
224-
if err != nil {
225-
return result, fmt.Errorf("Failure retrieving obsolete repos: %w", err)
226-
}
227-
228-
if len(out) <= 2 {
229-
break
230-
}
231-
232-
pageNum++
233-
var response []interface{}
234-
235-
err = json.Unmarshal(out, &response)
236-
if err != nil {
237-
return nil, fmt.Errorf("%s: %w", string(out), err)
238-
}
239-
240-
for i := range response {
241-
m := response[i].(map[string]interface{})
242-
if flt, found := m["id"]; found {
243-
var id float64
244-
var ok bool
245-
if id, ok = flt.(float64); !ok {
246-
return result, fmt.Errorf("Project id in the response isn't a float: %w", err)
247-
}
248-
result = append(result, fmt.Sprintf("%.0f", id))
249-
250-
} else {
251-
return result, fmt.Errorf("Project id wasn't found in the response: %w", err)
252-
}
253-
}
165+
resp, err := g.httpClient.Do(req)
166+
if err != nil {
167+
return nil, fmt.Errorf("error sending request: %w", err)
254168
}
255169

256-
return result, nil
170+
return resp, nil
257171
}

0 commit comments

Comments
 (0)