2022-03-07 12:16:11 +00:00
|
|
|
package upload
|
2020-12-02 15:09:37 +00:00
|
|
|
|
|
|
|
import (
|
|
|
|
"archive/zip"
|
|
|
|
"bytes"
|
|
|
|
"crypto/md5"
|
|
|
|
"encoding/hex"
|
|
|
|
"fmt"
|
2022-06-09 12:08:25 +00:00
|
|
|
"io"
|
2020-12-02 15:09:37 +00:00
|
|
|
"mime/multipart"
|
|
|
|
"net/http"
|
|
|
|
"net/http/httptest"
|
2022-06-09 12:08:25 +00:00
|
|
|
"os"
|
2020-12-02 15:09:37 +00:00
|
|
|
"testing"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
|
2021-07-21 15:08:52 +00:00
|
|
|
"gitlab.com/gitlab-org/gitlab/workhorse/internal/api"
|
|
|
|
"gitlab.com/gitlab-org/gitlab/workhorse/internal/testhelper"
|
2022-03-15 12:07:44 +00:00
|
|
|
"gitlab.com/gitlab-org/gitlab/workhorse/internal/upload/destination/objectstore/test"
|
2020-12-02 15:09:37 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
func createTestZipArchive(t *testing.T) (data []byte, md5Hash string) {
|
|
|
|
var buffer bytes.Buffer
|
|
|
|
archive := zip.NewWriter(&buffer)
|
|
|
|
fileInArchive, err := archive.Create("test.file")
|
|
|
|
require.NoError(t, err)
|
|
|
|
fmt.Fprint(fileInArchive, "test")
|
|
|
|
archive.Close()
|
|
|
|
data = buffer.Bytes()
|
|
|
|
|
|
|
|
hasher := md5.New()
|
|
|
|
hasher.Write(data)
|
|
|
|
hexHash := hasher.Sum(nil)
|
|
|
|
md5Hash = hex.EncodeToString(hexHash)
|
|
|
|
|
|
|
|
return data, md5Hash
|
|
|
|
}
|
|
|
|
|
|
|
|
func createTestMultipartForm(t *testing.T, data []byte) (bytes.Buffer, string) {
|
|
|
|
var buffer bytes.Buffer
|
|
|
|
writer := multipart.NewWriter(&buffer)
|
|
|
|
file, err := writer.CreateFormFile("file", "my.file")
|
|
|
|
require.NoError(t, err)
|
|
|
|
file.Write(data)
|
|
|
|
writer.Close()
|
|
|
|
return buffer, writer.FormDataContentType()
|
|
|
|
}
|
|
|
|
|
|
|
|
func testUploadArtifactsFromTestZip(t *testing.T, ts *httptest.Server) *httptest.ResponseRecorder {
|
|
|
|
archiveData, _ := createTestZipArchive(t)
|
|
|
|
contentBuffer, contentType := createTestMultipartForm(t, archiveData)
|
|
|
|
|
|
|
|
return testUploadArtifacts(t, contentType, ts.URL+Path, &contentBuffer)
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestUploadHandlerSendingToExternalStorage(t *testing.T) {
|
2022-05-30 06:09:32 +00:00
|
|
|
tempPath := t.TempDir()
|
2020-12-02 15:09:37 +00:00
|
|
|
|
|
|
|
archiveData, md5 := createTestZipArchive(t)
|
2022-06-09 12:08:25 +00:00
|
|
|
archiveFile, err := os.CreateTemp(tempPath, "artifact.zip")
|
2020-12-02 15:09:37 +00:00
|
|
|
require.NoError(t, err)
|
|
|
|
_, err = archiveFile.Write(archiveData)
|
|
|
|
require.NoError(t, err)
|
|
|
|
archiveFile.Close()
|
|
|
|
|
|
|
|
storeServerCalled := 0
|
|
|
|
storeServerMux := http.NewServeMux()
|
|
|
|
storeServerMux.HandleFunc("/url/put", func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
require.Equal(t, "PUT", r.Method)
|
|
|
|
|
2022-06-09 12:08:25 +00:00
|
|
|
receivedData, err := io.ReadAll(r.Body)
|
2020-12-02 15:09:37 +00:00
|
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, archiveData, receivedData)
|
|
|
|
|
|
|
|
storeServerCalled++
|
|
|
|
w.Header().Set("ETag", md5)
|
|
|
|
w.WriteHeader(200)
|
|
|
|
})
|
|
|
|
storeServerMux.HandleFunc("/store-id", func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
http.ServeFile(w, r, archiveFile.Name())
|
|
|
|
})
|
|
|
|
|
|
|
|
responseProcessorCalled := 0
|
|
|
|
responseProcessor := func(w http.ResponseWriter, r *http.Request) {
|
2022-06-23 03:08:49 +00:00
|
|
|
require.Equal(t, "store-id", r.FormValue("file.remote_id"))
|
|
|
|
require.NotEmpty(t, r.FormValue("file.remote_url"))
|
2020-12-02 15:09:37 +00:00
|
|
|
w.WriteHeader(200)
|
|
|
|
responseProcessorCalled++
|
|
|
|
}
|
|
|
|
|
|
|
|
storeServer := httptest.NewServer(storeServerMux)
|
|
|
|
defer storeServer.Close()
|
|
|
|
|
|
|
|
qs := fmt.Sprintf("?%s=%s", ArtifactFormatKey, ArtifactFormatZip)
|
|
|
|
|
|
|
|
tests := []struct {
|
|
|
|
name string
|
2021-10-11 09:09:08 +00:00
|
|
|
preauth *api.Response
|
2020-12-02 15:09:37 +00:00
|
|
|
}{
|
|
|
|
{
|
|
|
|
name: "ObjectStore Upload",
|
2021-10-11 09:09:08 +00:00
|
|
|
preauth: &api.Response{
|
2020-12-02 15:09:37 +00:00
|
|
|
RemoteObject: api.RemoteObject{
|
|
|
|
StoreURL: storeServer.URL + "/url/put" + qs,
|
|
|
|
ID: "store-id",
|
|
|
|
GetURL: storeServer.URL + "/store-id",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, test := range tests {
|
|
|
|
t.Run(test.name, func(t *testing.T) {
|
|
|
|
storeServerCalled = 0
|
|
|
|
responseProcessorCalled = 0
|
|
|
|
|
|
|
|
ts := testArtifactsUploadServer(t, test.preauth, responseProcessor)
|
|
|
|
defer ts.Close()
|
|
|
|
|
|
|
|
contentBuffer, contentType := createTestMultipartForm(t, archiveData)
|
|
|
|
response := testUploadArtifacts(t, contentType, ts.URL+Path+qs, &contentBuffer)
|
|
|
|
require.Equal(t, http.StatusOK, response.Code)
|
|
|
|
testhelper.RequireResponseHeader(t, response, MetadataHeaderKey, MetadataHeaderPresent)
|
|
|
|
require.Equal(t, 1, storeServerCalled, "store should be called only once")
|
|
|
|
require.Equal(t, 1, responseProcessorCalled, "response processor should be called only once")
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestUploadHandlerSendingToExternalStorageAndStorageServerUnreachable(t *testing.T) {
|
2022-05-30 06:09:32 +00:00
|
|
|
tempPath := t.TempDir()
|
2020-12-02 15:09:37 +00:00
|
|
|
|
|
|
|
responseProcessor := func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
t.Fatal("it should not be called")
|
|
|
|
}
|
|
|
|
|
2021-10-11 09:09:08 +00:00
|
|
|
authResponse := &api.Response{
|
2020-12-02 15:09:37 +00:00
|
|
|
TempPath: tempPath,
|
|
|
|
RemoteObject: api.RemoteObject{
|
|
|
|
StoreURL: "http://localhost:12323/invalid/url",
|
|
|
|
ID: "store-id",
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
ts := testArtifactsUploadServer(t, authResponse, responseProcessor)
|
|
|
|
defer ts.Close()
|
|
|
|
|
|
|
|
response := testUploadArtifactsFromTestZip(t, ts)
|
|
|
|
require.Equal(t, http.StatusInternalServerError, response.Code)
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestUploadHandlerSendingToExternalStorageAndInvalidURLIsUsed(t *testing.T) {
|
2022-05-30 06:09:32 +00:00
|
|
|
tempPath := t.TempDir()
|
2020-12-02 15:09:37 +00:00
|
|
|
|
|
|
|
responseProcessor := func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
t.Fatal("it should not be called")
|
|
|
|
}
|
|
|
|
|
2021-10-11 09:09:08 +00:00
|
|
|
authResponse := &api.Response{
|
2020-12-02 15:09:37 +00:00
|
|
|
TempPath: tempPath,
|
|
|
|
RemoteObject: api.RemoteObject{
|
|
|
|
StoreURL: "htt:////invalid-url",
|
|
|
|
ID: "store-id",
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
ts := testArtifactsUploadServer(t, authResponse, responseProcessor)
|
|
|
|
defer ts.Close()
|
|
|
|
|
|
|
|
response := testUploadArtifactsFromTestZip(t, ts)
|
|
|
|
require.Equal(t, http.StatusInternalServerError, response.Code)
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestUploadHandlerSendingToExternalStorageAndItReturnsAnError(t *testing.T) {
|
|
|
|
putCalledTimes := 0
|
|
|
|
|
|
|
|
storeServerMux := http.NewServeMux()
|
|
|
|
storeServerMux.HandleFunc("/url/put", func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
putCalledTimes++
|
|
|
|
require.Equal(t, "PUT", r.Method)
|
|
|
|
w.WriteHeader(510)
|
|
|
|
})
|
|
|
|
|
|
|
|
responseProcessor := func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
t.Fatal("it should not be called")
|
|
|
|
}
|
|
|
|
|
|
|
|
storeServer := httptest.NewServer(storeServerMux)
|
|
|
|
defer storeServer.Close()
|
|
|
|
|
2021-10-11 09:09:08 +00:00
|
|
|
authResponse := &api.Response{
|
2020-12-02 15:09:37 +00:00
|
|
|
RemoteObject: api.RemoteObject{
|
|
|
|
StoreURL: storeServer.URL + "/url/put",
|
|
|
|
ID: "store-id",
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
ts := testArtifactsUploadServer(t, authResponse, responseProcessor)
|
|
|
|
defer ts.Close()
|
|
|
|
|
|
|
|
response := testUploadArtifactsFromTestZip(t, ts)
|
|
|
|
require.Equal(t, http.StatusInternalServerError, response.Code)
|
|
|
|
require.Equal(t, 1, putCalledTimes, "upload should be called only once")
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestUploadHandlerSendingToExternalStorageAndSupportRequestTimeout(t *testing.T) {
|
2022-07-13 15:09:14 +00:00
|
|
|
shutdown := make(chan struct{})
|
2020-12-02 15:09:37 +00:00
|
|
|
storeServerMux := http.NewServeMux()
|
|
|
|
storeServerMux.HandleFunc("/url/put", func(w http.ResponseWriter, r *http.Request) {
|
2022-07-13 15:09:14 +00:00
|
|
|
<-shutdown
|
2020-12-02 15:09:37 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
responseProcessor := func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
t.Fatal("it should not be called")
|
|
|
|
}
|
|
|
|
|
|
|
|
storeServer := httptest.NewServer(storeServerMux)
|
2022-07-13 15:09:14 +00:00
|
|
|
defer func() {
|
|
|
|
close(shutdown)
|
|
|
|
storeServer.Close()
|
|
|
|
}()
|
2020-12-02 15:09:37 +00:00
|
|
|
|
2021-10-11 09:09:08 +00:00
|
|
|
authResponse := &api.Response{
|
2020-12-02 15:09:37 +00:00
|
|
|
RemoteObject: api.RemoteObject{
|
|
|
|
StoreURL: storeServer.URL + "/url/put",
|
|
|
|
ID: "store-id",
|
2022-10-28 09:10:15 +00:00
|
|
|
Timeout: 0.1,
|
2020-12-02 15:09:37 +00:00
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
ts := testArtifactsUploadServer(t, authResponse, responseProcessor)
|
|
|
|
defer ts.Close()
|
|
|
|
|
|
|
|
response := testUploadArtifactsFromTestZip(t, ts)
|
2022-07-13 15:09:14 +00:00
|
|
|
// HTTP status 504 (gateway timeout) proves that the timeout was enforced
|
|
|
|
require.Equal(t, http.StatusGatewayTimeout, response.Code)
|
2020-12-02 15:09:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func TestUploadHandlerMultipartUploadSizeLimit(t *testing.T) {
|
|
|
|
os, server := test.StartObjectStore()
|
|
|
|
defer server.Close()
|
|
|
|
|
|
|
|
err := os.InitiateMultipartUpload(test.ObjectPath)
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
objectURL := server.URL + test.ObjectPath
|
|
|
|
|
|
|
|
uploadSize := 10
|
2021-10-11 09:09:08 +00:00
|
|
|
preauth := &api.Response{
|
2020-12-02 15:09:37 +00:00
|
|
|
RemoteObject: api.RemoteObject{
|
|
|
|
ID: "store-id",
|
|
|
|
MultipartUpload: &api.MultipartUploadParams{
|
|
|
|
PartSize: 1,
|
|
|
|
PartURLs: []string{objectURL + "?partNumber=1"},
|
|
|
|
AbortURL: objectURL, // DELETE
|
|
|
|
CompleteURL: objectURL, // POST
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
responseProcessor := func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
t.Fatal("it should not be called")
|
|
|
|
}
|
|
|
|
|
|
|
|
ts := testArtifactsUploadServer(t, preauth, responseProcessor)
|
|
|
|
defer ts.Close()
|
|
|
|
|
|
|
|
contentBuffer, contentType := createTestMultipartForm(t, make([]byte, uploadSize))
|
|
|
|
response := testUploadArtifacts(t, contentType, ts.URL+Path, &contentBuffer)
|
|
|
|
require.Equal(t, http.StatusRequestEntityTooLarge, response.Code)
|
2022-02-11 12:19:13 +00:00
|
|
|
require.Eventually(t, func() bool {
|
|
|
|
return !os.IsMultipartUpload(test.ObjectPath)
|
|
|
|
}, time.Second, time.Millisecond, "MultipartUpload should not be in progress anymore")
|
2020-12-02 15:09:37 +00:00
|
|
|
require.Empty(t, os.GetObjectMD5(test.ObjectPath), "upload should have failed, so the object should not exists")
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestUploadHandlerMultipartUploadMaximumSizeFromApi(t *testing.T) {
|
|
|
|
os, server := test.StartObjectStore()
|
|
|
|
defer server.Close()
|
|
|
|
|
|
|
|
err := os.InitiateMultipartUpload(test.ObjectPath)
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
objectURL := server.URL + test.ObjectPath
|
|
|
|
|
|
|
|
uploadSize := int64(10)
|
|
|
|
maxSize := uploadSize - 1
|
2021-10-11 09:09:08 +00:00
|
|
|
preauth := &api.Response{
|
2020-12-02 15:09:37 +00:00
|
|
|
MaximumSize: maxSize,
|
|
|
|
RemoteObject: api.RemoteObject{
|
|
|
|
ID: "store-id",
|
|
|
|
MultipartUpload: &api.MultipartUploadParams{
|
|
|
|
PartSize: uploadSize,
|
|
|
|
PartURLs: []string{objectURL + "?partNumber=1"},
|
|
|
|
AbortURL: objectURL, // DELETE
|
|
|
|
CompleteURL: objectURL, // POST
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
responseProcessor := func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
t.Fatal("it should not be called")
|
|
|
|
}
|
|
|
|
|
|
|
|
ts := testArtifactsUploadServer(t, preauth, responseProcessor)
|
|
|
|
defer ts.Close()
|
|
|
|
|
|
|
|
contentBuffer, contentType := createTestMultipartForm(t, make([]byte, uploadSize))
|
|
|
|
response := testUploadArtifacts(t, contentType, ts.URL+Path, &contentBuffer)
|
|
|
|
require.Equal(t, http.StatusRequestEntityTooLarge, response.Code)
|
|
|
|
|
|
|
|
testhelper.Retry(t, 5*time.Second, func() error {
|
|
|
|
if os.GetObjectMD5(test.ObjectPath) == "" {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
return fmt.Errorf("file is still present")
|
|
|
|
})
|
|
|
|
}
|