Skip to content

Commit 3bc7f8e

Browse files
Merge pull request #136 from shiv-am0/dev-s3-artifacts-download
Download artifacts functionality added
2 parents 002cbcb + 7eefd11 commit 3bc7f8e

File tree

8 files changed

+344
-33
lines changed

8 files changed

+344
-33
lines changed

README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ docker: Error response from daemon: Container command
4242

4343
Execute from the working directory:
4444

45+
* For upload
4546
```
4647
docker run --rm \
4748
-e PLUGIN_SOURCE=<source> \
@@ -53,3 +54,17 @@ docker run --rm \
5354
-w $(pwd) \
5455
plugins/s3 --dry-run
5556
```
57+
58+
* For download
59+
```
60+
docker run --rm \
61+
-e PLUGIN_SOURCE=<source directory to be downloaded from bucket> \
62+
-e PLUGIN_BUCKET=<bucket> \
63+
-e AWS_ACCESS_KEY_ID=<token> \
64+
-e AWS_SECRET_ACCESS_KEY=<secret> \
65+
-e PLUGIN_REGION=<region where the bucket is deployed> \
66+
-e PLUGIN_DOWNLOAD="true" \
67+
-v $(pwd):$(pwd) \
68+
-w $(pwd) \
69+
plugins/s3 --dry-run
70+
```

docker/Dockerfile.linux.arm64

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,4 @@ LABEL maintainer="Drone.IO Community <[email protected]>" \
66
org.label-schema.schema-version="1.0"
77

