Sitelet https://github.com/secureCodeBox/secureCodeBox/pull/3785/files
Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ jobs:
go vet ./...

- name: Test
working-directory: ./operator
working-directory: ./${{ matrix.component }}
run: task test

- name: Build Container Image
Expand Down
5 changes: 5 additions & 0 deletions lurker/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,8 @@
# SPDX-License-Identifier: Apache-2.0

main
lurker
bin

# Output of the go coverage tool
*.out
38 changes: 38 additions & 0 deletions lurker/Taskfile.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# SPDX-FileCopyrightText: the secureCodeBox authors
#
# SPDX-License-Identifier: Apache-2.0

version: "3.48.0"

tasks:
fmt:
desc: "Run go fmt against code"
dir: '{{ .TASKFILE_DIR }}'
cmds:
- go fmt ./...

vet:
desc: "Run go vet against code"
dir: '{{ .TASKFILE_DIR }}'
cmds:
- go vet ./...

test:
desc: "Run all tests"
deps: [fmt, vet]
dir: '{{ .TASKFILE_DIR }}'
cmds:
- go test ./... -coverprofile cover.out

view-coverage:
desc: "View test coverage in browser"
dir: '{{ .TASKFILE_DIR }}'
cmds:
- go tool cover -html=cover.out

build:
desc: "Build the lurker binary"
deps: [fmt, vet]
dir: '{{ .TASKFILE_DIR }}'
cmds:
- go build -o bin/lurker main.go
50 changes: 42 additions & 8 deletions lurker/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"context"
"flag"
"fmt"
"io"
"log"
"net/http"
"net/http/httputil"
Expand Down Expand Up @@ -55,13 +56,44 @@

log.Printf("Uploading result files.")
log.Printf("Uploading %s", filePath)
err = uploadFile(filePath, uploadURL)
err = uploadFileWithRetries(filePath, uploadURL)
if err != nil {
log.Fatal(err)
}
log.Printf("Uploaded file successfully")
}

// delays waited before each retry of a failed upload. The number of entries
// defines how often the upload is retried after the initial attempt.
var uploadBackoffs = []time.Duration{1 * time.Second, 3 * time.Second, 5 * time.Second, 10 * time.Second, 30 * time.Second}

// indirection to allow tests to run without actually waiting
var sleep = time.Sleep

// uploadFileWithRetries uploads the file and retries every failure (transport
// errors as well as non 2xx responses) using the uploadBackoffs schedule.
func uploadFileWithRetries(path, url string) error {
attempts := len(uploadBackoffs) + 1

var err error
for attempt := 1; attempt <= attempts; attempt++ {
err = uploadFile(path, url)
if err == nil {
return nil
}

if attempt == attempts {
break
}

backoff := uploadBackoffs[attempt-1]
log.Printf("Upload attempt %d of %d failed: %v. Retrying in %s", attempt, attempts, err, backoff)
sleep(backoff)
}

return fmt.Errorf("lurker failed to upload scan result file after %d attempts: %w", attempts, err)
}

func uploadFile(path, url string) error {
file, err := os.Open(path)
if err != nil {
Expand All @@ -81,7 +113,7 @@
// Create a new file upload request
req, err := http.NewRequest("PUT", url, file)
if err != nil {
log.Fatalf("Failed to create request: %v", err)
log.Printf("Failed to create request: %v", err)
return err
}

Expand Down Expand Up @@ -109,14 +141,16 @@
log.Printf("File upload returned non 2xx status code (%d)", res.StatusCode)

// Dump response for debugging purposes
resultBytes, err := httputil.DumpResponse(res, true)
if err != nil {
log.Fatal(fmt.Errorf("failed to dump out failed requests to upload scan report to the s3 bucket: %w", err))
resultBytes, dumpErr := httputil.DumpResponse(res, true)

Check warning on line 144 in lurker/main.go

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Make sure this debug feature is deactivated before delivering the code in production.

See more on https://sonarcloud.io/project/issues?id=secureCodeBox_secureCodeBox&issues=AaA5PEaTS_a-7DRiGpH3&open=AaA5PEaTS_a-7DRiGpH3&pullRequest=3785
if dumpErr != nil {
log.Printf("failed to dump out failed requests to upload scan report to the s3 bucket: %v", dumpErr)
// drain the body so that the connection can be reused by the next attempt
io.Copy(io.Discard, res.Body)
} else {
log.Println("Response of Failed Request:")
log.Println(string(resultBytes))
}

log.Println("Response of Failed Request:")
log.Println(string(resultBytes))

return fmt.Errorf("lurker failed to upload scan result file. File upload returned non 2xx status code (%d)", res.StatusCode)
}

Expand Down
162 changes: 162 additions & 0 deletions lurker/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
// SPDX-FileCopyrightText: the secureCodeBox authors
//
// SPDX-License-Identifier: Apache-2.0

package main

import (
"io"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
"time"
)

const testFileContents = `{"findings": []}`

// writeResultFile creates a temporary scan result file and returns its path.
func writeResultFile(t *testing.T) string {
t.Helper()

path := filepath.Join(t.TempDir(), "findings.json")
if err := os.WriteFile(path, []byte(testFileContents), 0o600); err != nil {
t.Fatalf("failed to write test file: %v", err)
}
return path
}

// stubSleep replaces the backoff sleep with a recorder, so that tests don't
// have to actually wait. Returns a pointer to the recorded delays.
func stubSleep(t *testing.T) *[]time.Duration {
t.Helper()

original := sleep
delays := []time.Duration{}
sleep = func(d time.Duration) {
delays = append(delays, d)
}
t.Cleanup(func() { sleep = original })

return &delays
}

func TestUploadFileWithRetriesSucceedsOnFirstAttempt(t *testing.T) {
delays := stubSleep(t)

requests := 0
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requests++
w.WriteHeader(http.StatusOK)
}))
defer server.Close()

