Skip to content

Commit 5d20510

Browse files
committed
test: reuse Gitlab repos
1 parent db914eb commit 5d20510

File tree

4 files changed

+149
-173
lines changed

4 files changed

+149
-173
lines changed

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: 79 additions & 169 deletions
Original file line numberDiff line numberDiff line change
@@ -16,31 +16,39 @@ package gitproviders
1616

1717
import (
1818
"encoding/json"
19-
"errors"
2019
"fmt"
21-
"os/exec"
22-
"strings"
20+
"io"
21+
"net/http"
2322
"time"
2423

2524
"github.com/GoogleContainerTools/config-sync/e2e"
26-
"github.com/google/uuid"
27-
"go.uber.org/multierr"
25+
"github.com/GoogleContainerTools/config-sync/e2e/nomostest/gitproviders/util"
26+
"github.com/GoogleContainerTools/config-sync/e2e/nomostest/testlogger"
2827
)
2928

3029
const (
31-
projectNameMaxLength = 256
32-
groupID = 15698791
33-
groupName = "configsync"
30+
groupID = 15698791
31+
groupName = "configsync"
3432
)
3533

3634
// GitlabClient is the client that will call Gitlab REST APIs.
3735
type GitlabClient struct {
3836
privateToken string
37+
logger *testlogger.TestLogger
38+
// repoSuffix is used to avoid overlap
39+
repoSuffix string
40+
httpClient *http.Client
3941
}
4042

4143
// newGitlabClient instantiates a new GitlabClient.
42-
func newGitlabClient() (*GitlabClient, error) {
43-
client := &GitlabClient{}
44+
func newGitlabClient(repoSuffix string, logger *testlogger.TestLogger) (*GitlabClient, error) {
45+
client := &GitlabClient{
46+
logger: logger,
47+
repoSuffix: repoSuffix,
48+
httpClient: &http.Client{
49+
Timeout: 10 * time.Second,
50+
},
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,88 @@ 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+
resp, err := g.sendRequest(http.MethodGet, repoURL, nil)
7288
if err != nil {
73-
return "", fmt.Errorf("failed to generate a new UUID: %w", err)
89+
return "", err
7490
}
7591

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-
}
92+
if resp.StatusCode == http.StatusOK {
93+
body, err := io.ReadAll(resp.Body)
94+
if err != nil {
95+
return "", fmt.Errorf("error reading response body: %w", err)
96+
}
8397

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()
98+
var output []map[string]interface{}
99+
if err := json.Unmarshal(body, &output); err != nil {
100+
return "", fmt.Errorf("unmarshalling project search response: %w", err)
101+
}
89102

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-
}
103+
// the assumption is that our project name is unique, so we'll get exactly 1 result
104+
if len(output) > 0 {
105+
return fullName, nil
96106

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

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()
112+
// Creates repository
113+
repoURL = fmt.Sprintf("https://gitlab.com/api/v4/projects?name=%s&namespace_id=%d&initialize_with_readme=true", fullName, groupID)
114+
resp, err = g.sendRequest(http.MethodPost, repoURL, nil)
106115

107116
if err != nil {
108-
return "", fmt.Errorf("Failure retrieving id for project %s: %w", name, err)
117+
return "", err
109118
}
110119

111-
var response []interface{}
112-
113-
err = json.Unmarshal(out, &response)
120+
defer func() {
121+
if closeErr := resp.Body.Close(); closeErr != nil {
122+
g.logger.Infof("failed to close response body: %v\n", closeErr)
123+
}
124+
}()
125+
body, err := io.ReadAll(resp.Body)
114126
if err != nil {
115-
return "", fmt.Errorf("%s: %w", string(out), err)
127+
return "", fmt.Errorf("error reading response body: %w", err)
116128
}
117129

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)
130+
if resp.StatusCode != http.StatusCreated {
131+
return "", fmt.Errorf("failed to create repository: status %d: %s", resp.StatusCode, string(body))
127132
}
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)
137133

138-
return id, nil
134+
return fullName, nil
139135
}
140136

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
137+
// DeleteRepositories is a no-op because Gitlab repo names are determined by the
138+
// test cluster name and RSync namespace and name, so they can be reset and reused
139+
// across test runs
140+
func (g *GitlabClient) DeleteRepositories(_ ...string) error {
141+
g.logger.Info("[Gitlab] Skip deletion of repos")
142+
return nil
172143
}
173144

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

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)
152+
// sendRequest sends an HTTP request to the Gitlab API.
153+
func (g *GitlabClient) sendRequest(method, url string, body io.Reader) (*http.Response, error) {
154+
req, err := http.NewRequest(method, url, body)
155+
if err != nil {
156+
return nil, fmt.Errorf("error creating request: %w", err)
206157
}
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())
158+
req.Header.Add("PRIVATE-TOKEN", g.privateToken)
159+
req.Header.Set("Content-Type", "application/json")
218160

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-
}
161+
resp, err := g.httpClient.Do(req)
162+
if err != nil {
163+
return nil, fmt.Errorf("error sending request: %w", err)
254164
}
255165

256-
return result, nil
166+
return resp, nil
257167
}

e2e/nomostest/gitproviders/util/reponame.go

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,10 @@ import (
2222
)
2323

2424
const (
25-
defaultRepoNameMaxLen = 63
26-
bitbucketRepoNameMaxLen = 62
27-
repoNameHashLen = 8
25+
defaultRepoNameMaxLen = 63
26+
bitbucketRepoNameMaxLen = 62
27+
gitlabProjectNameMaxLength = 256
28+
repoNameHashLen = 8
2829
)
2930

3031
// SanitizeGCPRepoName replaces all slashes with hyphens, and truncates the name.
@@ -63,6 +64,23 @@ func SanitizeBitbucketRepoName(repoSuffix, name string) string {
6364
return sanitize(fullName, hashStr, bitbucketRepoNameMaxLen)
6465
}
6566

67+
// SanitizeGitlabRepoName replaces all slashes with hyphens, and truncates the name for Gitlab.
68+
// repo name may contain between 3 and 256 lowercase letters, digits and hyphens.
69+
// The repo name will be of the form <name>-<repoSuffix>-<hash>
70+
func SanitizeGitlabRepoName(repoSuffix, name string) string {
71+
if name == "" {
72+
return name // Requires at least the base repo name
73+
}
74+
75+
fullName := name
76+
if repoSuffix != "" {
77+
fullName += "-" + repoSuffix
78+
}
79+
hashStr := hashName(fullName)
80+
81+
return sanitize(fullName, hashStr, gitlabProjectNameMaxLength)
82+
}
83+
6684
func hashName(fullName string) string {
6785
hashBytes := sha1.Sum([]byte(fullName))
6886
return hex.EncodeToString(hashBytes[:])[:repoNameHashLen]

0 commit comments

Comments
 (0)