forked from openshift/origin
-
Notifications
You must be signed in to change notification settings - Fork 1
/
rest.go
357 lines (309 loc) · 12.5 KB
/
rest.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
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
package instantiate
import (
"fmt"
"github.com/golang/glog"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
utilerrors "k8s.io/apimachinery/pkg/util/errors"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apiserver/pkg/admission"
apirequest "k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/apiserver/pkg/registry/generic/registry"
"k8s.io/apiserver/pkg/registry/rest"
kapi "k8s.io/kubernetes/pkg/api"
kapihelper "k8s.io/kubernetes/pkg/api/helper"
kclientset "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset"
kcoreclient "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/typed/core/internalversion"
"k8s.io/kubernetes/pkg/client/retry"
deployapi "github.com/openshift/origin/pkg/apps/apis/apps"
"github.com/openshift/origin/pkg/apps/apis/apps/validation"
deployutil "github.com/openshift/origin/pkg/apps/util"
imageapi "github.com/openshift/origin/pkg/image/apis/image"
imageclientinternal "github.com/openshift/origin/pkg/image/generated/internalclientset"
images "github.com/openshift/origin/pkg/image/generated/internalclientset/typed/image/internalversion"
)
// NewREST provides new REST storage for the apps API group.
func NewREST(store registry.Store, imagesclient imageclientinternal.Interface, kc kclientset.Interface, decoder runtime.Decoder, admission admission.Interface) *REST {
store.UpdateStrategy = Strategy
return &REST{store: &store, is: imagesclient.Image(), rn: kc.Core(), decoder: decoder, admit: admission}
}
// REST implements the Creater interface.
var _ = rest.Creater(&REST{})
type REST struct {
store *registry.Store
is images.ImageStreamsGetter
rn kcoreclient.ReplicationControllersGetter
decoder runtime.Decoder
admit admission.Interface
}
func (s *REST) New() runtime.Object {
return &deployapi.DeploymentRequest{}
}
// Create instantiates a deployment config
func (r *REST) Create(ctx apirequest.Context, obj runtime.Object, _ bool) (runtime.Object, error) {
req, ok := obj.(*deployapi.DeploymentRequest)
if !ok {
return nil, errors.NewInternalError(fmt.Errorf("wrong object passed for requesting a new rollout: %#v", obj))
}
var ret runtime.Object
err := retry.RetryOnConflict(retry.DefaultRetry, func() error {
configObj, err := r.store.Get(ctx, req.Name, &metav1.GetOptions{})
if err != nil {
return err
}
config := configObj.(*deployapi.DeploymentConfig)
old := config
if errs := validation.ValidateRequestForDeploymentConfig(req, config); len(errs) > 0 {
return errors.NewInvalid(deployapi.Kind("DeploymentRequest"), req.Name, errs)
}
// We need to process the deployment config before we can determine if it is possible to trigger
// a deployment.
if req.Latest {
if err := processTriggers(config, r.is, req.Force, req.ExcludeTriggers); err != nil {
return err
}
}
canTrigger, causes, err := canTrigger(config, r.rn, r.decoder, req.Force)
if err != nil {
return err
}
// If we cannot trigger then there is nothing to do here.
if !canTrigger {
ret = &metav1.Status{
Message: fmt.Sprintf("deployment config %q cannot be instantiated", config.Name),
Code: int32(204),
}
return nil
}
glog.V(4).Infof("New deployment for %q caused by %#v", config.Name, causes)
config.Status.Details = new(deployapi.DeploymentDetails)
config.Status.Details.Causes = causes
switch causes[0].Type {
case deployapi.DeploymentTriggerOnConfigChange:
config.Status.Details.Message = "config change"
case deployapi.DeploymentTriggerOnImageChange:
config.Status.Details.Message = "image change"
case deployapi.DeploymentTriggerManual:
config.Status.Details.Message = "manual change"
}
config.Status.LatestVersion++
userInfo, _ := apirequest.UserFrom(ctx)
attrs := admission.NewAttributesRecord(config, old, deployapi.Kind("DeploymentConfig").WithVersion(""), config.Namespace, config.Name, deployapi.Resource("DeploymentConfig").WithVersion(""), "", admission.Update, userInfo)
if err := r.admit.Admit(attrs); err != nil {
return err
}
ret, _, err = r.store.Update(ctx, config.Name, rest.DefaultUpdatedObjectInfo(config, kapi.Scheme))
return err
})
return ret, err
}
// processTriggers will go over all deployment triggers that require processing and update
// the deployment config accordingly. This contains the work that the image change controller
// had been doing up to the point we got the /instantiate endpoint.
func processTriggers(config *deployapi.DeploymentConfig, is images.ImageStreamsGetter, force bool, exclude []deployapi.DeploymentTriggerType) error {
errs := []error{}
// Process any image change triggers.
for _, trigger := range config.Spec.Triggers {
if trigger.Type != deployapi.DeploymentTriggerOnImageChange {
continue
}
params := trigger.ImageChangeParams
// Forced deployments should always try to resolve the images in the template.
// On the other hand, paused deployments or non-automatic triggers shouldn't.
if !force && (config.Spec.Paused || !params.Automatic) {
continue
}
if containsTriggerType(exclude, trigger.Type) {
continue
}
// Tag references are already validated
name, tag, _ := imageapi.SplitImageStreamTag(params.From.Name)
stream, err := is.ImageStreams(params.From.Namespace).Get(name, metav1.GetOptions{})
if err != nil {
if !errors.IsNotFound(err) {
errs = append(errs, err)
}
continue
}
// Find the latest tag event for the trigger reference.
latestReference, ok := imageapi.ResolveLatestTaggedImage(stream, tag)
if !ok {
continue
}
// Ensure a change occurred
if len(latestReference) == 0 || latestReference == params.LastTriggeredImage {
continue
}
// Update containers
names := sets.NewString(params.ContainerNames...)
for i := range config.Spec.Template.Spec.Containers {
container := &config.Spec.Template.Spec.Containers[i]
if !names.Has(container.Name) {
continue
}
if container.Image != latestReference || params.LastTriggeredImage != latestReference {
// Update the image
container.Image = latestReference
// Log the last triggered image ID
params.LastTriggeredImage = latestReference
}
}
for i := range config.Spec.Template.Spec.InitContainers {
container := &config.Spec.Template.Spec.InitContainers[i]
if !names.Has(container.Name) {
continue
}
if container.Image != latestReference || params.LastTriggeredImage != latestReference {
// Update the image
container.Image = latestReference
// Log the last triggered image ID
params.LastTriggeredImage = latestReference
}
}
}
if err := utilerrors.NewAggregate(errs); err != nil {
return errors.NewInternalError(err)
}
return nil
}
func containsTriggerType(types []deployapi.DeploymentTriggerType, triggerType deployapi.DeploymentTriggerType) bool {
for _, t := range types {
if t == triggerType {
return true
}
}
return false
}
// canTrigger determines if we can trigger a new deployment for config based on the various deployment triggers.
func canTrigger(
config *deployapi.DeploymentConfig,
rn kcoreclient.ReplicationControllersGetter,
decoder runtime.Decoder,
force bool,
) (bool, []deployapi.DeploymentCause, error) {
decoded, err := decodeFromLatestDeployment(config, rn, decoder)
if err != nil {
return false, nil, err
}
ictCount, resolved, canTriggerByImageChange := 0, 0, false
var causes []deployapi.DeploymentCause
for _, t := range config.Spec.Triggers {
if t.Type != deployapi.DeploymentTriggerOnImageChange {
continue
}
ictCount++
// If the image is yet to be resolved then we cannot process this trigger.
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 - there is no previous config to use for the
// comparison. Also configs with new/updated triggers should always trigger.
if config.Status.LatestVersion == 0 || hasUpdatedTriggers(*config, *decoded) || triggeredByDifferentImage(*t.ImageChangeParams, *decoded) {
canTriggerByImageChange = true
}
if !canTriggerByImageChange {
continue
}
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",
},
},
})
}
if ictCount != resolved {
err = errors.NewBadRequest(fmt.Sprintf("cannot trigger a deployment for %q because it contains unresolved images", config.Name))
return false, nil, err
}
if force {
return true, []deployapi.DeploymentCause{{Type: deployapi.DeploymentTriggerManual}}, nil
}
canTriggerByConfigChange := false
if deployutil.HasChangeTrigger(config) && // Our deployment config has a config change trigger
len(causes) == 0 && // and no other trigger has triggered.
(config.Status.LatestVersion == 0 || // Either it's the initial deployment
!kapihelper.Semantic.DeepEqual(config.Spec.Template, decoded.Spec.Template)) /* or a config change happened so we need to trigger */ {
canTriggerByConfigChange = true
causes = []deployapi.DeploymentCause{{Type: deployapi.DeploymentTriggerOnConfigChange}}
}
return canTriggerByConfigChange || canTriggerByImageChange, causes, nil
}
// decodeFromLatestDeployment 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 decodeFromLatestDeployment(config *deployapi.DeploymentConfig, rn kcoreclient.ReplicationControllersGetter, decoder runtime.Decoder) (*deployapi.DeploymentConfig, error) {
if config.Status.LatestVersion == 0 {
return config, nil
}
latestDeploymentName := deployutil.LatestDeploymentNameForConfig(config)
deployment, err := rn.ReplicationControllers(config.Namespace).Get(latestDeploymentName, metav1.GetOptions{})
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, err
}
decoded, err := deployutil.DecodeDeploymentConfig(deployment, decoder)
if err != nil {
return nil, errors.NewInternalError(err)
}
return decoded, nil
}
// hasUpdatedTriggers checks if there is an diffence between previous deployment config
// trigger configuration and current one.
func hasUpdatedTriggers(current, previous deployapi.DeploymentConfig) bool {
for _, ct := range current.Spec.Triggers {
found := false
if ct.Type != deployapi.DeploymentTriggerOnImageChange {
continue
}
for _, pt := range previous.Spec.Triggers {
if pt.Type != deployapi.DeploymentTriggerOnImageChange {
continue
}
if found = ct.ImageChangeParams.From.Namespace == pt.ImageChangeParams.From.Namespace &&
ct.ImageChangeParams.From.Name == pt.ImageChangeParams.From.Name; found {
break
}
}
if !found {
glog.V(4).Infof("Deployment config %s/%s current version contains new trigger %#v", current.Namespace, current.Name, ct)
return true
}
}
return false
}
// 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
}
if t.ImageChangeParams.LastTriggeredImage != ictParams.LastTriggeredImage {
glog.V(4).Infof("Deployment config %s/%s triggered by different image: %s -> %s", previous.Namespace, previous.Name, t.ImageChangeParams.LastTriggeredImage, ictParams.LastTriggeredImage)
return true
}
return false
}
return false
}