/
manifest_deployer.go
238 lines (213 loc) · 7.74 KB
/
manifest_deployer.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
// Copyright 2012-2014 Canonical Ltd.
// Licensed under the AGPLv3, see LICENCE file for details.
package charm
import (
"fmt"
"os"
"path/filepath"
"github.com/juju/utils"
"github.com/juju/utils/set"
"gopkg.in/juju/charm.v6-unstable"
)
const (
// deployingURLPath holds the path in the charm dir where the manifest
// deployer writes what charm is currently being deployed.
deployingURLPath = ".juju-deploying"
// manifestsDataPath holds the path in the data dir where the manifest
// deployer stores the manifests for its charms.
manifestsDataPath = "manifests"
)
// NewManifestDeployer returns a Deployer that installs bundles from the
// supplied BundleReader into charmPath, and which reads and writes its
// persistent data into dataPath.
//
// It works by always writing the full contents of a deployed charm; and, if
// another charm was previously deployed, deleting only those files unique to
// that base charm. It thus leaves user files in place, with the exception of
// those in directories referenced only in the original charm, which will be
// deleted.
func NewManifestDeployer(charmPath, dataPath string, bundles BundleReader) Deployer {
return &manifestDeployer{
charmPath: charmPath,
dataPath: dataPath,
bundles: bundles,
}
}
type manifestDeployer struct {
charmPath string
dataPath string
bundles BundleReader
staged struct {
url *charm.URL
bundle Bundle
manifest set.Strings
}
}
func (d *manifestDeployer) Stage(info BundleInfo, abort <-chan struct{}) error {
bundle, err := d.bundles.Read(info, abort)
if err != nil {
return err
}
manifest, err := bundle.Manifest()
if err != nil {
return err
}
url := info.URL()
if err := d.storeManifest(url, manifest); err != nil {
return err
}
d.staged.url = url
d.staged.bundle = bundle
d.staged.manifest = manifest
return nil
}
func (d *manifestDeployer) Deploy() (err error) {
if d.staged.url == nil {
return fmt.Errorf("charm deployment failed: no charm set")
}
// Detect and resolve state of charm directory.
baseURL, baseManifest, err := d.loadManifest(CharmURLPath)
if err != nil && !os.IsNotExist(err) {
return err
}
upgrading := baseURL != nil
defer manifestDeployError(&err, upgrading)
if err := d.ensureBaseFiles(baseManifest); err != nil {
return err
}
// Write or overwrite the deploying URL to point to the staged one.
if err := d.startDeploy(); err != nil {
return err
}
// Delete files in the base version not present in the staged charm.
if upgrading {
if err := d.removeDiff(baseManifest, d.staged.manifest); err != nil {
return err
}
}
// Overwrite whatever's in place with the staged charm.
logger.Debugf("deploying charm %q", d.staged.url)
if err := d.staged.bundle.ExpandTo(d.charmPath); err != nil {
return err
}
// Move the deploying file over the charm URL file, and we're done.
return d.finishDeploy()
}
func (d *manifestDeployer) NotifyResolved() error {
// Maybe it is resolved, maybe not. We'll find out soon enough, but we
// don't need to take any action now; if it's not, we'll just ErrConflict
// out of Deploy again.
return nil
}
func (d *manifestDeployer) NotifyRevert() error {
// The Deploy implementation always effectively reverts when required
// anyway, so we need take no action right now.
return nil
}
// startDeploy persists the fact that we've started deploying the staged bundle.
func (d *manifestDeployer) startDeploy() error {
logger.Debugf("preparing to deploy charm %q", d.staged.url)
if err := os.MkdirAll(d.charmPath, 0755); err != nil {
return err
}
return WriteCharmURL(d.CharmPath(deployingURLPath), d.staged.url)
}
// removeDiff removes every path in oldManifest that is not present in newManifest.
func (d *manifestDeployer) removeDiff(oldManifest, newManifest set.Strings) error {
diff := oldManifest.Difference(newManifest)
for _, path := range diff.SortedValues() {
fullPath := filepath.Join(d.charmPath, filepath.FromSlash(path))
if err := os.RemoveAll(fullPath); err != nil {
return err
}
}
return nil
}
// finishDeploy persists the fact that we've finished deploying the staged bundle.
func (d *manifestDeployer) finishDeploy() error {
logger.Debugf("finishing deploy of charm %q", d.staged.url)
oldPath := d.CharmPath(deployingURLPath)
newPath := d.CharmPath(CharmURLPath)
return utils.ReplaceFile(oldPath, newPath)
}
// ensureBaseFiles checks for an interrupted deploy operation and, if it finds
// one, removes all entries in the manifest unique to the interrupted operation.
// This leaves files from the base charm in an indeterminate state, but ready to
// be either removed (if they are not referenced by the new charm) or overwritten
// (if they are referenced by the new charm).
//
// Note that deployingURLPath is *not* written, because the charm state remains
// indeterminate; that file will be removed when and only when a deploy completes
// successfully.
func (d *manifestDeployer) ensureBaseFiles(baseManifest set.Strings) error {
deployingURL, deployingManifest, err := d.loadManifest(deployingURLPath)
if err == nil {
logger.Infof("detected interrupted deploy of charm %q", deployingURL)
if *deployingURL != *d.staged.url {
logger.Infof("removing files from charm %q", deployingURL)
if err := d.removeDiff(deployingManifest, baseManifest); err != nil {
return err
}
}
}
if os.IsNotExist(err) {
err = nil
}
return err
}
// storeManifest stores, into dataPath, the supplied manifest for the supplied charm.
func (d *manifestDeployer) storeManifest(url *charm.URL, manifest set.Strings) error {
if err := os.MkdirAll(d.DataPath(manifestsDataPath), 0755); err != nil {
return err
}
name := charm.Quote(url.String())
path := filepath.Join(d.DataPath(manifestsDataPath), name)
return utils.WriteYaml(path, manifest.SortedValues())
}
// loadManifest loads, from dataPath, the manifest for the charm identified by the
// identity file at the supplied path within the charm directory.
func (d *manifestDeployer) loadManifest(urlFilePath string) (*charm.URL, set.Strings, error) {
url, err := ReadCharmURL(d.CharmPath(urlFilePath))
if err != nil {
return nil, nil, err
}
name := charm.Quote(url.String())
path := filepath.Join(d.DataPath(manifestsDataPath), name)
manifest := []string{}
err = utils.ReadYaml(path, &manifest)
if os.IsNotExist(err) {
logger.Warningf("manifest not found at %q: files from charm %q may be left unremoved", path, url)
err = nil
}
return url, set.NewStrings(manifest...), err
}
// CharmPath returns the supplied path joined to the ManifestDeployer's charm directory.
func (d *manifestDeployer) CharmPath(path string) string {
return filepath.Join(d.charmPath, path)
}
// DataPath returns the supplied path joined to the ManifestDeployer's data directory.
func (d *manifestDeployer) DataPath(path string) string {
return filepath.Join(d.dataPath, path)
}
// manifestDeployError annotates or replaces the supplied error according
// to whether or not an upgrade operation is in play. It was extracted from
// Deploy to aid that method's readability.
func manifestDeployError(err *error, upgrading bool) {
if *err != nil {
if upgrading {
// We now treat any failure to overwrite the charm -- or otherwise
// manipulate the charm directory -- as a conflict, because it's
// actually plausible for a user (or at least a charm author, who
// is the real audience for this case) to get in there and fix it.
logger.Errorf("cannot upgrade charm: %v", *err)
*err = ErrConflict
} else {
// ...but if we can't install at all, we just fail out as the old
// gitDeployer did, because I'm not willing to mess around with
// the uniter to enable ErrConflict handling on install. We've
// never heard of it actually happening, so this is probably not
// a big deal.
*err = fmt.Errorf("cannot install charm: %v", *err)
}
}
}