forked from openshift/origin
-
Notifications
You must be signed in to change notification settings - Fork 0
/
controller.go
233 lines (199 loc) · 8.4 KB
/
controller.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
package generictrigger
import (
"fmt"
kapi "k8s.io/kubernetes/pkg/api"
kclient "k8s.io/kubernetes/pkg/client/unversioned"
"k8s.io/kubernetes/pkg/runtime"
utilruntime "k8s.io/kubernetes/pkg/util/runtime"
"k8s.io/kubernetes/pkg/util/workqueue"
"github.com/golang/glog"
osclient "github.com/openshift/origin/pkg/client"
oscache "github.com/openshift/origin/pkg/client/cache"
deployapi "github.com/openshift/origin/pkg/deploy/api"
deployutil "github.com/openshift/origin/pkg/deploy/util"
)
// DeploymentTriggerController processes all triggers for a deployment config
// and kicks new deployments whenever possible.
type DeploymentTriggerController struct {
// dn is used to update deployment configs.
dn osclient.DeploymentConfigsNamespacer
// rn is used for getting the latest deployment for a config.
rn kclient.ReplicationControllersNamespacer
// queue contains deployment configs that need to be synced.
queue workqueue.RateLimitingInterface
// dcStore provides a local cache for deployment configs.
dcStore oscache.StoreToDeploymentConfigLister
// dcStoreSynced makes sure the dc store is synced before reconcling any deployment config.
dcStoreSynced func() bool
// codec is used for decoding a config out of a deployment.
codec runtime.Codec
}
// fatalError is an error which can't be retried.
type fatalError string
func (e fatalError) Error() string {
return fmt.Sprintf("fatal error handling configuration: %s", string(e))
}
// Handle processes deployment triggers for a deployment config.
func (c *DeploymentTriggerController) Handle(config *deployapi.DeploymentConfig) error {
if len(config.Spec.Triggers) == 0 || config.Spec.Paused {
return nil
}
// Try to decode this deployment config from the encoded annotation found in
// its latest deployment.
decoded, err := c.decodeFromLatest(config)
if err != nil {
return err
}
canTrigger, causes := canTrigger(config, decoded)
// Return if we cannot trigger a new deployment.
if !canTrigger {
return nil
}
copied, err := deployutil.DeploymentConfigDeepCopy(config)
if err != nil {
return err
}
return c.update(copied, causes)
}
// decodeFromLatest will try to return the decoded version of the current deploymentconfig found
// in the annotations of its latest deployment. If there is no previous deploymentconfig (ie.
// latestVersion == 0), the returned deploymentconfig will be the same.
func (c *DeploymentTriggerController) decodeFromLatest(config *deployapi.DeploymentConfig) (*deployapi.DeploymentConfig, error) {
if config.Status.LatestVersion == 0 {
return config, nil
}
latestDeploymentName := deployutil.LatestDeploymentNameForConfig(config)
deployment, err := c.rn.ReplicationControllers(config.Namespace).Get(latestDeploymentName)
if err != nil {
// If there's no deployment for the latest config, we have no basis of
// comparison. It's the responsibility of the deployment config controller
// to make the deployment for the config, so return early.
return nil, fmt.Errorf("couldn't retrieve deployment for deployment config %q: %v", deployutil.LabelForDeploymentConfig(config), err)
}
latest, err := deployutil.DecodeDeploymentConfig(deployment, c.codec)
if err != nil {
return nil, fatalError(err.Error())
}
return latest, nil
}
// canTrigger is used by the trigger controller to determine if the provided config can trigger
// a deployment.
//
// Image change triggers are processed first. It is required for all of them to point to images
// that exist. Otherwise, this controller will wait for the images to land and be updated in the
// triggers that point to them by the image change controller.
//
// Config change triggers are processed last. If all images are resolved and an automatic trigger
// was updated, then it should be possible to trigger a new deployment without a config change
// trigger. Otherwise, if a config change trigger exists and the config is not deployed yet or it
// has a podtemplate change, then the controller should trigger a new deployment (assuming all
// image change triggers can trigger).
func canTrigger(config, decoded *deployapi.DeploymentConfig) (bool, []deployapi.DeploymentCause) {
if decoded == nil {
// The decoded deployment config will never be nil here but a sanity check
// never hurts.
return false, nil
}
ictCount, resolved, canTriggerByImageChange := 0, 0, false
var causes []deployapi.DeploymentCause
// IMAGE CHANGE TRIGGERS
for _, t := range config.Spec.Triggers {
if t.Type != deployapi.DeploymentTriggerOnImageChange {
continue
}
ictCount++
// If this is the initial deployment then we need to wait for the image change controller
// to resolve the image inside the pod template.
lastTriggered := t.ImageChangeParams.LastTriggeredImage
if len(lastTriggered) == 0 {
continue
}
resolved++
// Non-automatic triggers should not be able to trigger deployments.
if !t.ImageChangeParams.Automatic {
continue
}
// We need stronger checks in order to validate that this template
// change is an image change. Look at the deserialized config's
// triggers and compare with the present trigger. Initial deployments
// should always trigger since there is no previous config to compare to.
if config.Status.LatestVersion > 0 {
if !triggeredByDifferentImage(*t.ImageChangeParams, *decoded) {
continue
}
}
canTriggerByImageChange = true
causes = append(causes, deployapi.DeploymentCause{
Type: deployapi.DeploymentTriggerOnImageChange,
ImageTrigger: &deployapi.DeploymentCauseImageTrigger{
From: kapi.ObjectReference{
Name: t.ImageChangeParams.From.Name,
Namespace: t.ImageChangeParams.From.Namespace,
Kind: "ImageStreamTag",
},
},
})
}
// We need to wait for all images to resolve before triggering a new deployment.
if ictCount != resolved {
return false, nil
}
// CONFIG CHANGE TRIGGERS
canTriggerByConfigChange := false
// Our deployment config has a config change trigger and no image change has triggered.
// If an image change had happened, it would be enough to start a new deployment without
// caring about the config change trigger.
if deployutil.HasChangeTrigger(config) && !canTriggerByImageChange {
// This is the initial deployment or the config has a template change. We need to
// kick a new deployment.
if config.Status.LatestVersion == 0 || !kapi.Semantic.DeepEqual(config.Spec.Template, decoded.Spec.Template) {
canTriggerByConfigChange = true
causes = []deployapi.DeploymentCause{{Type: deployapi.DeploymentTriggerOnConfigChange}}
}
}
return canTriggerByConfigChange || canTriggerByImageChange, causes
}
// triggeredByDifferentImage compares the provided image change parameters with those found in the
// previous deployment config (the one we decoded from the annotations of its latest deployment)
// and returns whether the two deployment configs have been triggered by a different image change.
func triggeredByDifferentImage(ictParams deployapi.DeploymentTriggerImageChangeParams, previous deployapi.DeploymentConfig) bool {
for _, t := range previous.Spec.Triggers {
if t.Type != deployapi.DeploymentTriggerOnImageChange {
continue
}
if t.ImageChangeParams.From.Name != ictParams.From.Name &&
t.ImageChangeParams.From.Namespace != ictParams.From.Namespace {
continue
}
return t.ImageChangeParams.LastTriggeredImage != ictParams.LastTriggeredImage
}
return false
}
// update increments the latestVersion of the provided deployment config so the deployment config
// controller can run a new deployment and also updates the details of the deployment config.
func (c *DeploymentTriggerController) update(config *deployapi.DeploymentConfig, causes []deployapi.DeploymentCause) error {
config.Status.LatestVersion++
config.Status.Details = new(deployapi.DeploymentDetails)
config.Status.Details.Causes = causes
switch causes[0].Type {
case deployapi.DeploymentTriggerOnConfigChange:
config.Status.Details.Message = "caused by a config change"
case deployapi.DeploymentTriggerOnImageChange:
config.Status.Details.Message = "caused by an image change"
}
_, err := c.dn.DeploymentConfigs(config.Namespace).UpdateStatus(config)
return err
}
func (c *DeploymentTriggerController) handleErr(err error, key interface{}) {
if err == nil {
c.queue.Forget(key)
return
}
if c.queue.NumRequeues(key) < MaxRetries {
glog.V(2).Infof("Error instantiating deployment config %v: %v", key, err)
c.queue.AddRateLimited(key)
return
}
utilruntime.HandleError(err)
c.queue.Forget(key)
}