Skip to content

Commit f92b2ad

Browse files
authored
feat: add http download restore method (#483)
* add http download method and tests for sftp storage type * add http download method and tests for sftp storage type * feat: add http download restore method and tests for cache-cli using sftp as storage type
1 parent 5d1cab1 commit f92b2ad

File tree

14 files changed

+246
-5
lines changed

14 files changed

+246
-5
lines changed

cache-cli/.htpasswd

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
test:$apr1$kCL/ipwt$qOeC2mnzu9dJrmVYw2agO.

cache-cli/Dockerfile.sftp_server

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
1-
FROM ubuntu:18.04
1+
FROM ubuntu:22.04
22

3-
RUN apt-get update && apt-get install -y openssh-server
3+
RUN apt-get update && apt-get install -y openssh-server nginx
44
RUN mkdir -p /var/run/sshd
55

66
COPY sshd_config /etc/ssh/sshd_config
7+
COPY nginx.conf /etc/nginx/nginx.conf
8+
COPY default.conf /etc/nginx/sites-enabled/default
9+
COPY wrapper.sh /wrapper.sh
10+
COPY .htpasswd /etc/nginx/.htpasswd
711

812
RUN addgroup ftpaccess
913
RUN adduser tester --ingroup ftpaccess --shell /bin/bash --disabled-password --gecos ''
@@ -13,5 +17,6 @@ RUN mkdir /etc/ssh/authorized_keys
1317
COPY id_rsa.pub /tmp/id_rsa.pub
1418
RUN cat /tmp/id_rsa.pub >> /etc/ssh/authorized_keys/tester
1519

20+
EXPOSE 80
1621
EXPOSE 22
17-
CMD ["/usr/sbin/sshd", "-D"]
22+
CMD ["/wrapper.sh"]

cache-cli/cmd/restore.go

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ func findMatchingKey(availableKeys []storage.CacheKey, match string) string {
103103
func downloadAndUnpackKey(storage storage.Storage, archiver archive.Archiver, metricsManager metrics.MetricsManager, key string) {
104104
downloadStart := time.Now()
105105
log.Infof("Downloading key '%s'...", key)
106-
compressed, err := storage.Restore(key)
106+
compressed, err := downloadKey(storage, key)
107107
utils.Check(err)
108108

109109
downloadDuration := time.Since(downloadStart)
@@ -127,6 +127,29 @@ func downloadAndUnpackKey(storage storage.Storage, archiver archive.Archiver, me
127127
}
128128
}
129129

130+
func downloadKey(storage storage.Storage, key string) (*os.File, error) {
131+
backend := os.Getenv("SEMAPHORE_CACHE_BACKEND")
132+
133+
// If this is not an sftp backend, then we are not in a cloud environment,
134+
// and in there, there's no CDN variation, so just use the storage.
135+
if backend != "sftp" {
136+
return storage.Restore(key)
137+
}
138+
139+
// Here, we are using sftp, so we know we are in a cloud job.
140+
// But, not all cloud jobs should use this, so we only use it
141+
// if the SEMAPHORE_CACHE_CDN_* variables are defined
142+
cdnURL := os.Getenv("SEMAPHORE_CACHE_CDN_URL")
143+
cdnKey := os.Getenv("SEMAPHORE_CACHE_CDN_KEY")
144+
cdnSecret := os.Getenv("SEMAPHORE_CACHE_CDN_SECRET")
145+
if cdnURL == "" || cdnKey == "" || cdnSecret == "" {
146+
return storage.Restore(key)
147+
}
148+
149+
log.Infof("Restoring using HTTP URL %s...", cdnURL)
150+
return files.DownloadFromHTTP(cdnURL, cdnKey, cdnSecret, key)
151+
}
152+
130153
func publishMetrics(metricsManager metrics.MetricsManager, fileInfo fs.FileInfo, downloadDuration time.Duration) {
131154
metricsToPublish := []metrics.Metric{
132155
{Name: metrics.CacheDownloadSize, Value: fmt.Sprintf("%d", fileInfo.Size())},

cache-cli/cmd/restore_test.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,67 @@ func Test__Restore(t *testing.T) {
164164
os.Remove(tempDir)
165165
})
166166
})
167+
168+
runTestForSingleBackend(t, "sftp", func(storage storage.Storage) {
169+
t.Run("restoring using HTTP works", func(t *testing.T) {
170+
storage.Clear()
171+
172+
tempDir, _ := ioutil.TempDir(os.TempDir(), "*")
173+
tempFile, _ := ioutil.TempFile(tempDir, "*")
174+
_ = tempFile.Close()
175+
176+
archiver := archive.NewShellOutArchiver(metrics.NewNoOpMetricsManager())
177+
compressAndStore(storage, archiver, "abc", tempDir)
178+
179+
// set the environment variables to download using HTTP instead before restoring
180+
os.Setenv("SEMAPHORE_CACHE_CDN_URL", "http://sftp-server:80")
181+
os.Setenv("SEMAPHORE_CACHE_CDN_KEY", "test")
182+
os.Setenv("SEMAPHORE_CACHE_CDN_SECRET", "test")
183+
defer func() {
184+
os.Unsetenv("SEMAPHORE_CACHE_CDN_URL")
185+
os.Unsetenv("SEMAPHORE_CACHE_CDN_KEY")
186+
os.Unsetenv("SEMAPHORE_CACHE_CDN_SECRET")
187+
}()
188+
189+
RunRestore(restoreCmd, []string{"^abc"})
190+
output := readOutputFromFile(t)
191+
192+
restoredPath := filepath.FromSlash(fmt.Sprintf("%s/", tempDir))
193+
assert.Contains(t, output, "HIT: '^abc', using key 'abc'.")
194+
assert.Contains(t, output, fmt.Sprintf("Restored: %s.", restoredPath))
195+
196+
os.Remove(tempFile.Name())
197+
os.Remove(tempDir)
198+
})
199+
200+
t.Run("restoring defaults to use SFTP if not all variables are available", func(t *testing.T) {
201+
storage.Clear()
202+
203+
tempDir, _ := ioutil.TempDir(os.TempDir(), "*")
204+
tempFile, _ := ioutil.TempFile(tempDir, "*")
205+
_ = tempFile.Close()
206+
207+
archiver := archive.NewShellOutArchiver(metrics.NewNoOpMetricsManager())
208+
compressAndStore(storage, archiver, "abc", tempDir)
209+
210+
// Set just the URL, but not the user/pass
211+
// This means SFTP will still be used.
212+
os.Setenv("SEMAPHORE_CACHE_CDN_URL", "http://sftp-server:80")
213+
defer func() {
214+
os.Unsetenv("SEMAPHORE_CACHE_CDN_URL")
215+
}()
216+
217+
RunRestore(restoreCmd, []string{"^abc"})
218+
output := readOutputFromFile(t)
219+
220+
restoredPath := filepath.FromSlash(fmt.Sprintf("%s/", tempDir))
221+
assert.Contains(t, output, "HIT: '^abc', using key 'abc'.")
222+
assert.Contains(t, output, fmt.Sprintf("Restored: %s.", restoredPath))
223+
224+
os.Remove(tempFile.Name())
225+
os.Remove(tempDir)
226+
})
227+
})
167228
}
168229

