Skip to content

Commit 133490c

Browse files
authored
feat: add pipeline schedule variables to GitLab variables command (#324)
* feat: add pipeline schedule variables to GitLab variables command
1 parent aac0933 commit 133490c

3 files changed

Lines changed: 103 additions & 185 deletions

File tree

src/pipeleak/cmd/gitlab/variables.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ func FetchVariables(cmd *cobra.Command, args []string) {
7373
if len(pvs) > 0 {
7474
log.Warn().Str("project", project.WebURL).Any("variables", pvs).Msg("Project variables")
7575
}
76+
77+
fetchPipelineScheduleVariables(git, project)
7678
}
7779

7880
if resp.NextPage == 0 {
@@ -129,3 +131,51 @@ func FetchVariables(cmd *cobra.Command, args []string) {
129131

130132
log.Info().Msg("Fetched all variables")
131133
}
134+
135+
func fetchPipelineScheduleVariables(git *gitlab.Client, project *gitlab.Project) {
136+
scheduleOpts := &gitlab.ListPipelineSchedulesOptions{
137+
ListOptions: gitlab.ListOptions{
138+
PerPage: 100,
139+
Page: 1,
140+
},
141+
}
142+
143+
for {
144+
schedules, resp, err := git.PipelineSchedules.ListPipelineSchedules(project.ID, scheduleOpts)
145+
146+
if resp == nil {
147+
return
148+
}
149+
150+
// If we get a 404, the project has no schedules
151+
if resp.StatusCode == 404 {
152+
return
153+
}
154+
155+
if err != nil {
156+
log.Error().Stack().Err(err).Int("project", project.ID).Msg("Failed fetching pipeline schedules")
157+
break
158+
}
159+
160+
for _, schedule := range schedules {
161+
detailedSchedule, _, err := git.PipelineSchedules.GetPipelineSchedule(project.ID, schedule.ID)
162+
if err != nil {
163+
log.Error().Stack().Err(err).Int("scheduleID", schedule.ID).Msg("Failed fetching pipeline schedule details")
164+
continue
165+
}
166+
167+
if len(detailedSchedule.Variables) > 0 {
168+
log.Warn().
169+
Str("project", project.WebURL).
170+
Str("schedule", detailedSchedule.Description).
171+
Any("variables", detailedSchedule.Variables).
172+
Msg("Pipeline schedule variables")
173+
}
174+
}
175+
176+
if resp.NextPage == 0 {
177+
break
178+
}
179+
scheduleOpts.Page = resp.NextPage
180+
}
181+
}

src/pipeleak/tests/e2e/e2e_helpers_test.go

Lines changed: 0 additions & 183 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,10 @@ import (
2020
"net/http"
2121
"net/http/httptest"
2222
"os"
23-
"regexp"
2423
"strings"
2524
"sync"
2625
"testing"
2726
"time"
28-
29-
"github.com/google/go-cmp/cmp"
3027
)
3128

3229
// RecordedRequest captures details of an HTTP request received by the mock server
@@ -245,89 +242,6 @@ func assertLogContains(t *testing.T, output string, expected []string) {
245242
}
246243
}
247244

248-
// assertLogNotContains checks if the output does NOT contain specified strings
249-
//
250-
//nolint:unused
251-
func assertLogNotContains(t *testing.T, output string, forbidden []string) {
252-
t.Helper()
253-
for _, forb := range forbidden {
254-
if strings.Contains(output, forb) {
255-
t.Errorf("Expected output to NOT contain %q, but it did.\nOutput:\n%s", forb, output)
256-
}
257-
}
258-
}
259-
260-
// assertLogMatchesRegex checks if the output matches all provided regex patterns
261-
//
262-
// Parameters:
263-
// - t: testing.T instance
264-
// - output: log output to match against
265-
// - patterns: slice of regex pattern strings
266-
//
267-
// Example:
268-
//
269-
// assertLogMatchesRegex(t, stdout, []string{`\d+ secrets found`, `Scan completed in \d+\.\d+s`})
270-
//
271-
//nolint:unused
272-
func assertLogMatchesRegex(t *testing.T, output string, patterns []string) {
273-
t.Helper()
274-
for _, pattern := range patterns {
275-
matched, err := regexp.MatchString(pattern, output)
276-
if err != nil {
277-
t.Fatalf("Invalid regex pattern %q: %v", pattern, err)
278-
}
279-
if !matched {
280-
t.Errorf("Expected output to match pattern %q, but it didn't.\nOutput:\n%s", pattern, output)
281-
}
282-
}
283-
}
284-
285-
// compareJSON compares two JSON strings for structural equality
286-
//
287-
// This function unmarshals both strings and uses go-cmp to compare them,
288-
// providing detailed diff output on failure.
289-
//
290-
// Parameters:
291-
// - t: testing.T instance
292-
// - got: actual JSON string
293-
// - want: expected JSON string
294-
//
295-
// Example:
296-
//
297-
// compareJSON(t, stdout, `{"status":"success","count":5}`)
298-
//
299-
//nolint:unused
300-
func compareJSON(t *testing.T, got, want string) {
301-
t.Helper()
302-
303-
var gotData, wantData interface{}
304-
305-
if err := json.Unmarshal([]byte(got), &gotData); err != nil {
306-
t.Fatalf("Failed to unmarshal 'got' JSON: %v\nJSON:\n%s", err, got)
307-
}
308-
309-
if err := json.Unmarshal([]byte(want), &wantData); err != nil {
310-
t.Fatalf("Failed to unmarshal 'want' JSON: %v\nJSON:\n%s", err, want)
311-
}
312-
313-
if diff := cmp.Diff(wantData, gotData); diff != "" {
314-
t.Errorf("JSON mismatch (-want +got):\n%s", diff)
315-
}
316-
}
317-
318-
// assertRequestCount verifies the number of HTTP requests received
319-
//
320-
//nolint:unused
321-
func assertRequestCount(t *testing.T, requests []RecordedRequest, expected int) {
322-
t.Helper()
323-
if len(requests) != expected {
324-
t.Errorf("Expected %d requests, got %d", expected, len(requests))
325-
for i, req := range requests {
326-
t.Logf("Request %d: %s %s", i+1, req.Method, req.Path)
327-
}
328-
}
329-
}
330-
331245
// assertRequestMethodAndPath verifies a request has the expected method and path
332246
func assertRequestMethodAndPath(t *testing.T, req RecordedRequest, method, path string) {
333247
t.Helper()
@@ -348,40 +262,7 @@ func assertRequestHeader(t *testing.T, req RecordedRequest, header, expected str
348262
}
349263
}
350264

351-
// assertRequestHeaderContains verifies a request header contains a substring
352-
//
353-
//nolint:unused
354-
func assertRequestHeaderContains(t *testing.T, req RecordedRequest, header, substring string) {
355-
t.Helper()
356-
actual := req.Headers.Get(header)
357-
if !strings.Contains(actual, substring) {
358-
t.Errorf("Expected header %s to contain %q, got %q", header, substring, actual)
359-
}
360-
}
361-
362-
// assertRequestBody verifies the request body matches expected content
363-
//
364-
//nolint:unused
365-
func assertRequestBody(t *testing.T, req RecordedRequest, expected string) {
366-
t.Helper()
367-
actual := string(req.Body)
368-
if actual != expected {
369-
t.Errorf("Request body mismatch:\nExpected: %s\nGot: %s", expected, actual)
370-
}
371-
}
372-
373-
// assertRequestBodyJSON compares request body as JSON
374-
//
375-
//nolint:unused
376-
//nolint:unused
377-
func assertRequestBodyJSON(t *testing.T, req RecordedRequest, expected string) {
378-
t.Helper()
379-
compareJSON(t, string(req.Body), expected)
380-
}
381-
382265
// dumpRequests prints all recorded requests for debugging
383-
//
384-
//nolint:unused
385266
func dumpRequests(t *testing.T, requests []RecordedRequest) {
386267
t.Helper()
387268
t.Log("Recorded HTTP requests:")
@@ -402,47 +283,6 @@ func dumpRequests(t *testing.T, requests []RecordedRequest) {
402283
}
403284
}
404285

405-
// mockGitLabHandler returns a handler for common GitLab API endpoints
406-
//
407-
//nolint:unused
408-
func mockGitLabHandler(t *testing.T, responses map[string]interface{}) http.HandlerFunc {
409-
return func(w http.ResponseWriter, r *http.Request) {
410-
// Set common headers
411-
w.Header().Set("Content-Type", "application/json")
412-
413-
// Route to appropriate response
414-
key := r.Method + " " + r.URL.Path
415-
if response, ok := responses[key]; ok {
416-
if statusCode, ok := response.(int); ok && statusCode >= 400 {
417-
w.WriteHeader(statusCode)
418-
_ = json.NewEncoder(w).Encode(map[string]string{
419-
"error": "API error",
420-
})
421-
return
422-
}
423-
w.WriteHeader(http.StatusOK)
424-
_ = json.NewEncoder(w).Encode(response)
425-
return
426-
}
427-
428-
// Default 404 response
429-
w.WriteHeader(http.StatusNotFound)
430-
_ = json.NewEncoder(w).Encode(map[string]string{
431-
"error": "not found",
432-
})
433-
}
434-
}
435-
436-
// withTimeout wraps a handler with a delay for testing timeout scenarios
437-
//
438-
//nolint:unused
439-
func withTimeout(handler http.HandlerFunc, delay time.Duration) http.HandlerFunc {
440-
return func(w http.ResponseWriter, r *http.Request) {
441-
time.Sleep(delay)
442-
handler(w, r)
443-
}
444-
}
445-
446286
// withError returns a handler that always returns an error status
447287
func withError(statusCode int, message string) http.HandlerFunc {
448288
return func(w http.ResponseWriter, r *http.Request) {
@@ -466,26 +306,3 @@ func mockSuccessResponse() http.HandlerFunc {
466306
})
467307
}
468308
}
469-
470-
// createTempConfigFile creates a temporary config file for testing
471-
//
472-
//nolint:unused
473-
func createTempConfigFile(t *testing.T, content string) string {
474-
t.Helper()
475-
tmpDir := t.TempDir()
476-
configPath := fmt.Sprintf("%s/config.yaml", tmpDir)
477-
err := os.WriteFile(configPath, []byte(content), 0644)
478-
if err != nil {
479-
t.Fatalf("Failed to create temp config file: %v", err)
480-
}
481-
return configPath
482-
}
483-
484-
// skipIfShort skips the test if running in short mode
485-
//
486-
//nolint:unused
487-
func skipIfShort(t *testing.T, reason string) {
488-
if testing.Short() {
489-
t.Skipf("Skipping in short mode: %s", reason)
490-
}
491-
}

