1515package gitproviders
1616
1717import (
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
3030const (
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.
3737type 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
5466func (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.
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+ 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.
175152func (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