1package releasedir
2
3import (
4	"fmt"
5	"os"
6	"path/filepath"
7	"strings"
8
9	"code.cloudfoundry.org/clock"
10	bosherr "github.com/cloudfoundry/bosh-utils/errors"
11	boshsys "github.com/cloudfoundry/bosh-utils/system"
12	semver "github.com/cppforlife/go-semi-semantic/version"
13
14	boshrel "github.com/cloudfoundry/bosh-cli/release"
15	boshpkg "github.com/cloudfoundry/bosh-cli/release/pkg"
16	boshpkgman "github.com/cloudfoundry/bosh-cli/release/pkg/manifest"
17)
18
19var (
20	DefaultFinalVersion   = semver.MustNewVersionFromString("1")
21	DefaultDevVersion     = semver.MustNewVersionFromString("0+dev.0")
22	DefaultDevPostRelease = semver.MustNewVersionSegmentFromString("dev.1")
23)
24
25type FSReleaseDir struct {
26	dirPath string
27
28	config    Config
29	gitRepo   GitRepo
30	blobsDir  BlobsDir
31	generator Generator
32
33	devReleases   ReleaseIndex
34	finalReleases ReleaseIndex
35	finalIndicies boshrel.ArchiveIndicies
36
37	releaseReader        boshrel.Reader
38	releaseArchiveWriter boshrel.Writer
39
40	timeService clock.Clock
41	fs          boshsys.FileSystem
42
43	parallel int
44}
45
46func NewFSReleaseDir(
47	dirPath string,
48	config Config,
49	gitRepo GitRepo,
50	blobsDir BlobsDir,
51	generator Generator,
52	devReleases ReleaseIndex,
53	finalReleases ReleaseIndex,
54	finalIndicies boshrel.ArchiveIndicies,
55	releaseReader boshrel.Reader,
56	timeService clock.Clock,
57	fs boshsys.FileSystem,
58	parallel int,
59) FSReleaseDir {
60	return FSReleaseDir{
61		dirPath: dirPath,
62
63		config:    config,
64		gitRepo:   gitRepo,
65		blobsDir:  blobsDir,
66		generator: generator,
67
68		devReleases:   devReleases,
69		finalReleases: finalReleases,
70		finalIndicies: finalIndicies,
71
72		releaseReader: releaseReader,
73
74		timeService: timeService,
75		fs:          fs,
76
77		parallel: parallel,
78	}
79}
80
81func (d FSReleaseDir) Init(git bool) error {
82	for _, name := range []string{"jobs", "packages", "src"} {
83		err := d.fs.MkdirAll(filepath.Join(d.dirPath, name), os.ModePerm)
84		if err != nil {
85			return bosherr.WrapErrorf(err, "Creating %s/", name)
86		}
87	}
88
89	name := strings.TrimSuffix(filepath.Base(d.dirPath), "-release")
90
91	err := d.config.SaveName(name)
92	if err != nil {
93		return err
94	}
95
96	err = d.blobsDir.Init()
97	if err != nil {
98		return bosherr.WrapErrorf(err, "Initing blobs")
99	}
100
101	if git {
102		err = d.gitRepo.Init()
103		if err != nil {
104			return err
105		}
106	}
107
108	return nil
109}
110
111func (d FSReleaseDir) GenerateJob(name string) error {
112	return d.generator.GenerateJob(name)
113}
114
115func (d FSReleaseDir) GeneratePackage(name string) error {
116	return d.generator.GeneratePackage(name)
117}
118
119func (d FSReleaseDir) Reset() error {
120	for _, name := range []string{".dev_builds", "dev_releases", ".blobs", "blobs"} {
121		err := d.fs.RemoveAll(filepath.Join(d.dirPath, name))
122		if err != nil {
123			return bosherr.WrapErrorf(err, "Removing %s/", name)
124		}
125	}
126
127	return nil
128}
129
130func (d FSReleaseDir) DefaultName() (string, error) {
131	return d.config.Name()
132}
133
134func (d FSReleaseDir) NextFinalVersion(name string) (semver.Version, error) {
135	lastVer, err := d.finalReleases.LastVersion(name)
136	if err != nil {
137		return semver.Version{}, err
138	} else if lastVer == nil {
139		return DefaultFinalVersion, nil
140	}
141
142	incVer, err := lastVer.IncrementRelease()
143	if err != nil {
144		return semver.Version{}, bosherr.WrapErrorf(err, "Incrementing last final version")
145	}
146
147	return incVer, nil
148}
149
150func (d FSReleaseDir) NextDevVersion(name string, timestamp bool) (semver.Version, error) {
151	lastVer, _, err := d.lastDevOrFinalVersion(name)
152	if err != nil {
153		return semver.Version{}, err
154	} else if lastVer == nil {
155		lastVer = &DefaultDevVersion
156	}
157
158	incVer, err := lastVer.IncrementPostRelease(DefaultDevPostRelease)
159	if err != nil {
160		return semver.Version{}, bosherr.WrapErrorf(err, "Incrementing last dev version")
161	}
162
163	if timestamp {
164		ts := d.timeService.Now().Unix()
165
166		postRelease, err := semver.NewVersionSegmentFromString(fmt.Sprintf("dev.%d", ts))
167		if err != nil {
168			panic(fmt.Sprintf("Failed to build post release version segment from timestamp (%d): %s", ts, err))
169		}
170
171		incVer, err = semver.NewVersion(incVer.Release.Copy(), incVer.PreRelease.Copy(), postRelease)
172		if err != nil {
173			panic(fmt.Sprintf("Failed to build version: %s", err))
174		}
175	}
176
177	return incVer, nil
178}
179
180func (d FSReleaseDir) FindRelease(name string, version semver.Version) (boshrel.Release, error) {
181	if len(name) == 0 {
182		defaultName, err := d.DefaultName()
183		if err != nil {
184			return nil, err
185		}
186		name = defaultName
187	}
188
189	relIndex := d.finalReleases
190
191	if version.Empty() {
192		lastVer, lastRelIndex, err := d.lastDevOrFinalVersion(name)
193		if err != nil {
194			return nil, err
195		} else if lastVer == nil {
196			return nil, bosherr.Errorf("Expected to find at least one dev or final version")
197		}
198		version = *lastVer
199		relIndex = lastRelIndex
200	}
201
202	return d.releaseReader.Read(relIndex.ManifestPath(name, version.AsString()))
203}
204
205func (d FSReleaseDir) BuildRelease(name string, version semver.Version, force bool) (boshrel.Release, error) {
206	dirty, err := d.gitRepo.MustNotBeDirty(force)
207	if err != nil {
208		return nil, err
209	}
210
211	commitSHA, err := d.gitRepo.LastCommitSHA()
212	if err != nil {
213		return nil, err
214	}
215
216	err = d.blobsDir.SyncBlobs(1)
217	if err != nil {
218		return nil, err
219	}
220
221	release, err := d.releaseReader.Read(d.dirPath)
222	if err != nil {
223		return nil, bosherr.WrapErrorf(err, "Building a release from directory '%s'", d.dirPath)
224	}
225
226	release.SetName(name)
227	release.SetVersion(version.AsString())
228	release.SetCommitHash(commitSHA)
229	release.SetUncommittedChanges(dirty)
230
231	err = d.devReleases.Add(release.Manifest())
232	if err != nil {
233		return nil, err
234	}
235
236	return release, nil
237}
238
239func (d FSReleaseDir) VendorPackage(pkg *boshpkg.Package) error {
240	allInterestingPkgs := map[*boshpkg.Package]struct{}{}
241
242	d.collectDependentPackages(pkg, allInterestingPkgs)
243
244	for pkg2, _ := range allInterestingPkgs {
245		err := pkg2.Finalize(d.finalIndicies.Packages)
246		if err != nil {
247			return bosherr.WrapErrorf(err, "Finalizing vendored package")
248		}
249
250		err = d.writeVendoredPackage(pkg2)
251		if err != nil {
252			return bosherr.WrapErrorf(err, "Writing vendored package")
253		}
254	}
255
256	return nil
257}
258
259func (d FSReleaseDir) collectDependentPackages(pkg *boshpkg.Package, allInterestingPkgs map[*boshpkg.Package]struct{}) {
260	allInterestingPkgs[pkg] = struct{}{}
261	for _, pkg2 := range pkg.Dependencies {
262		d.collectDependentPackages(pkg2, allInterestingPkgs)
263	}
264}
265
266func (d FSReleaseDir) writeVendoredPackage(pkg *boshpkg.Package) error {
267	name := pkg.Name()
268	pkgDirPath := filepath.Join(d.dirPath, "packages", name)
269
270	err := d.fs.RemoveAll(pkgDirPath)
271	if err != nil {
272		return bosherr.WrapErrorf(err, "Removing package '%s' dir", name)
273	}
274
275	err = d.fs.MkdirAll(pkgDirPath, os.ModePerm)
276	if err != nil {
277		return bosherr.WrapErrorf(err, "Creating package '%s' dir", name)
278	}
279
280	manifestLock := boshpkgman.ManifestLock{Name: name, Fingerprint: pkg.Fingerprint()}
281
282	for _, pkg2 := range pkg.Dependencies {
283		manifestLock.Dependencies = append(manifestLock.Dependencies, pkg2.Name())
284	}
285
286	manifestLockBytes, err := manifestLock.AsBytes()
287	if err != nil {
288		return bosherr.WrapErrorf(err, "Marshaling vendored package '%s' spec lock", name)
289	}
290
291	err = d.fs.WriteFile(filepath.Join(pkgDirPath, "spec.lock"), manifestLockBytes)
292	if err != nil {
293		return bosherr.WrapErrorf(err, "Creating package '%s' spec lock file", name)
294	}
295
296	return nil
297}
298
299func (d FSReleaseDir) FinalizeRelease(release boshrel.Release, force bool) error {
300	_, err := d.gitRepo.MustNotBeDirty(force)
301	if err != nil {
302		return err
303	}
304
305	found, err := d.finalReleases.Contains(release)
306	if err != nil {
307		return err
308	} else if found {
309		return bosherr.Errorf("Release '%s' version '%s' already exists", release.Name(), release.Version())
310	}
311
312	err = release.Finalize(d.finalIndicies, d.parallel)
313	if err != nil {
314		return err
315	}
316
317	return d.finalReleases.Add(release.Manifest())
318}
319
320func (d FSReleaseDir) lastDevOrFinalVersion(name string) (*semver.Version, ReleaseIndex, error) {
321	lastDevVer, err := d.devReleases.LastVersion(name)
322	if err != nil {
323		return nil, nil, err
324	}
325
326	lastFinalVer, err := d.finalReleases.LastVersion(name)
327	if err != nil {
328		return nil, nil, err
329	}
330
331	switch {
332	case lastDevVer != nil && lastFinalVer != nil:
333		if lastFinalVer.IsGt(*lastDevVer) {
334			return lastFinalVer, d.finalReleases, nil
335		} else {
336			return lastDevVer, d.devReleases, nil
337		}
338	case lastDevVer != nil && lastFinalVer == nil:
339		return lastDevVer, d.devReleases, nil
340	case lastDevVer == nil && lastFinalVer != nil:
341		return lastFinalVer, d.finalReleases, nil
342	default:
343		return nil, nil, nil
344	}
345}
346