if err := uploadFileWithRetries(writeResultFile(t), server.URL); err != nil {
t.Fatalf("expected upload to succeed, got: %v", err)
}
if requests != 1 {
t.Errorf("expected 1 request, got %d", requests)
}
if len(*delays) != 0 {
t.Errorf("expected no backoff, got %v", *delays)
}
}

func TestUploadFileWithRetriesRetriesUntilSuccess(t *testing.T) {
delays := stubSleep(t)

requests := 0
receivedBodies := []string{}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requests++
body, err := io.ReadAll(r.Body)
if err != nil {
t.Errorf("failed to read request body: %v", err)
}
receivedBodies = append(receivedBodies, string(body))

if requests < 3 {
w.WriteHeader(http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
}))
defer server.Close()

if err := uploadFileWithRetries(writeResultFile(t), server.URL); err != nil {
t.Fatalf("expected upload to succeed after retries, got: %v", err)
}
if requests != 3 {
t.Errorf("expected 3 requests, got %d", requests)
}

// every attempt, especially the last one, has to send the complete file
for attempt, body := range receivedBodies {
if body != testFileContents {
t.Errorf("attempt %d uploaded %q, expected %q", attempt+1, body, testFileContents)
}
}

expectedDelays := uploadBackoffs[:2]
if len(*delays) != len(expectedDelays) {
t.Fatalf("expected %d backoffs, got %v", len(expectedDelays), *delays)
}
for i, delay := range *delays {
if delay != expectedDelays[i] {
t.Errorf("backoff %d was %s, expected %s", i+1, delay, expectedDelays[i])
}
}
}

func TestUploadFileWithRetriesGivesUpAfterAllAttempts(t *testing.T) {
delays := stubSleep(t)

requests := 0
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requests++
w.WriteHeader(http.StatusInternalServerError)
}))
defer server.Close()

if err := uploadFileWithRetries(writeResultFile(t), server.URL); err == nil {
t.Fatal("expected upload to fail")
}

expectedRequests := len(uploadBackoffs) + 1
if requests != expectedRequests {
t.Errorf("expected %d requests, got %d", expectedRequests, requests)
}
if len(*delays) != len(uploadBackoffs) {
t.Errorf("expected %d backoffs, got %v", len(uploadBackoffs), *delays)
}
for i, delay := range *delays {
if delay != uploadBackoffs[i] {
t.Errorf("backoff %d was %s, expected %s", i+1, delay, uploadBackoffs[i])
}
}
}

func TestUploadFileWithRetriesRetriesTransportErrors(t *testing.T) {
delays := stubSleep(t)

// closing the server up front makes every attempt fail on the transport level
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
url := server.URL
server.Close()

if err := uploadFileWithRetries(writeResultFile(t), url); err == nil {
t.Fatal("expected upload to fail")
}
if len(*delays) != len(uploadBackoffs) {
t.Errorf("expected %d backoffs, got %v", len(uploadBackoffs), *delays)
}
}

func TestUploadFileWithRetriesFailsForMissingFile(t *testing.T) {
stubSleep(t)

if err := uploadFileWithRetries(filepath.Join(t.TempDir(), "does-not-exist.json"), "http://127.0.0.1:1"); err == nil {
t.Fatal("expected upload of a missing file to fail")
}
}
Loading