Browse code

Implement oc set route-backend and A/B to describers / printers

oc set route-backends foo a=10 b=20 c=0

Adds describer and printer support for router.

Clayton Coleman authored on 2016/08/20 12:58:29
Showing 7 changed files
1 1
new file mode 100644
... ...
@@ -0,0 +1,585 @@
0
+package set
1
+
2
+import (
3
+	"fmt"
4
+	"io"
5
+	"os"
6
+	"strconv"
7
+	"strings"
8
+	"text/tabwriter"
9
+
10
+	"github.com/golang/glog"
11
+	"github.com/spf13/cobra"
12
+
13
+	kapi "k8s.io/kubernetes/pkg/api"
14
+	"k8s.io/kubernetes/pkg/api/meta"
15
+	"k8s.io/kubernetes/pkg/api/unversioned"
16
+	kcmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
17
+	"k8s.io/kubernetes/pkg/kubectl/resource"
18
+	"k8s.io/kubernetes/pkg/runtime"
19
+	"k8s.io/kubernetes/pkg/util/sets"
20
+
21
+	cmdutil "github.com/openshift/origin/pkg/cmd/util"
22
+	"github.com/openshift/origin/pkg/cmd/util/clientcmd"
23
+	routeapi "github.com/openshift/origin/pkg/route/api"
24
+)
25
+
26
+const (
27
+	backendsLong = `
28
+Set and adjust route backends
29
+
30
+Routes may have one or more optional backend services with weights controlling how much
31
+traffic flows to each service. Traffic is assigned proportional to the combined weights
32
+of each backend. A weight of zero means that the backend will receive no traffic. If all
33
+weights are zero the route will not send traffic to any backends.
34
+
35
+When setting backends, the first backend is the primary and the other backends are
36
+considered alternates. For example:
37
+
38
+    $ %[1]s route-backends web prod=99 canary=1
39
+
40
+will set the primary backend to service "prod" with a weight of 99 and the first
41
+alternate backend to service "canary" with a weight of 1. This means 99%% of traffic will
42
+be sent to the service "prod".
43
+
44
+The --adjust flag allows you to alter the weight of an individual service relative to
45
+itself or to the primary backend. Specifying a percentage will adjust the backend
46
+relative to either the primary or the first alternate (if you specify the primary).
47
+If there are other backends their weights will be kept proportional to the changed.
48
+
49
+Not all routers may support multiple or weighted backends.`
50
+
51
+	backendsExample = `  # Print the backends on the route 'web'
52
+  %[1]s route-backends web
53
+
54
+  # Set two backend services on route 'web' with 2/3rds of traffic going to 'a'
55
+  %[1]s route-backends web a=2 b=1
56
+
57
+  # Increase the traffic percentage going to b by 10%% relative to a
58
+  %[1]s route-backends web --adjust b=+10%%
59
+
60
+  # Set traffic percentage going to b to 10%% of the traffic going to a
61
+  %[1]s route-backends web --adjust b=10%%
62
+
63
+  # Set weight of b to 10
64
+  %[1]s route-backends web --adjust b=10
65
+
66
+  # Set the weight to all backends to zero
67
+  %[1]s route-backends web --zero`
68
+)
69
+
70
+type BackendsOptions struct {
71
+	Out io.Writer
72
+	Err io.Writer
73
+
74
+	Filenames []string
75
+	Selector  string
76
+	All       bool
77
+
78
+	Builder *resource.Builder
79
+	Infos   []*resource.Info
80
+
81
+	Encoder runtime.Encoder
82
+
83
+	ShortOutput   bool
84
+	Mapper        meta.RESTMapper
85
+	OutputVersion unversioned.GroupVersion
86
+
87
+	PrintTable  bool
88
+	PrintObject func(runtime.Object) error
89
+
90
+	Transform BackendTransform
91
+}
92
+
93
+// NewCmdRouteBackends implements the set route-backends command
94
+func NewCmdRouteBackends(fullName string, f *clientcmd.Factory, out, errOut io.Writer) *cobra.Command {
95
+	options := &BackendsOptions{
96
+		Out: out,
97
+		Err: errOut,
98
+	}
99
+	cmd := &cobra.Command{
100
+		Use:     "route-backends ROUTENAME [--zero|--equal] [--adjust] SERVICE=WEIGHT[%] [...]",
101
+		Short:   "Update the backends for a route",
102
+		Long:    fmt.Sprintf(backendsLong, fullName),
103
+		Example: fmt.Sprintf(backendsExample, fullName),
104
+		Run: func(cmd *cobra.Command, args []string) {
105
+			kcmdutil.CheckErr(options.Complete(f, cmd, args))
106
+			kcmdutil.CheckErr(options.Validate())
107
+			err := options.Run()
108
+			if err == cmdutil.ErrExit {
109
+				os.Exit(1)
110
+			}
111
+			kcmdutil.CheckErr(err)
112
+		},
113
+	}
114
+
115
+	kcmdutil.AddPrinterFlags(cmd)
116
+	cmd.Flags().StringVarP(&options.Selector, "selector", "l", options.Selector, "Selector (label query) to filter on")
117
+	cmd.Flags().BoolVar(&options.All, "all", options.All, "Select all resources in the namespace of the specified resource types")
118
+	cmd.Flags().StringSliceVarP(&options.Filenames, "filename", "f", options.Filenames, "Filename, directory, or URL to file to use to edit the resource.")
119
+
120
+	cmd.Flags().BoolVar(&options.Transform.Adjust, "adjust", options.Transform.Adjust, "Adjust a single backend using an absolute or relative weight. If the primary backend is selected and there is more than one alternate an error will be returned.")
121
+	cmd.Flags().BoolVar(&options.Transform.Zero, "zero", options.Transform.Zero, "Set the weight of all backends to zero.")
122
+	cmd.Flags().BoolVar(&options.Transform.Equal, "equal", options.Transform.Equal, "Set the weight of all backends to 100.")
123
+
124
+	cmd.MarkFlagFilename("filename", "yaml", "yml", "json")
125
+
126
+	return cmd
127
+}
128
+
129
+// Complete takes command line information to fill out BackendOptions or returns an error.
130
+func (o *BackendsOptions) Complete(f *clientcmd.Factory, cmd *cobra.Command, args []string) error {
131
+	cmdNamespace, explicit, err := f.DefaultNamespace()
132
+	if err != nil {
133
+		return err
134
+	}
135
+
136
+	clientConfig, err := f.ClientConfig()
137
+	if err != nil {
138
+		return err
139
+	}
140
+
141
+	o.OutputVersion, err = kcmdutil.OutputVersion(cmd, clientConfig.GroupVersion)
142
+	if err != nil {
143
+		return err
144
+	}
145
+
146
+	var resources []string
147
+	for _, arg := range args {
148
+		if !strings.Contains(arg, "=") {
149
+			resources = append(resources, arg)
150
+			continue
151
+		}
152
+		input, err := ParseBackendInput(arg)
153
+		if err != nil {
154
+			return fmt.Errorf("invalid argument %q: %v", arg, err)
155
+		}
156
+		o.Transform.Inputs = append(o.Transform.Inputs, *input)
157
+	}
158
+
159
+	o.PrintTable = o.Transform.Empty()
160
+
161
+	mapper, typer := f.Object(false)
162
+	o.Builder = resource.NewBuilder(mapper, typer, resource.ClientMapperFunc(f.ClientForMapping), kapi.Codecs.UniversalDecoder()).
163
+		ContinueOnError().
164
+		NamespaceParam(cmdNamespace).DefaultNamespace().
165
+		FilenameParam(explicit, false, o.Filenames...).
166
+		SelectorParam(o.Selector).
167
+		SelectAllParam(o.All).
168
+		ResourceNames("route", resources...).
169
+		Flatten()
170
+	if len(resources) == 0 {
171
+		o.Builder.ResourceTypes("routes")
172
+	}
173
+
174
+	output := kcmdutil.GetFlagString(cmd, "output")
175
+	if len(output) != 0 {
176
+		o.PrintObject = func(obj runtime.Object) error { return f.PrintObject(cmd, mapper, obj, o.Out) }
177
+	}
178
+
179
+	o.Encoder = f.JSONEncoder()
180
+	o.ShortOutput = kcmdutil.GetFlagString(cmd, "output") == "name"
181
+	o.Mapper = mapper
182
+
183
+	return nil
184
+}
185
+
186
+// Validate verifies the provided options are valid or returns an error.
187
+func (o *BackendsOptions) Validate() error {
188
+	return o.Transform.Validate()
189
+}
190
+
191
+// Run executes the BackendOptions or returns an error.
192
+func (o *BackendsOptions) Run() error {
193
+	infos := o.Infos
194
+	singular := len(o.Infos) <= 1
195
+	if o.Builder != nil {
196
+		loaded, err := o.Builder.Do().IntoSingular(&singular).Infos()
197
+		if err != nil {
198
+			return err
199
+		}
200
+		infos = loaded
201
+	}
202
+
203
+	if o.PrintTable && o.PrintObject == nil {
204
+		return o.printBackends(infos)
205
+	}
206
+
207
+	patches := CalculatePatches(infos, o.Encoder, func(info *resource.Info) (bool, error) {
208
+		return UpdateBackendsForObject(info.Object, o.Transform.Apply)
209
+	})
210
+	if singular && len(patches) == 0 {
211
+		return fmt.Errorf("%s/%s is not a deployment config or build config", infos[0].Mapping.Resource, infos[0].Name)
212
+	}
213
+	if o.PrintObject != nil {
214
+		object, err := resource.AsVersionedObject(infos, !singular, o.OutputVersion, kapi.Codecs.LegacyCodec(o.OutputVersion))
215
+		if err != nil {
216
+			return err
217
+		}
218
+		return o.PrintObject(object)
219
+	}
220
+
221
+	failed := false
222
+	for _, patch := range patches {
223
+		info := patch.Info
224
+		if patch.Err != nil {
225
+			failed = true
226
+			fmt.Fprintf(o.Err, "error: %s/%s %v\n", info.Mapping.Resource, info.Name, patch.Err)
227
+			continue
228
+		}
229
+
230
+		if string(patch.Patch) == "{}" || len(patch.Patch) == 0 {
231
+			fmt.Fprintf(o.Err, "info: %s %q was not changed\n", info.Mapping.Resource, info.Name)
232
+			continue
233
+		}
234
+
235
+		glog.V(4).Infof("Calculated patch %s", patch.Patch)
236
+
237
+		obj, err := resource.NewHelper(info.Client, info.Mapping).Patch(info.Namespace, info.Name, kapi.StrategicMergePatchType, patch.Patch)
238
+		if err != nil {
239
+			handlePodUpdateError(o.Err, err, "altered")
240
+			failed = true
241
+			continue
242
+		}
243
+
244
+		info.Refresh(obj, true)
245
+		kcmdutil.PrintSuccess(o.Mapper, o.ShortOutput, o.Out, info.Mapping.Resource, info.Name, "updated")
246
+	}
247
+	if failed {
248
+		return cmdutil.ErrExit
249
+	}
250
+	return nil
251
+}
252
+
253
+// printBackends displays a tabular output of the backends for each object.
254
+func (o *BackendsOptions) printBackends(infos []*resource.Info) error {
255
+	w := tabwriter.NewWriter(o.Out, 0, 2, 2, ' ', 0)
256
+	defer w.Flush()
257
+	fmt.Fprintf(w, "NAME\tKIND\tTO\tWEIGHT\n")
258
+	for _, info := range infos {
259
+		_, err := UpdateBackendsForObject(info.Object, func(backends *Backends) error {
260
+			totalWeight := int32(0)
261
+			for _, b := range backends.Backends {
262
+				if b.Weight != nil {
263
+					totalWeight += *b.Weight
264
+				}
265
+			}
266
+			for _, b := range backends.Backends {
267
+				switch {
268
+				case b.Weight == nil:
269
+					fmt.Fprintf(w, "%s/%s\t%s\t%s\t%s\n", info.Mapping.Resource, info.Name, b.Kind, b.Name, "")
270
+				case totalWeight == 0, len(backends.Backends) == 1 && totalWeight != 0:
271
+					fmt.Fprintf(w, "%s/%s\t%s\t%s\t%d\n", info.Mapping.Resource, info.Name, b.Kind, b.Name, totalWeight)
272
+				default:
273
+					fmt.Fprintf(w, "%s/%s\t%s\t%s\t%d (%d%%)\n", info.Mapping.Resource, info.Name, b.Kind, b.Name, *b.Weight, *b.Weight*100/totalWeight)
274
+				}
275
+			}
276
+			return nil
277
+		})
278
+		if err != nil {
279
+			fmt.Fprintf(w, "%s/%s\t%s\t%s\t%d\n", info.Mapping.Resource, info.Name, "", "<error>", 0)
280
+		}
281
+	}
282
+	return nil
283
+}
284
+
285
+// BackendTransform describes the desired transformation of backends.
286
+type BackendTransform struct {
287
+	// Adjust expects a single Input to transform, relative to other backends.
288
+	Adjust bool
289
+	// Zero sets all backend weights to zero.
290
+	Zero bool
291
+	// Equal means backends will be set to equal weights.
292
+	Equal bool
293
+	// Inputs is the desired backends.
294
+	Inputs []BackendInput
295
+}
296
+
297
+// Empty returns true if no transformations have been specified.
298
+func (t BackendTransform) Empty() bool {
299
+	return !(t.Zero || t.Equal || len(t.Inputs) > 0)
300
+}
301
+
302
+// Validate returns an error if the transformations are not internally consistent.
303
+func (t BackendTransform) Validate() error {
304
+	switch {
305
+	case t.Adjust:
306
+		if t.Zero {
307
+			return fmt.Errorf("--adjust and --zero may not be specified together")
308
+		}
309
+		if t.Equal {
310
+			return fmt.Errorf("--adjust and --equal may not be specified together")
311
+		}
312
+		if len(t.Inputs) != 1 {
313
+			return fmt.Errorf("only one backend may be specified when adjusting")
314
+		}
315
+
316
+	case t.Zero, t.Equal:
317
+		if t.Equal && t.Zero {
318
+			return fmt.Errorf("--zero and --equal may not be specified together")
319
+		}
320
+		if len(t.Inputs) > 0 {
321
+			return fmt.Errorf("arguments may not be provided when --zero or --equal is specified")
322
+		}
323
+
324
+	default:
325
+		percent := false
326
+		names := sets.NewString()
327
+		for i, input := range t.Inputs {
328
+			if names.Has(input.Name) {
329
+				return fmt.Errorf("backend name %q may only be specified once", input.Name)
330
+			}
331
+			names.Insert(input.Name)
332
+			if input.Percentage {
333
+				if !percent && i != 0 {
334
+					return fmt.Errorf("all backends must either be percentages or weights")
335
+				}
336
+				percent = true
337
+			}
338
+			if input.Value < 0 {
339
+				return fmt.Errorf("negative percentages are not allowed")
340
+			}
341
+		}
342
+	}
343
+	return nil
344
+}
345
+
346
+// Apply transforms the provided backends or returns an error.
347
+func (t BackendTransform) Apply(b *Backends) error {
348
+	switch {
349
+	case t.Zero:
350
+		zero := int32(0)
351
+		for i := range b.Backends {
352
+			b.Backends[i].Weight = &zero
353
+		}
354
+
355
+	case t.Equal:
356
+		equal := int32(100)
357
+		for i := range b.Backends {
358
+			b.Backends[i].Weight = &equal
359
+		}
360
+
361
+	case t.Adjust:
362
+		input := t.Inputs[0]
363
+		switch {
364
+		case len(b.Backends) == 0:
365
+			return fmt.Errorf("no backends can be adjusted")
366
+		case len(b.Backends) == 1:
367
+			// treat adjusting primary specially
368
+			backend := &b.Backends[0]
369
+			if backend.Name != input.Name {
370
+				return fmt.Errorf("backend %q is not in the list of backends (%s)", input.Name, strings.Join(b.Names(), ", "))
371
+			}
372
+			if input.Relative {
373
+				return fmt.Errorf("cannot adjust a single backend by relative weight")
374
+			}
375
+			// ignore distinction between percentage and weight for single backend
376
+			backend.Weight = &input.Value
377
+		case b.Backends[0].Name == input.Name:
378
+			// changing the primary backend, multiple available
379
+			if len(b.Backends) == 1 {
380
+				input.Apply(&b.Backends[0], nil, b.Backends)
381
+				return nil
382
+			}
383
+			input.Apply(&b.Backends[0], &b.Backends[1], b.Backends)
384
+
385
+		default:
386
+			// changing an alternate backend, multiple available
387
+			for i := range b.Backends {
388
+				if b.Backends[i].Name != input.Name {
389
+					continue
390
+				}
391
+				input.Apply(&b.Backends[i], &b.Backends[0], b.Backends)
392
+				return nil
393
+			}
394
+			return fmt.Errorf("backend %q is not in the list of backends (%s)", input.Name, strings.Join(b.Names(), ", "))
395
+		}
396
+
397
+	default:
398
+		b.Backends = nil
399
+		for _, input := range t.Inputs {
400
+			weight := input.Value
401
+			b.Backends = append(b.Backends, routeapi.RouteTargetReference{
402
+				Kind:   "Service",
403
+				Name:   input.Name,
404
+				Weight: &weight,
405
+			})
406
+		}
407
+	}
408
+	return nil
409
+}
410
+
411
+// BackendInput describes a change to a named service.
412
+type BackendInput struct {
413
+	// Name is the name of a service.
414
+	Name string
415
+	// Value is the amount to change.
416
+	Value int32
417
+	// Percentage means value should be interpreted as a percentage between -100 and 100, inclusive.
418
+	Percentage bool
419
+	// Relative means value is applied relative to the current values.
420
+	Relative bool
421
+}
422
+
423
+// Apply alters the weights of two services.
424
+func (input *BackendInput) Apply(ref, to *routeapi.RouteTargetReference, backends []routeapi.RouteTargetReference) {
425
+	weight := int32(100)
426
+	if ref.Weight != nil {
427
+		weight = *ref.Weight
428
+	}
429
+	switch {
430
+	case input.Percentage:
431
+		if to == nil {
432
+			weight += (weight * input.Value) / 100
433
+			ref.Weight = &weight
434
+			return
435
+		}
436
+
437
+		otherWeight := int32(0)
438
+		if to.Weight != nil {
439
+			otherWeight = *to.Weight
440
+		}
441
+		previousWeight := weight + otherWeight
442
+
443
+		// rebalance all other backends to be relative in weight to the current
444
+		for i, other := range backends {
445
+			if previousWeight == 0 || other.Weight == nil || other.Name == ref.Name || other.Name == to.Name {
446
+				continue
447
+			}
448
+			adjusted := *other.Weight * 100 / previousWeight
449
+			backends[i].Weight = &adjusted
450
+		}
451
+
452
+		// adjust the weight between ref and to
453
+		target := float32(input.Value) / 100
454
+		if input.Relative {
455
+			if previousWeight != 0 {
456
+				percent := float32(weight) / float32(previousWeight)
457
+				target = percent + target
458
+			}
459
+		}
460
+		switch {
461
+		case target < 0:
462
+			target = 0
463
+		case target > 1:
464
+			target = 1
465
+		}
466
+		weight = int32(target * 100)
467
+		otherWeight = int32((1 - target) * 100)
468
+		ref.Weight = &weight
469
+		to.Weight = &otherWeight
470
+
471
+		// rescale the max to 200 in case we are dealing with very small percentages
472
+		max := int32(0)
473
+		for _, other := range backends {
474
+			if other.Weight == nil {
475
+				continue
476
+			}
477
+			if *other.Weight > max {
478
+				max = *other.Weight
479
+			}
480
+		}
481
+		if max > 256 {
482
+			for i, other := range backends {
483
+				if other.Weight == nil || *other.Weight == 0 {
484
+					continue
485
+				}
486
+				adjusted := 200 * *other.Weight / max
487
+				if adjusted < 1 {
488
+					adjusted = 1
489
+				}
490
+				backends[i].Weight = &adjusted
491
+			}
492
+		}
493
+
494
+	case input.Relative:
495
+		weight += input.Value
496
+		if weight < 0 {
497
+			weight = 0
498
+		}
499
+		ref.Weight = &weight
500
+
501
+	default:
502
+		ref.Weight = &input.Value
503
+	}
504
+}
505
+
506
+// ParseBackendInput turns the provided input into a BackendInput or returns an error.
507
+func ParseBackendInput(s string) (*BackendInput, error) {
508
+	parts := strings.SplitN(s, "=", 2)
509
+	switch {
510
+	case len(parts) != 2, len(parts[0]) == 0, len(parts[1]) == 0:
511
+		return nil, fmt.Errorf("expected NAME=WEIGHT")
512
+	}
513
+
514
+	if strings.Contains(parts[0], "/") {
515
+		return nil, fmt.Errorf("only NAME=WEIGHT may be specified")
516
+	}
517
+
518
+	input := &BackendInput{}
519
+	input.Name = parts[0]
520
+
521
+	if strings.HasSuffix(parts[1], "%") {
522
+		input.Percentage = true
523
+		parts[1] = strings.TrimSuffix(parts[1], "%")
524
+	}
525
+	if strings.HasPrefix(parts[1], "+") {
526
+		input.Relative = true
527
+		parts[1] = strings.TrimPrefix(parts[1], "+")
528
+	}
529
+	value, err := strconv.Atoi(parts[1])
530
+	if err != nil {
531
+		return nil, fmt.Errorf("WEIGHT must be a number: %v", err)
532
+	}
533
+	input.Value = int32(value)
534
+	if input.Value < 0 {
535
+		input.Relative = true
536
+	}
537
+	return input, nil
538
+}
539
+
540
+// Backends is a struct that represents the backends to be transformed.
541
+type Backends struct {
542
+	Backends []routeapi.RouteTargetReference
543
+}
544
+
545
+// Names returns the referenced backend service names, in the order they appear.
546
+func (b *Backends) Names() []string {
547
+	var names []string
548
+	for _, backend := range b.Backends {
549
+		names = append(names, backend.Name)
550
+	}
551
+	return names
552
+}
553
+
554
+// UpdateBackendsForObject extracts a backend definition array from the provided object, passes it to fn,
555
+// and then applies the backend on the object. It returns true if the object was mutated and an optional error
556
+// if any part of the flow returns error.
557
+func UpdateBackendsForObject(obj runtime.Object, fn func(*Backends) error) (bool, error) {
558
+	// TODO: replace with a swagger schema based approach (identify pod template via schema introspection)
559
+	switch t := obj.(type) {
560
+	case *routeapi.Route:
561
+		b := &Backends{
562
+			Backends: []routeapi.RouteTargetReference{t.Spec.To},
563
+		}
564
+		for _, backend := range t.Spec.AlternateBackends {
565
+			b.Backends = append(b.Backends, backend)
566
+		}
567
+		if err := fn(b); err != nil {
568
+			return true, err
569
+		}
570
+		if len(b.Backends) == 0 {
571
+			t.Spec.To = routeapi.RouteTargetReference{}
572
+		} else {
573
+			t.Spec.To = b.Backends[0]
574
+		}
575
+		if len(b.Backends) > 1 {
576
+			t.Spec.AlternateBackends = b.Backends[1:]
577
+		} else {
578
+			t.Spec.AlternateBackends = nil
579
+		}
580
+		return true, nil
581
+	default:
582
+		return false, fmt.Errorf("the object is not a route")
583
+	}
584
+}
... ...
@@ -46,6 +46,12 @@ func NewCmdSet(fullName string, f *clientcmd.Factory, in io.Reader, out, errout
46 46
 				NewCmdBuildHook(name, f, out, errout),
47 47
 			},
