Skip to content

Commit b388a84

Browse files
committed
kiln upload-release calculates the remote path
- Use the ReleaseManifestReader to get the release's name and version - Build a remote path based on release name + version (i.e "${name}/${name}-${version}.tgz") - Still validate that the path is valid for the given release source. It doesn't make sense to upload a release that can't be fetched from that source - Change ReleaseManifestReader to use a billy filesystem (since we use that in our tests for upload-release Co-authored-by: Matt Royal <mroyal@pivotal.io> Co-authored-by: Carlos Iriarte <ciriarte@pivotal.io> [Finishes #170710078] kiln upload-release automatically calculates the conventional remote path
1 parent 55b9070 commit b388a84

File tree

6 files changed

+138
-63
lines changed

6 files changed

+138
-63
lines changed

builder/release_manifest_reader.go

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@ import (
77
"fmt"
88
"io"
99
"io/ioutil"
10-
"os"
1110
"path/filepath"
1211
"strings"
1312

14-
yaml "gopkg.in/yaml.v2"
13+
"gopkg.in/src-d/go-billy.v4"
14+
"gopkg.in/src-d/go-billy.v4/osfs"
15+
16+
"gopkg.in/yaml.v2"
1517
)
1618

1719
type ReleaseManifest struct {
@@ -33,14 +35,20 @@ type compiledPackage struct {
3335
Stemcell string `yaml:"stemcell"`
3436
}
3537

36-
type ReleaseManifestReader struct{}
38+
type ReleaseManifestReader struct {
39+
fs billy.Filesystem
40+
}
3741

38-
func NewReleaseManifestReader() ReleaseManifestReader {
39-
return ReleaseManifestReader{}
42+
func NewReleaseManifestReader(fs billy.Filesystem) ReleaseManifestReader {
43+
return ReleaseManifestReader{fs: fs}
4044
}
4145

4246
func (r ReleaseManifestReader) Read(releaseTarball string) (Part, error) {
43-
file, err := os.Open(releaseTarball)
47+
if r.fs == nil {
48+
r.fs = osfs.New("")
49+
}
50+
51+
file, err := r.fs.Open(releaseTarball)
4452
if err != nil {
4553
return Part{}, err
4654
}

builder/release_manifest_reader_test.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import (
1313
"path/filepath"
1414
"time"
1515

16+
"gopkg.in/src-d/go-billy.v4/osfs"
17+
1618
. "github.com/pivotal-cf/kiln/builder"
1719

1820
. "github.com/onsi/ginkgo"
@@ -75,7 +77,7 @@ var _ = Describe("ReleaseManifestReader", func() {
7577
)
7678

7779
BeforeEach(func() {
78-
reader = NewReleaseManifestReader()
80+
reader = NewReleaseManifestReader(osfs.New(""))
7981
tarball, releaseSHA1 = createReleaseTarball(`
8082
name: release
8183
version: 1.2.3

commands/upload_release.go

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import (
66
"log"
77
"regexp"
88

9+
"github.com/pivotal-cf/kiln/builder"
10+
911
"github.com/pivotal-cf/kiln/fetcher"
1012
"github.com/pivotal-cf/kiln/internal/cargo"
1113

@@ -34,7 +36,6 @@ type UploadRelease struct {
3436

3537
ReleaseSource string `short:"rs" long:"release-source" required:"true" description:"name of the release source specified in the Kilnfile"`
3638
LocalPath string `short:"lp" long:"local-path" required:"true" description:"path to BOSH release tarball"`
37-
RemotePath string `short:"rp" long:"remote-path" required:"true" description:"path at the remote source"`
3839
}
3940
}
4041

@@ -83,22 +84,31 @@ func (uploadRelease UploadRelease) Execute(args []string) error {
8384
return errors.New(msg)
8485
}
8586

87+
manifestReader := builder.NewReleaseManifestReader(uploadRelease.FS)
88+
part, err := manifestReader.Read(uploadRelease.Options.LocalPath)
89+
if err != nil {
90+
return fmt.Errorf("error reading the release manifest: %w", err)
91+
}
92+
93+
manifest := part.Metadata.(builder.ReleaseManifest)
94+
remotePath := fmt.Sprintf("%s/%s-%s.tgz", manifest.Name, manifest.Name, manifest.Version)
95+
8696
re, err := regexp.Compile(rc.Regex)
8797
if err != nil {
8898
return fmt.Errorf("could not compile the regular expression in Kilnfile for %q: %w", rc.Bucket, err)
8999
}
90100

91-
if !re.MatchString(uploadRelease.Options.RemotePath) {
92-
return fmt.Errorf("remote-path does not match regular expression in Kilnfile for %q", rc.Bucket)
101+
if !re.MatchString(remotePath) {
102+
return fmt.Errorf("remote path %q does not match regular expression in Kilnfile for %q", remotePath, rc.Bucket)
93103
}
94104

95105
uploader := uploadRelease.UploaderConfig(rc)
96106

97107
uploadRelease.Logger.Printf("Uploading release %q to %s as %q...\n",
98-
uploadRelease.Options.LocalPath, uploadRelease.Options.ReleaseSource, uploadRelease.Options.RemotePath)
108+
uploadRelease.Options.LocalPath, uploadRelease.Options.ReleaseSource, remotePath)
99109
if _, err := uploader.Upload(&s3manager.UploadInput{
100110
Bucket: aws.String(rc.Bucket),
101-
Key: aws.String(uploadRelease.Options.RemotePath),
111+
Key: aws.String(remotePath),
102112
Body: file,
103113
}); err != nil {
104114
return fmt.Errorf("upload failed: %w", err)

commands/upload_release_test.go

Lines changed: 102 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
11
package commands_test
22

33
import (
4-
"io/ioutil"
4+
"archive/tar"
5+
"compress/gzip"
6+
"crypto/sha1"
7+
"fmt"
8+
"io"
59
"log"
10+
"os"
11+
"strings"
12+
"time"
613

714
. "github.com/onsi/ginkgo"
815
. "github.com/onsi/gomega"
@@ -30,7 +37,7 @@ var _ = Describe("UploadRelease", func() {
3037
Region: "mars-2",
3138
AccessKeyId: "id",
3239
SecretAccessKey: "secret",
33-
Regex: "^(?P<release_name>[a-z-_0-9]+)-(?P<release_version>v?[0-9\\.]+-?[a-zA-Z0-9]\\.?[0-9]*)\\.tgz$",
40+
Regex: `^\w+/(?P<release_name>[a-z-_0-9]+)-(?P<release_version>v?[0-9\.]+-?[a-zA-Z0-9]\.?[0-9]*)\.tgz$`,
3441
},
3542
{
3643
Type: "boshio",
@@ -44,8 +51,49 @@ var _ = Describe("UploadRelease", func() {
4451
},
4552
}
4653
}
54+
55+
expectedReleaseSHA string
4756
)
4857

58+
var writeReleaseTarball = func(path, name, version string) string {
59+
f, err := fs.Create(path)
60+
Expect(err).NotTo(HaveOccurred())
61+
62+
gw := gzip.NewWriter(f)
63+
tw := tar.NewWriter(gw)
64+
65+
releaseManifest := `
66+
name: ` + name + `
67+
version: ` + version + `
68+
`
69+
manifestReader := strings.NewReader(releaseManifest)
70+
71+
header := &tar.Header{
72+
Name: "release.MF",
73+
Size: manifestReader.Size(),
74+
Mode: int64(os.O_RDONLY),
75+
ModTime: time.Now(),
76+
}
77+
Expect(tw.WriteHeader(header)).To(Succeed())
78+
79+
_, err = io.Copy(tw, manifestReader)
80+
Expect(err).NotTo(HaveOccurred())
81+
82+
Expect(tw.Close()).To(Succeed())
83+
Expect(gw.Close()).To(Succeed())
84+
Expect(f.Close()).To(Succeed())
85+
86+
tarball, err := fs.Open(path)
87+
Expect(err).NotTo(HaveOccurred())
88+
defer tarball.Close()
89+
90+
hash := sha1.New()
91+
_, err = io.Copy(hash, tarball)
92+
Expect(err).NotTo(HaveOccurred())
93+
94+
return fmt.Sprintf("%x", hash.Sum(nil))
95+
}
96+
4997
BeforeEach(func() {
5098
fs = memfs.New()
5199
loader = new(fakes.KilnfileLoader)
@@ -60,19 +108,14 @@ var _ = Describe("UploadRelease", func() {
60108
return nil
61109
},
62110
}
111+
expectedReleaseSHA = writeReleaseTarball("banana-release.tgz", "banana", "1.2.3")
63112
})
64113

65114
When("it receives a correct tarball path", func() {
66115
BeforeEach(func() {
67116
loader.LoadKilnfilesReturns(
68117
cargo.Kilnfile{ReleaseSources: exampleReleaseSourceList()},
69118
cargo.KilnfileLock{}, nil)
70-
71-
f, err := fs.Create("banana-release.tgz")
72-
_, _ = f.Write([]byte("banana"))
73-
f.Close()
74-
75-
Expect(err).NotTo(HaveOccurred())
76119
})
77120

78121
It("uploads the tarball to the release source", func() {
@@ -89,28 +132,56 @@ var _ = Describe("UploadRelease", func() {
89132
err := uploadRelease.Execute([]string{
90133
"--kilnfile", "not-read-see-struct/Kilnfile",
91134
"--local-path", "banana-release.tgz",
92-
"--remote-path", "banana-release-1.2.3.tgz",
93135
"--release-source", "orange-bucket",
94136
"--variables-file", "my-secrets",
95137
})
96138

97139
Expect(err).NotTo(HaveOccurred())
140+
98141
Expect(configUploaderCallCount).To(Equal(1))
142+
99143
Expect(relSrcConfig).NotTo(BeNil())
100144
Expect(relSrcConfig.Bucket).To(Equal("orange-bucket"))
145+
101146
Expect(uploader.UploadCallCount()).To(Equal(1))
102147

103148
opts, fns := uploader.UploadArgsForCall(0)
104-
105149
Expect(fns).To(HaveLen(0))
106-
107150
Expect(opts.Bucket).NotTo(BeNil())
108151
Expect(*opts.Bucket).To(Equal("orange-bucket"))
109152
Expect(opts.Key).NotTo(BeNil())
110-
Expect(*opts.Key).To(Equal("banana-release-1.2.3.tgz"))
153+
Expect(*opts.Key).To(Equal("banana/banana-1.2.3.tgz"))
111154

112-
buf, _ := ioutil.ReadAll(opts.Body)
113-
Expect(string(buf)).To(Equal("banana"))
155+
hash := sha1.New()
156+
_, err = io.Copy(hash, opts.Body)
157+
Expect(err).NotTo(HaveOccurred())
158+
159+
releaseSHA := fmt.Sprintf("%x", hash.Sum(nil))
160+
Expect(releaseSHA).To(Equal(expectedReleaseSHA))
161+
})
162+
})
163+
164+
When("the release tarball is invalid", func() {
165+
BeforeEach(func() {
166+
f, err := fs.Create("invalid-release.tgz")
167+
_, _ = f.Write([]byte("invalid"))
168+
f.Close()
169+
170+
Expect(err).NotTo(HaveOccurred())
171+
172+
loader.LoadKilnfilesReturns(
173+
cargo.Kilnfile{ReleaseSources: exampleReleaseSourceList()},
174+
cargo.KilnfileLock{}, nil)
175+
})
176+
177+
It("errors", func() {
178+
err := uploadRelease.Execute([]string{
179+
"--kilnfile", "not-read-see-struct/Kilnfile",
180+
"--local-path", "invalid-release.tgz",
181+
"--release-source", "orange-bucket",
182+
"--variables-file", "my-secrets",
183+
})
184+
Expect(err).To(MatchError(ContainSubstring("error reading the release manifest")))
114185
})
115186
})
116187

@@ -124,22 +195,15 @@ var _ = Describe("UploadRelease", func() {
124195
ReleaseSources: relSrcList,
125196
}, cargo.KilnfileLock{}, nil)
126197

127-
f, err := fs.Create("banana-release.tgz")
128-
_, _ = f.Write([]byte("banana"))
129-
f.Close()
130-
131198
uploadRelease.UploaderConfig = func(rsc *cargo.ReleaseSourceConfig) commands.S3Uploader {
132199
return uploader
133200
}
134-
135-
Expect(err).NotTo(HaveOccurred())
136201
})
137202

138203
It("returns a descriptive error", func() {
139204
err := uploadRelease.Execute([]string{
140205
"--kilnfile", "not-read-see-struct/Kilnfile",
141206
"--local-path", "banana-release.tgz",
142-
"--remote-path", "banana-release-1.2.3.tgz",
143207
"--release-source", "orange-bucket",
144208
"--variables-file", "my-secrets",
145209
})
@@ -148,82 +212,71 @@ var _ = Describe("UploadRelease", func() {
148212
})
149213
})
150214

151-
When("the remote-path does not match the regex in the release_source", func() {
215+
When("the conventional remote-path does not match the regex in the release_source", func() {
152216
BeforeEach(func() {
153-
relSrcList := exampleReleaseSourceList()
217+
relSrcList := []cargo.ReleaseSourceConfig{
218+
{
219+
Type: "s3",
220+
Bucket: "orange-bucket",
221+
Region: "mars-2",
222+
AccessKeyId: "id",
223+
SecretAccessKey: "secret",
224+
Regex: "^pointless-root-dir/(?P<release_name>[a-z-_0-9]+)-(?P<release_version>v?[0-9\\.]+-?[a-zA-Z0-9]\\.?[0-9]*)\\.tgz$",
225+
},
226+
}
154227

155228
loader.LoadKilnfilesReturns(cargo.Kilnfile{
156229
ReleaseSources: relSrcList,
157230
}, cargo.KilnfileLock{}, nil)
158231

159-
f, err := fs.Create("banana-release.tgz")
160-
_, _ = f.Write([]byte("banana"))
161-
f.Close()
162-
163232
uploadRelease.UploaderConfig = func(rsc *cargo.ReleaseSourceConfig) commands.S3Uploader {
164233
return uploader
165234
}
166-
167-
Expect(err).NotTo(HaveOccurred())
168235
})
169236

170237
It("returns a descriptive error", func() {
171238
err := uploadRelease.Execute([]string{
172239
"--kilnfile", "not-read-see-struct/Kilnfile",
173240
"--local-path", "banana-release.tgz",
174-
"--remote-path", "BLA_BLA_BLA.tgz",
175241
"--release-source", "orange-bucket",
176242
"--variables-file", "my-secrets",
177243
})
178244

179-
Expect(err).To(MatchError(ContainSubstring("remote-path does not match")))
245+
Expect(err).To(MatchError(ContainSubstring(`remote path "banana/banana-1.2.3.tgz" does not match`)))
180246
})
181247
})
182248

183-
When("some the remote does not exist in the Kilnfile", func() {
249+
When("the given release source doesn't exist", func() {
184250
When("no release sources are s3 buckets", func() {
185251
BeforeEach(func() {
186252
loader.LoadKilnfilesReturns(cargo.Kilnfile{}, cargo.KilnfileLock{}, nil)
187-
188-
f, err := fs.Create("banana-release.tgz")
189-
_, _ = f.Write([]byte("banana"))
190-
f.Close()
191-
192-
Expect(err).NotTo(HaveOccurred())
193253
})
194254

195255
It("returns an error without suggested release sources", func() {
196256
err := uploadRelease.Execute([]string{
197257
"--kilnfile", "not-read-see-struct/Kilnfile",
198258
"--local-path", "banana-release.tgz",
199-
"--remote-path", "banana-release-1.2.3.tgz",
200259
"--release-source", "orange-bucket",
201260
"--variables-file", "my-secrets",
202261
})
203262

204263
Expect(err).To(MatchError(ContainSubstring("remote release source")))
205264
})
206265
})
207-
When("at least one release source is an s3 bucket", func() {
266+
267+
When("some release sources are s3 buckets", func() {
208268
BeforeEach(func() {
209269
loader.LoadKilnfilesReturns(
210270
cargo.Kilnfile{ReleaseSources: exampleReleaseSourceList()},
211271
cargo.KilnfileLock{}, nil,
212272
)
213-
214-
f, err := fs.Create("banana-release.tgz")
215-
_, _ = f.Write([]byte("banana"))
216-
f.Close()
217-
218-
Expect(err).NotTo(HaveOccurred())
219273
})
220274

221-
It("returns an error without suggested release sources", func() {
275+
It("returns an error that suggests valid release sources", func() {
222276
err := uploadRelease.Execute([]string{
223277
"--kilnfile", "not-read-see-struct/Kilnfile",
224278
"--local-path", "banana-release.tgz",
225-
"--remote-path", "banana-release-1.2.3.tgz",
226-
"--release-source", "dog-bucket",
279+
"--release-source", "no-such-release-source",
227280
"--variables-file", "my-secrets",
228281
})
229282

0 commit comments

Comments
 (0)