-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathundertaker.go
192 lines (170 loc) · 5.11 KB
/
undertaker.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
// Copyright 2015-2016 Canonical Ltd.
// Licensed under the AGPLv3, see LICENCE file for details.
package undertaker
import (
"fmt"
"github.com/juju/errors"
"gopkg.in/juju/worker.v1/catacomb"
"github.com/juju/juju/apiserver/params"
"github.com/juju/juju/environs"
"github.com/juju/juju/environs/context"
"github.com/juju/juju/status"
"github.com/juju/juju/watcher"
"github.com/juju/juju/worker/common"
)
// Facade covers the parts of the api/undertaker.UndertakerClient that we
// need for the worker. It's more than a little raw, but we'll survive.
type Facade interface {
ModelInfo() (params.UndertakerModelInfoResult, error)
WatchModelResources() (watcher.NotifyWatcher, error)
ProcessDyingModel() error
RemoveModel() error
SetStatus(status status.Status, message string, data map[string]interface{}) error
}
// Config holds the resources and configuration necessary to run an
// undertaker worker.
type Config struct {
Facade Facade
Destroyer environs.CloudDestroyer
CredentialAPI common.CredentialAPI
}
// Validate returns an error if the config cannot be expected to drive
// a functional undertaker worker.
func (config Config) Validate() error {
if config.Facade == nil {
return errors.NotValidf("nil Facade")
}
if config.CredentialAPI == nil {
return errors.NotValidf("nil CredentialAPI")
}
if config.Destroyer == nil {
return errors.NotValidf("nil Destroyer")
}
return nil
}
// NewUndertaker returns a worker which processes a dying model.
func NewUndertaker(config Config) (*Undertaker, error) {
if err := config.Validate(); err != nil {
return nil, errors.Trace(err)
}
u := &Undertaker{
config: config,
callCtx: common.NewCloudCallContext(config.CredentialAPI),
}
err := catacomb.Invoke(catacomb.Plan{
Site: &u.catacomb,
Work: u.run,
})
if err != nil {
return nil, errors.Trace(err)
}
return u, nil
}
type Undertaker struct {
catacomb catacomb.Catacomb
config Config
callCtx context.ProviderCallContext
}
// Kill is part of the worker.Worker interface.
func (u *Undertaker) Kill() {
u.catacomb.Kill(nil)
}
// Wait is part of the worker.Worker interface.
func (u *Undertaker) Wait() error {
return u.catacomb.Wait()
}
func (u *Undertaker) run() error {
result, err := u.config.Facade.ModelInfo()
if err != nil {
return errors.Trace(err)
}
if result.Error != nil {
return errors.Trace(result.Error)
}
modelInfo := result.Result
if modelInfo.Life == params.Alive {
return errors.Errorf("model still alive")
}
if modelInfo.Life == params.Dying {
// TODO(axw) 2016-04-14 #1570285
// We should update status with information
// about the remaining resources here, and
// also make the worker responsible for
// checking the emptiness criteria before
// attempting to remove the model.
if err := u.setStatus(
status.Destroying,
"cleaning up cloud resources",
); err != nil {
return errors.Trace(err)
}
// Process the dying model. This blocks until the model
// is dead or the worker is stopped.
if err := u.processDyingModel(); err != nil {
return errors.Trace(err)
}
}
// If we get this far, the model must be dead (or *have been*
// dead, but actually removed by something else since the call).
if modelInfo.IsSystem {
// Nothing to do. We don't destroy environ resources or
// delete model docs for a controller model, because we're
// running inside that controller and can't safely clean up
// our own infrastructure. (That'll be the client's job in
// the end, once we've reported that we've tidied up what we
// can, by returning nil here, indicating that we've set it
// to Dead -- implied by processDyingModel succeeding.)
return nil
}
// Now the model is known to be hosted and dead, we can tidy up any
// provider resources it might have used.
if err := u.setStatus(
status.Destroying, "tearing down cloud environment",
); err != nil {
return errors.Trace(err)
}
if err := u.config.Destroyer.Destroy(u.callCtx); err != nil {
return errors.Trace(err)
}
// Finally, remove the model.
if err := u.config.Facade.RemoveModel(); err != nil {
return errors.Annotate(err, "cannot remove model")
}
return nil
}
func (u *Undertaker) setStatus(modelStatus status.Status, message string) error {
return u.config.Facade.SetStatus(modelStatus, message, nil)
}
func (u *Undertaker) processDyingModel() error {
watcher, err := u.config.Facade.WatchModelResources()
if err != nil {
return errors.Trace(err)
}
if err := u.catacomb.Add(watcher); err != nil {
return errors.Trace(err)
}
defer watcher.Kill()
attempt := 1
for {
select {
case <-u.catacomb.Dying():
return u.catacomb.ErrDying()
case <-watcher.Changes():
err := u.config.Facade.ProcessDyingModel()
if err == nil {
// ProcessDyingModel succeeded. We're free to
// destroy any remaining environ resources.
return nil
}
if !params.IsCodeModelNotEmpty(err) && !params.IsCodeHasHostedModels(err) {
return errors.Trace(err)
}
// Retry once there are changes to the model's resources.
u.setStatus(
status.Destroying,
fmt.Sprintf("attempt %d to destroy model failed (will retry): %v", attempt, err),
)
}
attempt++
}
}