48 48
 		},
49
+		{
50
+			Message: "Control load balancing:",
51
+			Commands: []*cobra.Command{
52
+				NewCmdRouteBackends(name, f, out, errout),
53
+			},
54
+		},
49 55
 	}
50 56
 	groups.Add(set)
51 57
 	templates.ActsAsRootCommand(set, []string{"options"}, groups...)
... ...
@@ -652,6 +652,11 @@ type RouteDescriber struct {
652 652
 	kubeClient kclient.Interface
653 653
 }
654 654
 
655
+type routeEndpointInfo struct {
656
+	*kapi.Endpoints
657
+	Err error
658
+}
659
+
655 660
 // Describe returns the description of a route
656 661
 func (d *RouteDescriber) Describe(namespace, name string, settings kctl.DescriberSettings) (string, error) {
657 662
 	c := d.Routes(namespace)
... ...
@@ -660,7 +665,16 @@ func (d *RouteDescriber) Describe(namespace, name string, settings kctl.Describe
660 660
 		return "", err
661 661
 	}
662 662
 
663
-	endpoints, endsErr := d.kubeClient.Endpoints(namespace).Get(route.Spec.To.Name)
663
+	backends := append([]routeapi.RouteTargetReference{route.Spec.To}, route.Spec.AlternateBackends...)
664
+	totalWeight := int32(0)
665
+	endpoints := make(map[string]routeEndpointInfo)
666
+	for _, backend := range backends {
667
+		if backend.Weight != nil {
668
+			totalWeight += *backend.Weight
669
+		}
670
+		ep, endpointsErr := d.kubeClient.Endpoints(namespace).Get(backend.Name)
671
+		endpoints[backend.Name] = routeEndpointInfo{ep, endpointsErr}
672
+	}
664 673
 
665 674
 	return tabbedString(func(out *tabwriter.Writer) error {
666 675
 		formatMeta(out, route.ObjectMeta)
... ...
@@ -683,6 +697,7 @@ func (d *RouteDescriber) Describe(namespace, name string, settings kctl.Describe
683 683
 		} else {
684 684
 			formatString(out, "Requested Host", "<auto>")
685 685
 		}
686
+
686 687
 		for _, ingress := range route.Status.Ingress {
687 688
 			if route.Spec.Host == ingress.Host {
688 689
 				continue
... ...
@@ -707,23 +722,39 @@ func (d *RouteDescriber) Describe(namespace, name string, settings kctl.Describe
707 707
 		}
708 708
 		formatString(out, "TLS Termination", tlsTerm)
709 709
 		formatString(out, "Insecure Policy", insecurePolicy)
710
-
711
-		formatString(out, "Service", route.Spec.To.Name)
712 710
 		if route.Spec.Port != nil {
713 711
 			formatString(out, "Endpoint Port", route.Spec.Port.TargetPort.String())
714 712
 		} else {
715 713
 			formatString(out, "Endpoint Port", "<all endpoint ports>")
716 714
 		}
717 715
 
718
-		ends := "<none>"
719
-		if endsErr != nil {
720
-			ends = fmt.Sprintf("Unable to get endpoints: %v", endsErr)
721
-		} else if len(endpoints.Subsets) > 0 {
722
-			list := []string{}
716
+		for _, backend := range backends {
717
+			fmt.Fprintln(out)
718
+			formatString(out, "Service", backend.Name)
719
+			weight := int32(0)
720
+			if backend.Weight != nil {
721
+				weight = *backend.Weight
722
+			}
723
+			if weight > 0 {
724
+				fmt.Fprintf(out, "Weight:\t%d (%d%%)\n", weight, weight*100/totalWeight)
725
+			} else {
726
+				formatString(out, "Weight", "0")
727
+			}
728
+
729
+			info := endpoints[backend.Name]
730
+			if info.Err != nil {
731
+				formatString(out, "Endpoints", fmt.Sprintf("<error: %v>", info.Err))
732
+				continue
733
+			}
734
+			endpoints := info.Endpoints
735
+			if len(endpoints.Subsets) == 0 {
736
+				formatString(out, "Endpoints", "<none>")
737
+				continue
738
+			}
723 739
 
740
+			list := []string{}
724 741
 			max := 3
725 742
 			count := 0
726
-
727 743
 			for i := range endpoints.Subsets {
728 744
 				ss := &endpoints.Subsets[i]
729 745
 				for p := range ss.Ports {
... ...
@@ -735,12 +766,12 @@ func (d *RouteDescriber) Describe(namespace, name string, settings kctl.Describe
735 735
 					}
736 736
 				}
737 737
 			}
738
-			ends = strings.Join(list, ", ")
738
+			ends := strings.Join(list, ", ")
739 739
 			if count > max {
740 740
 				ends += fmt.Sprintf(" + %d more...", count-max)
741 741
 			}
742
+			formatString(out, "Endpoints", ends)
742 743
 		}
743
-		formatString(out, "Endpoints", ends)
744 744
 		return nil
745 745
 	})
746 746
 }
... ...
@@ -12,7 +12,6 @@ import (
12 12
 	kapi "k8s.io/kubernetes/pkg/api"
13 13
 	"k8s.io/kubernetes/pkg/api/unversioned"
14 14
 	kctl "k8s.io/kubernetes/pkg/kubectl"
15
-	"k8s.io/kubernetes/pkg/labels"
16 15
 	"k8s.io/kubernetes/pkg/util/sets"
17 16
 
18 17
 	authorizationapi "github.com/openshift/origin/pkg/authorization/api"
... ...
@@ -36,7 +35,7 @@ var (
36 36
 	imageStreamImageColumns = []string{"NAME", "DOCKER REF", "UPDATED", "IMAGENAME"}
37 37
 	imageStreamColumns      = []string{"NAME", "DOCKER REPO", "TAGS", "UPDATED"}
38 38
 	projectColumns          = []string{"NAME", "DISPLAY NAME", "STATUS"}
39
-	routeColumns            = []string{"NAME", "HOST/PORT", "PATH", "SERVICE", "TERMINATION", "LABELS"}
39
+	routeColumns            = []string{"NAME", "HOST/PORT", "PATH", "SERVICES", "PORT", "TERMINATION"}
40 40
 	deploymentConfigColumns = []string{"NAME", "REVISION", "DESIRED", "CURRENT", "TRIGGERED BY"}
41 41
 	templateColumns         = []string{"NAME", "DESCRIPTION", "PARAMETERS", "OBJECTS"}
42 42
 	policyColumns           = []string{"NAME", "ROLES", "LAST MODIFIED"}
... ...
@@ -555,14 +554,35 @@ func printRoute(route *routeapi.Route, w io.Writer, opts kctl.PrintOptions) erro
555 555
 	default:
556 556
 		policy = ""
557 557
 	}
558
-	svc := route.Spec.To.Name
559
-	if route.Spec.Port != nil {
560
-		svc = fmt.Sprintf("%s:%s", svc, route.Spec.Port.TargetPort.String())
558
+
559
+	backends := append([]routeapi.RouteTargetReference{route.Spec.To}, route.Spec.AlternateBackends...)
560
+	totalWeight := int32(0)
561
+	for _, backend := range backends {
562
+		if backend.Weight != nil {
563
+			totalWeight += *backend.Weight
564
+		}
561 565
 	}
562
-	if _, err := fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\n", name, host, route.Spec.Path, svc, policy, labels.Set(route.Labels)); err != nil {
563
-		return err
566
+	var backendInfo []string
567
+	for _, backend := range backends {
568
+		switch {
569
+		case backend.Weight == nil, len(backends) == 1 && totalWeight != 0:
570
+			backendInfo = append(backendInfo, backend.Name)
571
+		case totalWeight == 0:
572
+			backendInfo = append(backendInfo, fmt.Sprintf("%s(0%%)", backend.Name))
573
+		default:
574
+			backendInfo = append(backendInfo, fmt.Sprintf("%s(%d%%)", backend.Name, *backend.Weight*100/totalWeight))
575
+		}
564 576
 	}
565
-	return nil
577
+
578
+	var port string
579
+	if route.Spec.Port != nil {
580
+		port = route.Spec.Port.TargetPort.String()
581
+	} else {
582
+		port = "<all>"
583
+	}
584
+
585
+	_, err := fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\n", name, host, route.Spec.Path, strings.Join(backendInfo, ","), port, policy)
586
+	return err
566 587
 }
567 588
 
568 589
 func printRouteList(routeList *routeapi.RouteList, w io.Writer, opts kctl.PrintOptions) error {
... ...
@@ -50,18 +50,19 @@ func ValidateRoute(route *routeapi.Route) field.ErrorList {
50 50
 		result = append(result, field.Invalid(specPath.Child("to", "weight"), route.Spec.To.Weight, "weight must be an integer between 0 and 256"))
51 51
 	}
52 52
 
53
+	backendPath := specPath.Child("alternateBackends")
53 54
 	if len(route.Spec.AlternateBackends) > 3 {
54
-		result = append(result, field.Required(specPath.Child("alternateBackends"), "cannot specify more than 3 additional backends"))
55
+		result = append(result, field.Required(backendPath, "cannot specify more than 3 additional backends"))
55 56
 	}
56
-	for _, svc := range route.Spec.AlternateBackends {
57
+	for i, svc := range route.Spec.AlternateBackends {
57 58
 		if len(svc.Name) == 0 {
58
-			result = append(result, field.Required(specPath.Child("alternateBackends", "name"), ""))
59
+			result = append(result, field.Required(backendPath.Index(i).Child("name"), ""))
59 60
 		}
60 61
 		if svc.Kind != "Service" {
61
-			result = append(result, field.Invalid(specPath.Child("alternateBackends", "kind"), svc.Kind, "must reference a Service"))
62
+			result = append(result, field.Invalid(backendPath.Index(i).Child("kind"), svc.Kind, "must reference a Service"))
62 63
 		}
63 64
 		if svc.Weight != nil && (*svc.Weight < 0 || *svc.Weight > 256) {
64
-			result = append(result, field.Invalid(specPath.Child("alternateBackends", "weight"), svc.Weight, "weight must be an integer between 0 and 256"))
65
+			result = append(result, field.Invalid(backendPath.Index(i).Child("weight"), svc.Weight, "weight must be an integer between 0 and 256"))
65 66
 		}
66 67
 	}
67 68
 
... ...
@@ -174,24 +174,6 @@ os::cmd::expect_success 'oc delete -f examples/pets/zookeeper/zookeeper.yaml'
174 174
 echo "petsets: ok"
175 175
 os::test::junit::declare_suite_end
176 176
 
177
-os::test::junit::declare_suite_start "cmd/basicresources/routes"
178
-os::cmd::expect_success 'oc get routes'
179
-os::cmd::expect_success 'oc create -f test/integration/testdata/test-route.json'
180
-os::cmd::expect_success 'oc delete routes testroute'
181
-os::cmd::expect_success 'oc create -f test/integration/testdata/test-service.json'
182
-os::cmd::expect_success 'oc create route passthrough --service=svc/frontend'
183
-os::cmd::expect_success 'oc delete routes frontend'
184
-os::cmd::expect_success 'oc create route edge --path /test --service=services/non-existent --port=80'
185
-os::cmd::expect_success 'oc delete routes non-existent'
186
-os::cmd::expect_success 'oc create route edge test-route --service=frontend'
187
-os::cmd::expect_success 'oc delete routes test-route'
188
-os::cmd::expect_failure 'oc create route edge new-route'
189
-os::cmd::expect_success 'oc delete services frontend'
190
-os::cmd::expect_success 'oc create route edge --insecure-policy=Allow --service=foo --port=80'
191
-os::cmd::expect_success_and_text 'oc get route foo -o jsonpath="{.spec.tls.insecureEdgeTerminationPolicy}"' 'Allow'
192
-os::cmd::expect_success 'oc delete routes foo'
193
-echo "routes: ok"
194
-os::test::junit::declare_suite_end
195 177
 
196 178
 os::test::junit::declare_suite_start "cmd/basicresources/setprobe"
197 179
 # Validate the probe command
... ...
@@ -291,7 +273,7 @@ os::cmd::expect_success 'oc delete svc,route -l name=frontend'
291 291
 # Test that external services are exposable
292 292
 os::cmd::expect_success 'oc create -f test/testdata/external-service.yaml'
293 293
 os::cmd::expect_success 'oc expose svc/external'
294
-os::cmd::expect_success_and_text 'oc get route external' 'external=service'
294
+os::cmd::expect_success_and_text 'oc get route external' 'external'
295 295
 os::cmd::expect_success 'oc delete route external'
296 296
 os::cmd::expect_success 'oc delete svc external'
297 297
 # Expose multiport service and verify we set a port in the route
298 298
new file mode 100755
... ...
@@ -0,0 +1,78 @@
0
+#!/bin/bash
1
+source "$(dirname "${BASH_SOURCE}")/../../hack/lib/init.sh"
2
+trap os::test::junit::reconcile_output EXIT
3
+
4
+# Cleanup cluster resources created by this test
5
+(
6
+  set +e
7
+  oc delete route foo bar testroute test-route new-route
8
+  exit 0
9
+) &>/dev/null
10
+
11
+
12
+os::test::junit::declare_suite_start "cmd/routes"
13
+
14
+os::cmd::expect_success 'oc get routes'
15
+os::cmd::expect_success 'oc create -f test/integration/testdata/test-route.json'
16
+os::cmd::expect_success 'oc delete routes testroute'
17
+os::cmd::expect_success 'oc create -f test/integration/testdata/test-service.json'
18
+os::cmd::expect_success 'oc create route passthrough --service=svc/frontend'
19
+os::cmd::expect_success 'oc delete routes frontend'
20
+os::cmd::expect_success 'oc create route edge --path /test --service=services/non-existent --port=80'
21
+os::cmd::expect_success 'oc delete routes non-existent'
22
+os::cmd::expect_success 'oc create route edge test-route --service=frontend'
23
+os::cmd::expect_success 'oc delete routes test-route'
24
+os::cmd::expect_failure 'oc create route edge new-route'
25
+os::cmd::expect_success 'oc delete services frontend'
26
+os::cmd::expect_success 'oc create route edge --insecure-policy=Allow --service=foo --port=80'
27
+os::cmd::expect_success_and_text 'oc get route foo -o jsonpath="{.spec.tls.insecureEdgeTerminationPolicy}"' 'Allow'
28
+os::cmd::expect_success 'oc delete routes foo'
29
+
30
+os::cmd::expect_success_and_text 'oc create route edge --service foo --port=8080' 'created'
31
+os::cmd::expect_success_and_text 'oc create route edge --service bar --port=9090' 'created'
32
+
33
+os::cmd::expect_success_and_text 'oc set route-backends foo' 'routes/foo'
34
+os::cmd::expect_success_and_text 'oc set route-backends foo' 'Service'
35
+os::cmd::expect_success_and_text 'oc set route-backends foo' '100'
36
+os::cmd::expect_failure_and_text 'oc set route-backends foo --zero --equal' 'error: --zero and --equal may not be specified together'
37
+os::cmd::expect_failure_and_text 'oc set route-backends foo --zero --adjust' 'error: --adjust and --zero may not be specified together'
38
+os::cmd::expect_failure_and_text 'oc set route-backends foo a=' 'expected NAME=WEIGHT'
39
+os::cmd::expect_failure_and_text 'oc set route-backends foo =10' 'expected NAME=WEIGHT'
40
+os::cmd::expect_failure_and_text 'oc set route-backends foo a=a' 'WEIGHT must be a number'
41
+os::cmd::expect_success_and_text 'oc set route-backends foo a=10' 'updated'
42
+os::cmd::expect_success_and_text 'oc set route-backends foo a=100' 'updated'
43
+os::cmd::expect_success_and_text 'oc set route-backends foo a=0' 'updated'
44
+os::cmd::expect_success_and_text 'oc set route-backends foo' '0'
45
+os::cmd::expect_success_and_text 'oc get routes foo' 'a'
46
+os::cmd::expect_success_and_text 'oc set route-backends foo a1=0 b2=0' 'updated'
47
+os::cmd::expect_success_and_text 'oc set route-backends foo' 'a1'
48
+os::cmd::expect_success_and_text 'oc set route-backends foo' 'b2'
49
+os::cmd::expect_success_and_text 'oc set route-backends foo a1=100 b2=50 c3=0' 'updated'
50
+os::cmd::expect_success_and_text 'oc get routes foo' 'a1\(66%\),b2\(33%\),c3\(0%\)'
51
+os::cmd::expect_success_and_text 'oc set route-backends foo a1=100 b2=0 c3=0' 'updated'
52
+os::cmd::expect_success_and_text 'oc set route-backends foo --adjust b2=+10%' 'updated'
53
+os::cmd::expect_success_and_text 'oc get routes foo' 'a1\(90%\),b2\(10%\),c3\(0%\)'
54
+os::cmd::expect_success_and_text 'oc set route-backends foo --adjust b2=+25%' 'updated'
55
+os::cmd::expect_success_and_text 'oc get routes foo' 'a1\(65%\),b2\(35%\),c3\(0%\)'
56
+os::cmd::expect_success_and_text 'oc set route-backends foo --adjust b2=+99%' 'updated'
57
+os::cmd::expect_success_and_text 'oc get routes foo' 'a1\(0%\),b2\(100%\),c3\(0%\)'
58
+os::cmd::expect_success_and_text 'oc set route-backends foo --adjust b2=-51%' 'updated'
59
+os::cmd::expect_success_and_text 'oc get routes foo' 'a1\(51%\),b2\(49%\),c3\(0%\)'
60
+os::cmd::expect_success_and_text 'oc set route-backends foo --adjust a1=20%' 'updated'
61
+os::cmd::expect_success_and_text 'oc get routes foo' 'a1\(20%\),b2\(80%\),c3\(0%\)'
62
+os::cmd::expect_success_and_text 'oc set route-backends foo --adjust c3=50%' 'updated'
63
+os::cmd::expect_success_and_text 'oc get routes foo' 'a1\(10%\),b2\(80%\),c3\(10%\)'
64
+os::cmd::expect_success_and_text 'oc describe routes foo' '25 \(10%\)'
65
+os::cmd::expect_success_and_text 'oc describe routes foo' '200 \(80%\)'
66
+os::cmd::expect_success_and_text 'oc describe routes foo' '25 \(10%\)'
67
+os::cmd::expect_success_and_text 'oc describe routes foo' '<error: endpoints "c3" not found>'
68
+os::cmd::expect_success_and_text 'oc set route-backends foo --adjust c3=1' 'updated'
69
+os::cmd::expect_success_and_text 'oc describe routes foo' '1 \(0%\)'
70
+os::cmd::expect_success_and_text 'oc set route-backends foo --equal' 'updated'
71
+os::cmd::expect_success_and_text 'oc get routes foo' 'a1\(33%\),b2\(33%\),c3\(33%\)'
72
+os::cmd::expect_success_and_text 'oc describe routes foo' '100 \(33%\)'
73
+os::cmd::expect_success_and_text 'oc set route-backends foo --zero' 'updated'
74
+os::cmd::expect_success_and_text 'oc get routes foo' 'a1\(0%\),b2\(0%\),c3\(0%\)'
75
+os::cmd::expect_success_and_text 'oc describe routes foo' '0'
76
+
77
+os::test::junit::declare_suite_end