src/pipeleak/tests/e2e/gitlab_commands_test.go

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ func TestGitLabVariables(t *testing.T) {
1818
case "/api/v4/projects":
1919
w.WriteHeader(http.StatusOK)
2020
_ = json.NewEncoder(w).Encode([]map[string]interface{}{
21-
{"id": 1, "name": "test-project"},
21+
{"id": 1, "name": "test-project", "web_url": "https://gitlab.example.com/test-project"},
2222
})
2323

2424
case "/api/v4/projects/1/variables":
@@ -33,6 +33,30 @@ func TestGitLabVariables(t *testing.T) {
3333
},
3434
})
3535

36+
case "/api/v4/projects/1/pipeline_schedules":
37+
w.WriteHeader(http.StatusOK)
38+
_ = json.NewEncoder(w).Encode([]map[string]interface{}{
39+
{
40+
"id": 1,
41+
"description": "Nightly build",
42+
"ref": "main",
43+
},
44+
})
45+
46+
case "/api/v4/projects/1/pipeline_schedules/1":
47+
w.WriteHeader(http.StatusOK)
48+
_ = json.NewEncoder(w).Encode(map[string]interface{}{
49+
"id": 1,
50+
"description": "Nightly build",
51+
"ref": "main",
52+
"variables": []map[string]interface{}{
53+
{
54+
"key": "DEPLOY_ENV",
55+
"value": "production",
56+
},
57+
},
58+
})
59+
3660
default:
3761
w.WriteHeader(http.StatusOK)
3862
_ = json.NewEncoder(w).Encode([]map[string]interface{}{})
@@ -50,15 +74,42 @@ func TestGitLabVariables(t *testing.T) {
5074

5175
requests := getRequests()
5276
variablesRequestFound := false
77+
schedulesRequestFound := false
78+
scheduleDetailsRequestFound := false
79+
5380
for _, req := range requests {
5481
if req.Path == "/api/v4/projects/1/variables" {
5582
variablesRequestFound = true
5683
assertRequestMethodAndPath(t, req, "GET", "/api/v4/projects/1/variables")
57-
break
84+
}
85+
if req.Path == "/api/v4/projects/1/pipeline_schedules" {
86+
schedulesRequestFound = true
87+
assertRequestMethodAndPath(t, req, "GET", "/api/v4/projects/1/pipeline_schedules")
88+
}
89+
if req.Path == "/api/v4/projects/1/pipeline_schedules/1" {
90+
scheduleDetailsRequestFound = true
91+
assertRequestMethodAndPath(t, req, "GET", "/api/v4/projects/1/pipeline_schedules/1")
5892
}
5993
}
6094

95+
assert.True(t, variablesRequestFound, "Should request project variables")
96+
assert.True(t, schedulesRequestFound, "Should request pipeline schedules")
97+
assert.True(t, scheduleDetailsRequestFound, "Should request pipeline schedule details")
98+
99+
// Assert that project variables are printed in stdout
100+
assert.Contains(t, stdout, "Project variables", "Should log project variables")
101+
assert.Contains(t, stdout, "DATABASE_URL", "Should contain the DATABASE_URL variable key")
102+
assert.Contains(t, stdout, "postgres://user:pass@localhost/db", "Should contain the DATABASE_URL variable value")
103+
104+
// Assert that pipeline schedule variables are printed in stdout
105+
assert.Contains(t, stdout, "Pipeline schedule variables", "Should log pipeline schedule variables")
106+
assert.Contains(t, stdout, "Nightly build", "Should contain the schedule description")
107+
assert.Contains(t, stdout, "DEPLOY_ENV", "Should contain the DEPLOY_ENV variable key")
108+
assert.Contains(t, stdout, "production", "Should contain the DEPLOY_ENV variable value")
109+
61110
t.Logf("Variables request made: %v", variablesRequestFound)
111+
t.Logf("Schedules request made: %v", schedulesRequestFound)
112+
t.Logf("Schedule details request made: %v", scheduleDetailsRequestFound)
62113
t.Logf("STDOUT:\n%s", stdout)
63114
t.Logf("STDERR:\n%s", stderr)
64115
}

0 commit comments

Comments
 (0)