@@ -16,31 +16,39 @@ package gitproviders
1616
1717import (
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
3029const (
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.
3735type 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
5466func (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.
7082func (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.
175148func (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}
0 commit comments