169230
func Test__AutomaticRestore(t *testing.T) {

cache-cli/cmd/root_test.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,21 @@ var testBackends = map[string]TestBackend{
3838
},
3939
}
4040

41+
func runTestForSingleBackend(t *testing.T, testBackend string, test func(storage.Storage)) {
42+
backend := testBackends[testBackend]
43+
if runtime.GOOS == "windows" && !backend.runInWindows {
44+
return
45+
}
46+
47+
for envVarName, envVarValue := range backend.envVars {
48+
os.Setenv(envVarName, envVarValue)
49+
}
50+
51+
storage, err := storage.InitStorage()
52+
assert.Nil(t, err)
53+
test(storage)
54+
}
55+
4156
func runTestForAllBackends(t *testing.T, test func(string, storage.Storage)) {
4257
for backendType, testBackend := range testBackends {
4358
if runtime.GOOS == "windows" && !testBackend.runInWindows {

cache-cli/default.conf

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
server {
2+
listen 80 default_server;
3+
server_name _;
4+
root /home/tester;
5+
auth_basic "Auth required";
6+
auth_basic_user_file /etc/nginx/.htpasswd;
7+
location / {
8+
autoindex on;
9+
}
10+
}

cache-cli/docker-compose.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ services:
3939
container_name: sftp-server
4040
ports:
4141
- "2222:22"
42+
- "8080:8080"
4243
build:
4344
context: .
4445
dockerfile: Dockerfile.sftp_server

cache-cli/nginx.conf

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
user root;
2+
worker_processes auto;
3+
pid /run/nginx.pid;
4+
events {
5+
worker_connections 10;
6+
}
7+
http {
8+
sendfile off;
9+
tcp_nopush on;
10+
tcp_nodelay on;
11+
keepalive_timeout 65;
12+
types_hash_max_size 2048;
13+
server_tokens off;
14+
include /etc/nginx/mime.types;
15+
default_type application/octet-stream;
16+
gzip off;
17+
include /etc/nginx/sites-enabled/*;
18+
}

cache-cli/pkg/files/download.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package files
2+
3+
import (
4+
"fmt"
5+
"net/http"
6+
"os"
7+
)
8+
9+
func DownloadFromHTTP(URL, username, password, key string) (*os.File, error) {
10+
client := &http.Client{}
11+
downloadURL := fmt.Sprintf("%s/%s", URL, key)
12+
req, err := http.NewRequest("GET", downloadURL, nil)
13+
if err != nil {
14+
return nil, err
15+
}
16+
17+
req.SetBasicAuth(username, password)
18+
resp, err := client.Do(req)
19+
if err != nil {
20+
return nil, err
21+
}
22+
23+
if resp.StatusCode != http.StatusOK {
24+
return nil, fmt.Errorf("failed to download file: %s", resp.Status)
25+
}
26+
27+
localFile, err := os.CreateTemp(os.TempDir(), fmt.Sprintf("%s-*", key))
28+
if err != nil {
29+
return nil, err
30+
}
31+
32+
_, err = localFile.ReadFrom(resp.Body)
33+
if err != nil {
34+
_ = localFile.Close()
35+
return nil, err
36+
}
37+
38+
err = resp.Body.Close()
39+
if err != nil {
40+
_ = localFile.Close()
41+
return nil, err
42+
}
43+
44+
return localFile, localFile.Close()
45+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package files
2+
3+
import (
4+
"os"
5+
"testing"
6+
"runtime"
7+
"github.com/semaphoreci/toolbox/cache-cli/pkg/storage"
8+
"github.com/stretchr/testify/require"
9+
)
10+
11+
func Test__DownloadFromHTTP(t *testing.T) {
12+
if runtime.GOOS == "windows" {
13+
t.Skip()
14+
}
15+
sftpStorage, err := storage.NewSFTPStorage(storage.SFTPStorageOptions{
16+
URL: "sftp-server:22",
17+
Username: "tester",
18+
PrivateKeyPath: "/root/.ssh/semaphore_cache_key",
19+
Config: storage.StorageConfig{
20+
MaxSpace: 1024,
21+
SortKeysBy: storage.SortBySize,
22+
},
23+
})
24+
25+
require.NoError(t, err)
26+
require.NoError(t, sftpStorage.Clear())
27+
require.NoError(t, sftpStorage.Store("abc", "testdata/test.txt"))
28+
29+
t.Run("download works", func(t *testing.T) {
30+
f, err := DownloadFromHTTP("http://sftp-server:80", "test", "test", "abc")
31+
require.NoError(t, err)
32+
require.FileExists(t, f.Name())
33+
34+
content, err := os.ReadFile(f.Name())
35+
require.NoError(t, err)
36+
require.Equal(t, "Test 123", string(content))
37+
})
38+
39+
t.Run("download fails if URL is not reachable", func(t *testing.T) {
40+
_, err := DownloadFromHTTP("http://sftp-server:801", "test", "test", "abc")
41+
require.ErrorContains(t, err, "connection refused")
42+
})
43+
44+
t.Run("download fails if username is invalid", func(t *testing.T) {
45+
_, err := DownloadFromHTTP("http://sftp-server:80", "wrong", "test", "abc")
46+
require.ErrorContains(t, err, "failed to download file: 401 Unauthorized")
47+
})
48+
49+
t.Run("download fails if password is wrong", func(t *testing.T) {
50+
_, err := DownloadFromHTTP("http://sftp-server:80", "test", "wrong", "abc")
51+
require.ErrorContains(t, err, "failed to download file: 401 Unauthorized")
52+
})
53+
54+
t.Run("download fails if file does not exist", func(t *testing.T) {
55+
_, err := DownloadFromHTTP("http://sftp-server:80", "test", "test", "does-not-exist")
56+
require.ErrorContains(t, err, "failed to download file: 404 Not Found")
57+
})
58+
}

0 commit comments

Comments
 (0)