88
ADD release/linux/arm64/drone-s3 /bin/
9-
ENTRYPOINT ["/bin/drone-s3"]
9+
ENTRYPOINT ["/bin/drone-s3"]

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ require (
1313
require (
1414
github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect
1515
github.com/jmespath/go-jmespath v0.4.0 // indirect
16+
github.com/pkg/errors v0.9.1
1617
github.com/russross/blackfriday/v2 v2.1.0 // indirect
18+
golang.org/x/sync v0.6.0
1719
golang.org/x/sys v0.1.0 // indirect
1820
)

go.sum

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg=
1515
github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
1616
github.com/mattn/go-zglob v0.0.4 h1:LQi2iOm0/fGgu80AioIJ/1j9w9Oh+9DZ39J4VAGzHQM=
1717
github.com/mattn/go-zglob v0.0.4/go.mod h1:MxxjyoXXnMxfIpxTK2GAkw1w8glPsQILx3N5wrKakiY=
18+
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
1819
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
1920
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
2021
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@@ -40,6 +41,8 @@ golang.org/x/net v0.1.0 h1:hZ/3BUoy5aId7sCpA/Tc5lt8DkFgdVS2onTpJsZ/fl0=
4041
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
4142
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
4243
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
44+
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
45+
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
4346
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
4447
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
4548
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=

main.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ func main() {
8181
},
8282
cli.StringFlag{
8383
Name: "strip-prefix",
84-
Usage: "strip the prefix from the target",
84+
Usage: "used to add or remove a prefix from the source/target path",
8585
EnvVar: "PLUGIN_STRIP_PREFIX",
8686
},
8787
cli.StringSliceFlag{
@@ -94,6 +94,11 @@ func main() {
9494
Usage: "server-side encryption algorithm, defaults to none",
9595
EnvVar: "PLUGIN_ENCRYPTION",
9696
},
97+
cli.BoolFlag{
98+
Name: "download",
99+
Usage: "switch to download mode, which will fetch `source`'s files from s3 bucket",
100+
EnvVar: "PLUGIN_DOWNLOAD",
101+
},
97102
cli.BoolFlag{
98103
Name: "dry-run",
99104
Usage: "dry run for debug purposes",
@@ -164,6 +169,7 @@ func run(c *cli.Context) error {
164169
Exclude: c.StringSlice("exclude"),
165170
Encryption: c.String("encryption"),
166171
ContentType: c.Generic("content-type").(*StringMapFlag).Get(),
172+
Download: c.Bool("download"),
167173
ContentEncoding: c.Generic("content-encoding").(*StringMapFlag).Get(),
168174
CacheControl: c.Generic("cache-control").(*StringMapFlag).Get(),
169175
StorageClass: c.String("storage-class"),

plugin.go

Lines changed: 148 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package main
22

33
import (
4+
"io"
45
"mime"
56
"os"
67
"path/filepath"
@@ -15,6 +16,7 @@ import (
1516
"github.com/aws/aws-sdk-go/service/s3"
1617
"github.com/aws/aws-sdk-go/service/sts"
1718
"github.com/mattn/go-zglob"
19+
"github.com/pkg/errors"
1820
log "github.com/sirupsen/logrus"
1921
)
2022

@@ -44,6 +46,9 @@ type Plugin struct {
4446
// sa-east-1
4547
Region string
4648

49+
// if true, plugin is set to download mode, which means `source` from the bucket will be downloaded
50+
Download bool
51+
4752
// Indicates the files ACL, which should be one
4853
// of the following:
4954
// private
@@ -97,42 +102,21 @@ type Plugin struct {
97102

98103
// Exec runs the plugin
99104
func (p *Plugin) Exec() error {
100-
// normalize the target URL
101-
p.Target = strings.TrimPrefix(p.Target, "/")
102-
103-
// create the client
104-
conf := &aws.Config{
105-
Region: aws.String(p.Region),
106-
Endpoint: &p.Endpoint,
107-
DisableSSL: aws.Bool(strings.HasPrefix(p.Endpoint, "http://")),
108-
S3ForcePathStyle: aws.Bool(p.PathStyle),
109-
}
110-
111-
if p.Key != "" && p.Secret != "" {
112-
conf.Credentials = credentials.NewStaticCredentials(p.Key, p.Secret, "")
113-
} else if p.AssumeRole != "" {
114-
conf.Credentials = assumeRole(p.AssumeRole, p.AssumeRoleSessionName, p.ExternalID)
105+
if p.Download {
106+
p.Source = normalizePath(p.Source)
107+
p.Target = normalizePath(p.Target)
115108
} else {
116-
log.Warn("AWS Key and/or Secret not provided (falling back to ec2 instance profile)")
109+
p.Target = strings.TrimPrefix(p.Target, "/")
117110
}
118111

119-
var client *s3.S3
120-
sess, err := session.NewSession(conf)
121-
if err != nil {
122-
log.WithError(err).Errorln("could not instantiate session")
123-
return err
124-
}
112+
// create the client
113+
client := p.createS3Client()
125114

126-
// If user role ARN is set then assume role here
127-
if len(p.UserRoleArn) > 0 {
128-
confRoleArn := aws.Config{
129-
Region: aws.String(p.Region),
130-
Credentials: stscreds.NewCredentials(sess, p.UserRoleArn),
131-
}
115+
// If in download mode, call the downloadS3Objects method
116+
if p.Download {
117+
sourceDir := normalizePath(p.Source)
132118

133-
client = s3.New(sess, &confRoleArn)
134-
} else {
135-
client = s3.New(sess)
119+
return p.downloadS3Objects(client, sourceDir)
136120
}
137121

138122
// find the bucket
@@ -322,6 +306,14 @@ func resolveKey(target, srcPath, stripPrefix string) string {
322306
return key
323307
}
324308

309+
func resolveSource(sourceDir, source, stripPrefix string) string {
310+
// Remove the leading sourceDir from the source path
311+
path := strings.TrimPrefix(strings.TrimPrefix(source, sourceDir), "/")
312+
313+
// Add the specified stripPrefix to the resulting path
314+
return stripPrefix + path
315+
}
316+
325317
// checks if the source path is a dir
326318
func isDir(source string, matches []string) bool {
327319
stat, err := os.Stat(source)
@@ -342,3 +334,128 @@ func isDir(source string, matches []string) bool {
342334
}
343335
return false
344336
}
337+
338+
// normalizePath converts the path to a forward slash format and trims the prefix.
339+
func normalizePath(path string) string {
340+
return strings.TrimPrefix(filepath.ToSlash(path), "/")
341+
}
342+
343+
// downloadS3Object downloads a single object from S3
344+
func (p *Plugin) downloadS3Object(client *s3.S3, sourceDir, key, target string) error {
345+
log.WithFields(log.Fields{
346+
"bucket": p.Bucket,
347+
"key": key,
348+
}).Info("Getting S3 object")
349+
350+
obj, err := client.GetObject(&s3.GetObjectInput{
351+
Bucket: &p.Bucket,
352+
Key: &key,
353+
})
354+
if err != nil {
355+
log.WithFields(log.Fields{
356+
"error": err,
357+
"bucket": p.Bucket,
358+
"key": key,
359+
}).Error("Cannot get S3 object")
360+
return err
361+
}
362+
defer obj.Body.Close()
363+
364+
// Create the destination file path
365+
destination := filepath.Join(p.Target, target)
366+
log.Println("Destination: ", destination)
367+
368+
// Extract the directory from the destination path
369+
dir := filepath.Dir(destination)
370+
371+
// Create the directory and any necessary parent directories
372+
if err := os.MkdirAll(dir, os.ModePerm); err != nil {
373+
return errors.Wrap(err, "error creating directories")
374+
}
375+
376+
f, err := os.Create(destination)
377+
if err != nil {
378+
log.WithFields(log.Fields{
379+
"error": err,
380+
"file": destination,
381+
}).Error("Failed to create file")
382+
return err
383+
}
384+
defer f.Close()
385+
386+
_, err = io.Copy(f, obj.Body)
387+
if err != nil {
388+
log.WithFields(log.Fields{
389+
"error": err,
390+
"file": destination,
391+
}).Error("Failed to write file")
392+
return err
393+
}
394+
395+
return nil
396+
}
397+
398+
// downloadS3Objects downloads all objects in the specified S3 bucket path
399+
func (p *Plugin) downloadS3Objects(client *s3.S3, sourceDir string) error {
400+
log.WithFields(log.Fields{
401+
"bucket": p.Bucket,
402+
"dir": sourceDir,
403+
}).Info("Listing S3 directory")
404+
405+
list, err := client.ListObjectsV2(&s3.ListObjectsV2Input{
406+
Bucket: &p.Bucket,
407+
Prefix: &sourceDir,
408+
})
409+
if err != nil {
410+
log.WithFields(log.Fields{
411+
"error": err,
412+
"bucket": p.Bucket,
413+
"dir": sourceDir,
414+
}).Error("Cannot list S3 directory")
415+
return err
416+
}
417+
418+
for _, item := range list.Contents {
419+
// resolveSource takes a source directory, a source path, and a prefix to strip,
420+
// and returns a resolved target path by removing the sourceDir from the source
421+
// and appending the stripPrefix.
422+
target := resolveSource(sourceDir, *item.Key, p.StripPrefix)
423+
424+
if err := p.downloadS3Object(client, sourceDir, *item.Key, target); err != nil {
425+
return err
426+
}
427+
}
428+
429+
return nil
430+
}
431+
432+
// createS3Client creates and returns an S3 client based on the plugin configuration
433+
func (p *Plugin) createS3Client() *s3.S3 {
434+
conf := &aws.Config{
435+
Region: aws.String(p.Region),
436+
Endpoint: &p.Endpoint,
437+
DisableSSL: aws.Bool(strings.HasPrefix(p.Endpoint, "http://")),
438+
S3ForcePathStyle: aws.Bool(p.PathStyle),
439+
}
440+
441+
if p.Key != "" && p.Secret != "" {
442+
conf.Credentials = credentials.NewStaticCredentials(p.Key, p.Secret, "")
443+
} else if p.AssumeRole != "" {
444+
conf.Credentials = assumeRole(p.AssumeRole, p.AssumeRoleSessionName, p.ExternalID)
445+
} else {
446+
log.Warn("AWS Key and/or Secret not provided (falling back to ec2 instance profile)")
447+
}
448+
449+
sess, _ := session.NewSession(conf)
450+
client := s3.New(sess)
451+
452+
if len(p.UserRoleArn) > 0 {
453+
confRoleArn := aws.Config{
454+
Region: aws.String(p.Region),
455+
Credentials: stscreds.NewCredentials(sess, p.UserRoleArn),
456+
}
457+
client = s3.New(sess, &confRoleArn)
458+
}
459+
460+
return client
461+
}

plugin_unix_test.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,87 @@ func TestResolveUnixKey(t *testing.T) {
4444
}
4545
}
4646
}
47+
48+
func TestNormalizePath(t *testing.T) {
49+
tests := []struct {
50+
input string
51+
expected string
52+
}{
53+
{
54+
input: "/path/to/file.txt",
55+
expected: "path/to/file.txt",
56+
},
57+
{
58+
input: "C:\\Users\\username\\Documents\\file.doc",
59+
expected: "C:\\Users\\username\\Documents\\file.doc",
60+
},
61+
{
62+
input: "relative/path/to/file",
63+
expected: "relative/path/to/file",
64+
},
65+
{
66+
input: "file.txt",
67+
expected: "file.txt",
68+
},
69+
{
70+
input: "/root/directory/",
71+
expected: "root/directory/",
72+
},
73+
{
74+
input: "no_slash",
75+
expected: "no_slash",
76+
},
77+
}
78+
79+
for _, tc := range tests {
80+
result := normalizePath(tc.input)
81+
if result != tc.expected {
82+
t.Errorf("Expected: %s, Got: %s", tc.expected, result)
83+
}
84+
}
85+
}
86+
87+
func TestResolveSource(t *testing.T) {
88+
tests := []struct {
89+
sourceDir string
90+
source string
91+
stripPrefix string
92+
expected string
93+
}{
94+
// Test case 1
95+
{
96+
sourceDir: "/home/user/documents",
97+
source: "/home/user/documents/file.txt",
98+
stripPrefix: "output-",
99+
expected: "output-file.txt",
100+
},
101+
// Test case 2
102+
{
103+
sourceDir: "assets",
104+
source: "assets/images/logo.png",
105+
stripPrefix: "",
106+
expected: "images/logo.png",
107+
},
108+
// Test case 3
109+
{
110+
sourceDir: "/var/www/html",
111+
source: "/var/www/html/pages/index.html",
112+
stripPrefix: "web",
113+
expected: "webpages/index.html",
114+
},
115+
// Test case 4
116+
{
117+
sourceDir: "dist",
118+
source: "dist/js/app.js",
119+
stripPrefix: "public",
120+
expected: "publicjs/app.js",
121+
},
122+
}
123+
124+
for _, tc := range tests {
125+
result := resolveSource(tc.sourceDir, tc.source, tc.stripPrefix)
126+
if result != tc.expected {
127+
t.Errorf("Expected: %s, Got: %s", tc.expected, result)
128+
}
129+
}
130+
}

0 commit comments

Comments
 (0)