Browse code

Fix merge conflicts

Troy Dawson authored on 2016/03/29 01:15:35
Showing 145 changed files
... ...
@@ -18955,7 +18955,8 @@
18955 18955
      "allowHostNetwork",
18956 18956
      "allowHostPorts",
18957 18957
      "allowHostPID",
18958
-     "allowHostIPC"
18958
+     "allowHostIPC",
18959
+     "readOnlyRootFilesystem"
18959 18960
     ],
18960 18961
     "properties": {
18961 18962
      "kind": {
... ...
@@ -19035,6 +19036,10 @@
19035 19035
       "$ref": "v1.FSGroupStrategyOptions",
19036 19036
       "description": "FSGroup is the strategy that will dictate what fs group is used by the SecurityContext."
19037 19037
      },
19038
+     "readOnlyRootFilesystem": {
19039
+      "type": "boolean",
19040
+      "description": "ReadOnlyRootFilesystem when set to true will force containers to run with a read only root file system.  If the container specifically requests to run with a non-read only root file system the SCC should deny the pod. If set to false the container may run with a read only root file system if it wishes but it will not be forced to."
19041
+     },
19038 19042
      "users": {
19039 19043
       "type": "array",
19040 19044
       "items": {
... ...
@@ -19377,4 +19382,4 @@
19377 19377
     }
19378 19378
    }
19379 19379
   }
19380
- }
19381 19380
\ No newline at end of file
19381
+ }
... ...
@@ -2772,6 +2772,7 @@ func DeepCopy_api_SecurityContextConstraints(in SecurityContextConstraints, out
2772 2772
 	if err := DeepCopy_api_FSGroupStrategyOptions(in.FSGroup, &out.FSGroup, c); err != nil {
2773 2773
 		return err
2774 2774
 	}
2775
+	out.ReadOnlyRootFilesystem = in.ReadOnlyRootFilesystem
2775 2776
 	if in.Users != nil {
2776 2777
 		in, out := in.Users, &out.Users
2777 2778
 		*out = make([]string, len(in))
... ...
@@ -2585,6 +2585,12 @@ type SecurityContextConstraints struct {
2585 2585
 	SupplementalGroups SupplementalGroupsStrategyOptions
2586 2586
 	// FSGroup is the strategy that will dictate what fs group is used by the SecurityContext.
2587 2587
 	FSGroup FSGroupStrategyOptions
2588
+	// ReadOnlyRootFilesystem when set to true will force containers to run with a read only root file
2589
+	// system.  If the container specifically requests to run with a non-read only root file system
2590
+	// the SCC should deny the pod.
2591
+	// If set to false the container may run with a read only root file system if it wishes but it
2592
+	// will not be forced to.
2593
+	ReadOnlyRootFilesystem bool
2588 2594
 
2589 2595
 	// The users who have permissions to use this security context constraints
2590 2596
 	Users []string
... ...
@@ -3039,6 +3039,7 @@ func autoConvert_api_SecurityContextConstraints_To_v1_SecurityContextConstraints
3039 3039
 	if err := Convert_api_FSGroupStrategyOptions_To_v1_FSGroupStrategyOptions(&in.FSGroup, &out.FSGroup, s); err != nil {
3040 3040
 		return err
3041 3041
 	}
3042
+	out.ReadOnlyRootFilesystem = in.ReadOnlyRootFilesystem
3042 3043
 	if in.Users != nil {
3043 3044
 		out.Users = make([]string, len(in.Users))
3044 3045
 		for i := range in.Users {
... ...
@@ -6440,6 +6441,7 @@ func autoConvert_v1_SecurityContextConstraints_To_api_SecurityContextConstraints
6440 6440
 	if err := Convert_v1_FSGroupStrategyOptions_To_api_FSGroupStrategyOptions(&in.FSGroup, &out.FSGroup, s); err != nil {
6441 6441
 		return err
6442 6442
 	}
6443
+	out.ReadOnlyRootFilesystem = in.ReadOnlyRootFilesystem
6443 6444
 	if in.Users != nil {
6444 6445
 		out.Users = make([]string, len(in.Users))
6445 6446
 		for i := range in.Users {
... ...
@@ -2395,6 +2395,7 @@ func deepCopy_v1_SecurityContextConstraints(in SecurityContextConstraints, out *
2395 2395
 	if err := deepCopy_v1_FSGroupStrategyOptions(in.FSGroup, &out.FSGroup, c); err != nil {
2396 2396
 		return err
2397 2397
 	}
2398
+	out.ReadOnlyRootFilesystem = in.ReadOnlyRootFilesystem
2398 2399
 	if in.Users != nil {
2399 2400
 		out.Users = make([]string, len(in.Users))
2400 2401
 		for i := range in.Users {
... ...
@@ -3034,6 +3034,12 @@ type SecurityContextConstraints struct {
3034 3034
 	SupplementalGroups SupplementalGroupsStrategyOptions `json:"supplementalGroups,omitempty" description:"strategy used to generate supplemental groups"`
3035 3035
 	// FSGroup is the strategy that will dictate what fs group is used by the SecurityContext.
3036 3036
 	FSGroup FSGroupStrategyOptions `json:"fsGroup,omitempty" description:"strategy used to generate fsGroup"`
3037
+	// ReadOnlyRootFilesystem when set to true will force containers to run with a read only root file
3038
+	// system.  If the container specifically requests to run with a non-read only root file system
3039
+	// the SCC should deny the pod.
3040
+	// If set to false the container may run with a read only root file system if it wishes but it
3041
+	// will not be forced to.
3042
+	ReadOnlyRootFilesystem bool `json:"readOnlyRootFilesystem" description:"require containers to run with a read only root filesystem"`
3037 3043
 
3038 3044
 	// The users who have permissions to use this security context constraints
3039 3045
 	Users []string `json:"users,omitempty" description:"users allowed to use this SecurityContextConstraints"`
... ...
@@ -1524,6 +1524,7 @@ var map_SecurityContextConstraints = map[string]string{
1524 1524
 	"runAsUser":                "RunAsUser is the strategy that will dictate what RunAsUser is used in the SecurityContext.",
1525 1525
 	"supplementalGroups":       "SupplementalGroups is the strategy that will dictate what supplemental groups are used by the SecurityContext.",
1526 1526
 	"fsGroup":                  "FSGroup is the strategy that will dictate what fs group is used by the SecurityContext.",
1527
+	"readOnlyRootFilesystem":   "ReadOnlyRootFilesystem when set to true will force containers to run with a read only root file system.  If the container specifically requests to run with a non-read only root file system the SCC should deny the pod. If set to false the container may run with a read only root file system if it wishes but it will not be forced to.",
1527 1528
 	"users":                    "The users who have permissions to use this security context constraints",
1528 1529
 	"groups":                   "The groups that have permission to use this security context constraints",
1529 1530
 }
... ...
@@ -2086,6 +2086,7 @@ func convert_api_SecurityContextConstraints_To_v1beta3_SecurityContextConstraint
2086 2086
 	} else {
2087 2087
 		out.RequiredDropCapabilities = nil
2088 2088
 	}
2089
+	out.ReadOnlyRootFilesystem = in.ReadOnlyRootFilesystem
2089 2090
 	if in.Users != nil {
2090 2091
 		out.Users = make([]string, len(in.Users))
2091 2092
 		for i := range in.Users {
... ...
@@ -4350,6 +4351,7 @@ func convert_v1beta3_SecurityContextConstraints_To_api_SecurityContextConstraint
4350 4350
 	} else {
4351 4351
 		out.RequiredDropCapabilities = nil
4352 4352
 	}
4353
+	out.ReadOnlyRootFilesystem = in.ReadOnlyRootFilesystem
4353 4354
 	if in.Users != nil {
4354 4355
 		out.Users = make([]string, len(in.Users))
4355 4356
 		for i := range in.Users {
... ...
@@ -2085,6 +2085,7 @@ func deepCopy_v1beta3_SecurityContextConstraints(in SecurityContextConstraints,
2085 2085
 	} else {
2086 2086
 		out.RequiredDropCapabilities = nil
2087 2087
 	}
2088
+	out.ReadOnlyRootFilesystem = in.ReadOnlyRootFilesystem
2088 2089
 	if in.Users != nil {
2089 2090
 		out.Users = make([]string, len(in.Users))
2090 2091
 		for i := range in.Users {
... ...
@@ -2121,6 +2121,12 @@ type SecurityContextConstraints struct {
2121 2121
 	SupplementalGroups SupplementalGroupsStrategyOptions `json:"supplementalGroups,omitempty" description:"strategy used to generate supplemental groups"`
2122 2122
 	// FSGroup is the strategy that will dictate what fs group is used by the SecurityContext.
2123 2123
 	FSGroup FSGroupStrategyOptions `json:"fsGroup,omitempty" description:"strategy used to generate fsGroup"`
2124
+	// ReadOnlyRootFilesystem when set to true will force containers to run with a read only root file
2125
+	// system.  If the container specifically requests to run with a non-read only root file system
2126
+	// the SCC should deny the pod.
2127
+	// If set to false the container may run with a read only root file system if it wishes but it
2128
+	// will not be forced to.
2129
+	ReadOnlyRootFilesystem bool `json:"readOnlyRootFilesystem" description:"require containers to run with a read only root filesystem"`
2124 2130
 
2125 2131
 	// The users who have permissions to use this security context constraints
2126 2132
 	Users []string `json:"users,omitempty" description:"users allowed to use this SecurityContextConstraints"`
... ...
@@ -335,13 +335,13 @@ type versionToResourceToFieldMapping map[unversioned.GroupVersion]resourceTypeTo
335 335
 func (v versionToResourceToFieldMapping) filterField(groupVersion *unversioned.GroupVersion, resourceType, field, value string) (newField, newValue string, err error) {
336 336
 	rMapping, ok := v[*groupVersion]
337 337
 	if !ok {
338
-		glog.Warningf("Field selector: %v - %v - %v - %v: need to check if this is versioned correctly.", groupVersion, resourceType, field, value)
338
+		// glog.Warningf("Field selector: %v - %v - %v - %v: need to check if this is versioned correctly.", groupVersion, resourceType, field, value)
339 339
 		return field, value, nil
340 340
 	}
341 341
 	newField, newValue, err = rMapping.filterField(resourceType, field, value)
342 342
 	if err != nil {
343 343
 		// This is only a warning until we find and fix all of the client's usages.
344
-		glog.Warningf("Field selector: %v - %v - %v - %v: need to check if this is versioned correctly.", groupVersion, resourceType, field, value)
344
+		// glog.Warningf("Field selector: %v - %v - %v - %v: need to check if this is versioned correctly.", groupVersion, resourceType, field, value)
345 345
 		return field, value, nil
346 346
 	}
347 347
 	return newField, newValue, nil
... ...
@@ -28,8 +28,8 @@ import (
28 28
 	"github.com/golang/glog"
29 29
 	"github.com/spf13/cobra"
30 30
 	"k8s.io/kubernetes/pkg/api"
31
-	"k8s.io/kubernetes/pkg/client/restclient"
32 31
 	apierrors "k8s.io/kubernetes/pkg/api/errors"
32
+	"k8s.io/kubernetes/pkg/client/restclient"
33 33
 	client "k8s.io/kubernetes/pkg/client/unversioned"
34 34
 	"k8s.io/kubernetes/pkg/client/unversioned/remotecommand"
35 35
 	cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
... ...
@@ -98,7 +98,6 @@ func RunHistory(f *cmdutil.Factory, cmd *cobra.Command, out io.Writer, args []st
98 98
 			continue
99 99
 		}
100 100
 
101
-		formattedOutput := ""
102 101
 		if revisionDetail > 0 {
103 102
 			// Print details of a specific revision
104 103
 			template, ok := historyInfo.RevisionToTemplate[revisionDetail]
... ...
@@ -106,16 +105,16 @@ func RunHistory(f *cmdutil.Factory, cmd *cobra.Command, out io.Writer, args []st
106 106
 				return fmt.Errorf("unable to find revision %d of %s %q", revisionDetail, mapping.Resource, info.Name)
107 107
 			}
108 108
 			fmt.Fprintf(out, "%s %q revision %d\n", mapping.Resource, info.Name, revisionDetail)
109
-			formattedOutput, err = kubectl.DescribePodTemplate(template)
109
+			kubectl.DescribePodTemplate(template, out)
110 110
 		} else {
111 111
 			// Print all revisions
112
-			formattedOutput, err = kubectl.PrintRolloutHistory(historyInfo, mapping.Resource, info.Name)
113
-		}
114
-		if err != nil {
115
-			errs = append(errs, err)
116
-			continue
112
+			formattedOutput, printErr := kubectl.PrintRolloutHistory(historyInfo, mapping.Resource, info.Name)
113
+			if printErr != nil {
114
+				errs = append(errs, printErr)
115
+				continue
116
+			}
117
+			fmt.Fprintf(out, "%s\n", formattedOutput)
117 118
 		}
118
-		fmt.Fprintf(out, "%s\n", formattedOutput)
119 119
 	}
120 120
 
121 121
 	return errors.NewAggregate(errs)
... ...
@@ -494,6 +494,7 @@ func describeSecurityContextConstraints(scc *api.SecurityContextConstraints) (st
494 494
 		fmt.Fprintf(out, "  Allow Host Ports:\t%t\n", scc.AllowHostPorts)
495 495
 		fmt.Fprintf(out, "  Allow Host PID:\t%t\n", scc.AllowHostPID)
496 496
 		fmt.Fprintf(out, "  Allow Host IPC:\t%t\n", scc.AllowHostIPC)
497
+		fmt.Fprintf(out, "  Read Only Root Filesystem:\t%t\n", scc.ReadOnlyRootFilesystem)
497 498
 
498 499
 		fmt.Fprintf(out, "  Run As User Strategy: %s\t\n", string(scc.RunAsUser.Type))
499 500
 		uid := ""
... ...
@@ -626,7 +627,7 @@ func describePod(pod *api.Pod, events *api.EventList) (string, error) {
626 626
 		fmt.Fprintf(out, "IP:\t%s\n", pod.Status.PodIP)
627 627
 		fmt.Fprintf(out, "Controllers:\t%s\n", printControllers(pod.Annotations))
628 628
 		fmt.Fprintf(out, "Containers:\n")
629
-		DescribeContainers(pod.Spec.Containers, pod.Status.ContainerStatuses, EnvValueRetriever(pod), out)
629
+		describeContainers(pod.Spec.Containers, pod.Status.ContainerStatuses, EnvValueRetriever(pod), out)
630 630
 		if len(pod.Status.Conditions) > 0 {
631 631
 			fmt.Fprint(out, "Conditions:\n  Type\tStatus\n")
632 632
 			for _, c := range pod.Status.Conditions {
... ...
@@ -635,7 +636,7 @@ func describePod(pod *api.Pod, events *api.EventList) (string, error) {
635 635
 					c.Status)
636 636
 			}
637 637
 		}
638
-		describeVolumes(pod.Spec.Volumes, out)
638
+		describeVolumes(pod.Spec.Volumes, out, "")
639 639
 		if events != nil {
640 640
 			DescribeEvents(events, out)
641 641
 		}
... ...
@@ -655,12 +656,12 @@ func printControllers(annotation map[string]string) string {
655 655
 	return "<none>"
656 656
 }
657 657
 
658
-func describeVolumes(volumes []api.Volume, out io.Writer) {
658
+func describeVolumes(volumes []api.Volume, out io.Writer, space string) {
659 659
 	if volumes == nil || len(volumes) == 0 {
660
-		fmt.Fprint(out, "No volumes.\n")
660
+		fmt.Fprintf(out, "%sNo volumes.\n", space)
661 661
 		return
662 662
 	}
663
-	fmt.Fprint(out, "Volumes:\n")
663
+	fmt.Fprintf(out, "%sVolumes:\n", space)
664 664
 	for _, volume := range volumes {
665 665
 		fmt.Fprintf(out, "  %v:\n", volume.Name)
666 666
 		switch {
... ...
@@ -879,8 +880,8 @@ func (d *PersistentVolumeClaimDescriber) Describe(namespace, name string) (strin
879 879
 	})
880 880
 }
881 881
 
882
-// DescribeContainers is exported for consumers in other API groups that have container templates
883
-func DescribeContainers(containers []api.Container, containerStatuses []api.ContainerStatus, resolverFn EnvVarResolverFunc, out io.Writer) {
882
+// describeContainers is exported for consumers in other API groups that have container templates
883
+func describeContainers(containers []api.Container, containerStatuses []api.ContainerStatus, resolverFn EnvVarResolverFunc, out io.Writer) {
884 884
 	statuses := map[string]api.ContainerStatus{}
885 885
 	for _, status := range containerStatuses {
886 886
 		statuses[status.Name] = status
... ...
@@ -1096,7 +1097,7 @@ func describeReplicationController(controller *api.ReplicationController, events
1096 1096
 		fmt.Fprintf(out, "Replicas:\t%d current / %d desired\n", controller.Status.Replicas, controller.Spec.Replicas)
1097 1097
 		fmt.Fprintf(out, "Pods Status:\t%d Running / %d Waiting / %d Succeeded / %d Failed\n", running, waiting, succeeded, failed)
1098 1098
 		if controller.Spec.Template != nil {
1099
-			describeVolumes(controller.Spec.Template.Spec.Volumes, out)
1099
+			describeVolumes(controller.Spec.Template.Spec.Volumes, out, "")
1100 1100
 		}
1101 1101
 		if events != nil {
1102 1102
 			DescribeEvents(events, out)
... ...
@@ -1105,18 +1106,21 @@ func describeReplicationController(controller *api.ReplicationController, events
1105 1105
 	})
1106 1106
 }
1107 1107
 
1108
-func DescribePodTemplate(template *api.PodTemplateSpec) (string, error) {
1109
-	return tabbedString(func(out io.Writer) error {
1110
-		if template == nil {
1111
-			fmt.Fprintf(out, "<unset>")
1112
-			return nil
1113
-		}
1114
-		fmt.Fprintf(out, "Labels:\t%s\n", labels.FormatLabels(template.Labels))
1115
-		fmt.Fprintf(out, "Annotations:\t%s\n", labels.FormatLabels(template.Annotations))
1116
-		fmt.Fprintf(out, "Image(s):\t%s\n", makeImageList(&template.Spec))
1117
-		describeVolumes(template.Spec.Volumes, out)
1118
-		return nil
1119
-	})
1108
+func DescribePodTemplate(template *api.PodTemplateSpec, out io.Writer) {
1109
+	if template == nil {
1110
+		fmt.Fprintf(out, "  <unset>")
1111
+		return
1112
+	}
1113
+	fmt.Fprintf(out, "  Labels:\t%s\n", labels.FormatLabels(template.Labels))
1114
+	if len(template.Annotations) > 0 {
1115
+		fmt.Fprintf(out, "  Annotations:\t%s\n", labels.FormatLabels(template.Annotations))
1116
+	}
1117
+	if len(template.Spec.ServiceAccountName) > 0 {
1118
+		fmt.Fprintf(out, "  Service Account:\t%s\n", template.Spec.ServiceAccountName)
1119
+	}
1120
+	fmt.Fprintf(out, "  Containers:\n")
1121
+	describeContainers(template.Spec.Containers, nil, nil, out)
1122
+	describeVolumes(template.Spec.Volumes, out, "  ")
1120 1123
 }
1121 1124
 
1122 1125
 // ReplicaSetDescriber generates information about a ReplicaSet and the pods it has created.
... ...
@@ -1157,7 +1161,7 @@ func describeReplicaSet(rs *extensions.ReplicaSet, events *api.EventList, runnin
1157 1157
 		fmt.Fprintf(out, "Labels:\t%s\n", labels.FormatLabels(rs.Labels))
1158 1158
 		fmt.Fprintf(out, "Replicas:\t%d current / %d desired\n", rs.Status.Replicas, rs.Spec.Replicas)
1159 1159
 		fmt.Fprintf(out, "Pods Status:\t%d Running / %d Waiting / %d Succeeded / %d Failed\n", running, waiting, succeeded, failed)
1160
-		describeVolumes(rs.Spec.Template.Spec.Volumes, out)
1160
+		describeVolumes(rs.Spec.Template.Spec.Volumes, out, "")
1161 1161
 		if events != nil {
1162 1162
 			DescribeEvents(events, out)
1163 1163
 		}
... ...
@@ -1202,7 +1206,7 @@ func describeJob(job *extensions.Job, events *api.EventList) (string, error) {
1202 1202
 		}
1203 1203
 		fmt.Fprintf(out, "Labels:\t%s\n", labels.FormatLabels(job.Labels))
1204 1204
 		fmt.Fprintf(out, "Pods Statuses:\t%d Running / %d Succeeded / %d Failed\n", job.Status.Active, job.Status.Succeeded, job.Status.Failed)
1205
-		describeVolumes(job.Spec.Template.Spec.Volumes, out)
1205
+		describeVolumes(job.Spec.Template.Spec.Volumes, out, "")
1206 1206
 		if events != nil {
1207 1207
 			DescribeEvents(events, out)
1208 1208
 		}
... ...
@@ -268,7 +268,7 @@ func TestDescribeContainers(t *testing.T) {
268 268
 				ContainerStatuses: []api.ContainerStatus{testCase.status},
269 269
 			},
270 270
 		}
271
-		DescribeContainers(pod.Spec.Containers, pod.Status.ContainerStatuses, EnvValueRetriever(&pod), out)
271
+		describeContainers(pod.Spec.Containers, pod.Status.ContainerStatuses, EnvValueRetriever(&pod), out)
272 272
 		output := out.String()
273 273
 		for _, expected := range testCase.expectedElements {
274 274
 			if !strings.Contains(output, expected) {
... ...
@@ -420,7 +420,7 @@ var withNamespacePrefixColumns = []string{"NAMESPACE"} // TODO(erictune): print
420 420
 var deploymentColumns = []string{"NAME", "DESIRED", "CURRENT", "UP-TO-DATE", "AVAILABLE", "AGE"}
421 421
 var configMapColumns = []string{"NAME", "DATA", "AGE"}
422 422
 var podSecurityPolicyColumns = []string{"NAME", "PRIV", "CAPS", "VOLUMEPLUGINS", "SELINUX", "RUNASUSER"}
423
-var securityContextConstraintsColumns = []string{"NAME", "PRIV", "CAPS", "HOSTDIR", "SELINUX", "RUNASUSER", "FSGROUP", "SUPGROUP", "PRIORITY"}
423
+var securityContextConstraintsColumns = []string{"NAME", "PRIV", "CAPS", "HOSTDIR", "SELINUX", "RUNASUSER", "FSGROUP", "SUPGROUP", "PRIORITY", "READONLYROOTFS"}
424 424
 
425 425
 // addDefaultHandlers adds print handlers for default Kubernetes types.
426 426
 func (h *HumanReadablePrinter) addDefaultHandlers() {
... ...
@@ -1950,9 +1950,9 @@ func printSecurityContextConstraints(item *api.SecurityContextConstraints, w io.
1950 1950
 		priority = strconv.Itoa(*item.Priority)
1951 1951
 	}
1952 1952
 
1953
-	_, err := fmt.Fprintf(w, "%s\t%t\t%v\t%t\t%s\t%s\t%s\t%s\t%s\n", item.Name, item.AllowPrivilegedContainer,
1953
+	_, err := fmt.Fprintf(w, "%s\t%t\t%v\t%t\t%s\t%s\t%s\t%s\t%s\t%t\n", item.Name, item.AllowPrivilegedContainer,
1954 1954
 		item.AllowedCapabilities, item.AllowHostDirVolumePlugin, item.SELinuxContext.Type,
1955
-		item.RunAsUser.Type, item.FSGroup.Type, item.SupplementalGroups.Type, priority)
1955
+		item.RunAsUser.Type, item.FSGroup.Type, item.SupplementalGroups.Type, priority, item.ReadOnlyRootFilesystem)
1956 1956
 	return err
1957 1957
 }
1958 1958
 
... ...
@@ -1104,11 +1104,6 @@ func (kl *Kubelet) initialNodeStatus() (*api.Node, error) {
1104 1104
 		}
1105 1105
 	} else {
1106 1106
 		node.Spec.ExternalID = kl.hostname
1107
-		// If no cloud provider is defined - use the one detected by cadvisor
1108
-		info, err := kl.GetCachedMachineInfo()
1109
-		if err == nil {
1110
-			kl.updateCloudProviderFromMachineInfo(node, info)
1111
-		}
1112 1107
 	}
1113 1108
 	if err := kl.setNodeStatus(node); err != nil {
1114 1109
 		return nil, err
... ...
@@ -2870,17 +2865,6 @@ func (kl *Kubelet) setNodeAddress(node *api.Node) error {
2870 2870
 	return nil
2871 2871
 }
2872 2872
 
2873
-func (kl *Kubelet) updateCloudProviderFromMachineInfo(
2874
-	node *api.Node,
2875
-	info *cadvisorapi.MachineInfo) {
2876
-
2877
-	if info.CloudProvider != cadvisorapi.UnknownProvider &&
2878
-		info.CloudProvider != cadvisorapi.Baremetal {
2879
-		node.Spec.ProviderID = strings.ToLower(string(info.CloudProvider)) +
2880
-			":////" + string(info.InstanceID)
2881
-	}
2882
-}
2883
-
2884 2873
 func (kl *Kubelet) setNodeStatusMachineInfo(node *api.Node) {
2885 2874
 	// TODO: Post NotReady if we cannot get MachineInfo from cAdvisor. This needs to start
2886 2875
 	// cAdvisor locally, e.g. for test-cmd.sh, and in integration test.
... ...
@@ -159,6 +159,11 @@ func DetermineEffectiveSecurityContext(pod *api.Pod, container *api.Container) *
159 159
 		*effectiveSc.RunAsNonRoot = *containerSc.RunAsNonRoot
160 160
 	}
161 161
 
162
+	if containerSc.ReadOnlyRootFilesystem != nil {
163
+		effectiveSc.ReadOnlyRootFilesystem = new(bool)
164
+		*effectiveSc.ReadOnlyRootFilesystem = *containerSc.ReadOnlyRootFilesystem
165
+	}
166
+
162 167
 	return effectiveSc
163 168
 }
164 169
 
... ...
@@ -181,6 +181,13 @@ func (s *simpleProvider) CreateContainerSecurityContext(pod *api.Pod, container
181 181
 	}
182 182
 	sc.Capabilities = caps
183 183
 
184
+	// if the SCC requires a read only root filesystem and the container has not made a specific
185
+	// request then default ReadOnlyRootFilesystem to true.
186
+	if s.scc.ReadOnlyRootFilesystem && sc.ReadOnlyRootFilesystem == nil {
187
+		readOnlyRootFS := true
188
+		sc.ReadOnlyRootFilesystem = &readOnlyRootFS
189
+	}
190
+
184 191
 	return sc, nil
185 192
 }
186 193
 
... ...
@@ -271,6 +278,14 @@ func (s *simpleProvider) ValidateContainerSecurityContext(pod *api.Pod, containe
271 271
 		allErrs = append(allErrs, field.Invalid(fldPath.Child("hostIPC"), pod.Spec.SecurityContext.HostIPC, "Host IPC is not allowed to be used"))
272 272
 	}
273 273
 
274
+	if s.scc.ReadOnlyRootFilesystem {
275
+		if sc.ReadOnlyRootFilesystem == nil {
276
+			allErrs = append(allErrs, field.Invalid(fldPath.Child("readOnlyRootFilesystem"), sc.ReadOnlyRootFilesystem, "ReadOnlyRootFilesystem may not be nil and must be set to true"))
277
+		} else if !*sc.ReadOnlyRootFilesystem {
278
+			allErrs = append(allErrs, field.Invalid(fldPath.Child("readOnlyRootFilesystem"), *sc.ReadOnlyRootFilesystem, "ReadOnlyRootFilesystem must be set to true"))
279
+		}
280
+	}
281
+
274 282
 	return allErrs
275 283
 }
276 284
 
... ...
@@ -129,6 +129,8 @@ func TestCreateContainerSecurityContextNonmutating(t *testing.T) {
129 129
 			SupplementalGroups: api.SupplementalGroupsStrategyOptions{
130 130
 				Type: api.SupplementalGroupsStrategyRunAsAny,
131 131
 			},
132
+			// mutates the container SC by defaulting to true if container sets nil
133
+			ReadOnlyRootFilesystem: true,
132 134
 		}
133 135
 	}
134 136
 
... ...
@@ -141,7 +143,7 @@ func TestCreateContainerSecurityContextNonmutating(t *testing.T) {
141 141
 	}
142 142
 	sc, err := provider.CreateContainerSecurityContext(pod, &pod.Spec.Containers[0])
143 143
 	if err != nil {
144
-		t.Fatal("unable to create provider %v", err)
144
+		t.Fatal("unable to create container security context %v", err)
145 145
 	}
146 146
 
147 147
 	// The generated security context should have filled in missing options, so they should differ
... ...
@@ -316,6 +318,13 @@ func TestValidateContainerSecurityContextFailures(t *testing.T) {
316 316
 	failHostPortPod := defaultPod()
317 317
 	failHostPortPod.Spec.Containers[0].Ports = []api.ContainerPort{{HostPort: 1}}
318 318
 
319
+	readOnlyRootFSSCC := defaultSCC()
320
+	readOnlyRootFSSCC.ReadOnlyRootFilesystem = true
321
+
322
+	readOnlyRootFSPodFalse := defaultPod()
323
+	readOnlyRootFS := false
324
+	readOnlyRootFSPodFalse.Spec.Containers[0].SecurityContext.ReadOnlyRootFilesystem = &readOnlyRootFS
325
+
319 326
 	errorCases := map[string]struct {
320 327
 		pod           *api.Pod
321 328
 		scc           *api.SecurityContextConstraints
... ...
@@ -351,6 +360,16 @@ func TestValidateContainerSecurityContextFailures(t *testing.T) {
351 351
 			scc:           defaultSCC(),
352 352
 			expectedError: "Host ports are not allowed to be used",
353 353
 		},
354
+		"failReadOnlyRootFS - nil": {
355
+			pod:           defaultPod(),
356
+			scc:           readOnlyRootFSSCC,
357
+			expectedError: "ReadOnlyRootFilesystem may not be nil and must be set to true",
358
+		},
359
+		"failReadOnlyRootFS - false": {
360
+			pod:           readOnlyRootFSPodFalse,
361
+			scc:           readOnlyRootFSSCC,
362
+			expectedError: "ReadOnlyRootFilesystem must be set to true",
363
+		},
354 364
 	}
355 365
 
356 366
 	for k, v := range errorCases {
... ...
@@ -545,6 +564,14 @@ func TestValidateContainerSecurityContextSuccess(t *testing.T) {
545 545
 	hostPortPod := defaultPod()
546 546
 	hostPortPod.Spec.Containers[0].Ports = []api.ContainerPort{{HostPort: 1}}
547 547
 
548
+	readOnlyRootFSPodFalse := defaultPod()
549
+	readOnlyRootFSFalse := false
550
+	readOnlyRootFSPodFalse.Spec.Containers[0].SecurityContext.ReadOnlyRootFilesystem = &readOnlyRootFSFalse
551
+
552
+	readOnlyRootFSPodTrue := defaultPod()
553
+	readOnlyRootFSTrue := true
554
+	readOnlyRootFSPodTrue.Spec.Containers[0].SecurityContext.ReadOnlyRootFilesystem = &readOnlyRootFSTrue
555
+
548 556
 	errorCases := map[string]struct {
549 557
 		pod *api.Pod
550 558
 		scc *api.SecurityContextConstraints
... ...
@@ -577,6 +604,18 @@ func TestValidateContainerSecurityContextSuccess(t *testing.T) {
577 577
 			pod: hostPortPod,
578 578
 			scc: hostPortSCC,
579 579
 		},
580
+		"pass read only root fs - nil": {
581
+			pod: defaultPod(),
582
+			scc: defaultSCC(),
583
+		},
584
+		"pass read only root fs - false": {
585
+			pod: readOnlyRootFSPodFalse,
586
+			scc: defaultSCC(),
587
+		},
588
+		"pass read only root fs - true": {
589
+			pod: readOnlyRootFSPodTrue,
590
+			scc: defaultSCC(),
591
+		},
580 592
 	}
581 593
 
582 594
 	for k, v := range errorCases {
... ...
@@ -592,6 +631,85 @@ func TestValidateContainerSecurityContextSuccess(t *testing.T) {
592 592
 	}
593 593
 }
594 594
 
595
+func TestGenerateContainerSecurityContextReadOnlyRootFS(t *testing.T) {
596
+	trueSCC := defaultSCC()
597
+	trueSCC.ReadOnlyRootFilesystem = true
598
+
599
+	trueVal := true
600
+	expectTrue := &trueVal
601
+	falseVal := false
602
+	expectFalse := &falseVal
603
+
604
+	falsePod := defaultPod()
605
+	falsePod.Spec.Containers[0].SecurityContext.ReadOnlyRootFilesystem = expectFalse
606
+
607
+	truePod := defaultPod()
608
+	truePod.Spec.Containers[0].SecurityContext.ReadOnlyRootFilesystem = expectTrue
609
+
610
+	tests := map[string]struct {
611
+		pod      *api.Pod
612
+		scc      *api.SecurityContextConstraints
613
+		expected *bool
614
+	}{
615
+		"false scc, nil sc": {
616
+			scc:      defaultSCC(),
617
+			pod:      defaultPod(),
618
+			expected: nil,
619
+		},
620
+		"false scc, false sc": {
621
+			scc:      defaultSCC(),
622
+			pod:      falsePod,
623
+			expected: expectFalse,
624
+		},
625
+		"false scc, true sc": {
626
+			scc:      defaultSCC(),
627
+			pod:      truePod,
628
+			expected: expectTrue,
629
+		},
630
+		"true scc, nil sc": {
631
+			scc:      trueSCC,
632
+			pod:      defaultPod(),
633
+			expected: expectTrue,
634
+		},
635
+		"true scc, false sc": {
636
+			scc: trueSCC,
637
+			pod: falsePod,
638
+			// expect false even though it defaults to true to ensure it doesn't change set values
639
+			// validation catches the mismatch, not generation
640
+			expected: expectFalse,
641
+		},
642
+		"true scc, true sc": {
643
+			scc:      trueSCC,
644
+			pod:      truePod,
645
+			expected: expectTrue,
646
+		},
647
+	}
648
+
649
+	for k, v := range tests {
650
+		provider, err := NewSimpleProvider(v.scc)
651
+		if err != nil {
652
+			t.Errorf("%s unable to create provider %v", k, err)
653
+			continue
654
+		}
655
+		sc, err := provider.CreateContainerSecurityContext(v.pod, &v.pod.Spec.Containers[0])
656
+		if err != nil {
657
+			t.Errorf("%s unable to create container security context %v", k, err)
658
+			continue
659
+		}
660
+
661
+		if v.expected == nil && sc.ReadOnlyRootFilesystem != nil {
662
+			t.Errorf("%s expected a nil ReadOnlyRootFilesystem but got %t", k, *sc.ReadOnlyRootFilesystem)
663
+		}
664
+		if v.expected != nil && sc.ReadOnlyRootFilesystem == nil {
665
+			t.Errorf("%s expected a non nil ReadOnlyRootFilesystem but recieved nil", k)
666
+		}
667
+		if v.expected != nil && sc.ReadOnlyRootFilesystem != nil && (*v.expected != *sc.ReadOnlyRootFilesystem) {
668
+			t.Errorf("%s expected a non nil ReadOnlyRootFilesystem set to %t but got %t", k, *v.expected, *sc.ReadOnlyRootFilesystem)
669
+		}
670
+
671
+	}
672
+}
673
+
595 674
 func defaultSCC() *api.SecurityContextConstraints {
596 675
 	return &api.SecurityContextConstraints{
597 676
 		ObjectMeta: api.ObjectMeta{
... ...
@@ -30,11 +30,9 @@ import (
30 30
 // This test requires that --terminated-pod-gc-threshold=100 be set on the controller manager
31 31
 //
32 32
 // Slow by design (7 min)
33
-var _ = Describe("Garbage collector [Slow]", func() {
33
+var _ = Describe("Garbage collector [Feature:GarbageCollector] [Slow]", func() {
34 34
 	f := NewDefaultFramework("garbage-collector")
35 35
 	It("should handle the creation of 1000 pods", func() {
36
-		SkipUnlessProviderIs("gce")
37
-
38 36
 		var count int
39 37
 		for count < 1000 {
40 38
 			pod, err := createTerminatingPod(f)
... ...
@@ -18959,7 +18959,8 @@
18959 18959
      "allowHostNetwork",
18960 18960
      "allowHostPorts",
18961 18961
      "allowHostPID",
18962
-     "allowHostIPC"
18962
+     "allowHostIPC",
18963
+     "readOnlyRootFilesystem"
18963 18964
     ],
18964 18965
     "properties": {
18965 18966
      "kind": {
... ...
@@ -19039,6 +19040,10 @@
19039 19039
       "$ref": "v1.FSGroupStrategyOptions",
19040 19040
       "description": "FSGroup is the strategy that will dictate what fs group is used by the SecurityContext."
19041 19041
      },
19042
+     "readOnlyRootFilesystem": {
19043
+      "type": "boolean",
19044
+      "description": "ReadOnlyRootFilesystem when set to true will force containers to run with a read only root file system.  If the container specifically requests to run with a non-read only root file system the SCC should deny the pod. If set to false the container may run with a read only root file system if it wishes but it will not be forced to."
19045
+     },
19042 19046
      "users": {
19043 19047
       "type": "array",
19044 19048
       "items": {
... ...
@@ -21,6 +21,7 @@
21 21
   "unused": true,
22 22
   "globals": {
23 23
     "angular": false,
24
+    "ansi_up": false,
24 25
     "c3": false,
25 26
     "d3": false,
26 27
     "hawtioPluginLoader": false,
... ...
@@ -37,7 +37,23 @@
37 37
 
38 38
     <div ng-view></div>
39 39
 
40
-    <noscript class="attention-message"><h1>To use OpenShift, please enable JavaScript.</h1></noscript>
40
+    <noscript>
41
+      <nav class="navbar navbar-pf-alt" role="navigation">
42
+        <div row>
43
+          <div class="navbar-header">
44
+            <a class="navbar-brand" id="openshift-logo" href="./">
45
+              <div id="header-logo"></div>
46
+            </a>
47
+          </div>
48
+        </div>
49
+      </nav>
50
+      <div class="attention-message">
51
+        <h1>JavaScript Required</h1>
52
+        <p>The OpenShift web console requires JavaScript to provide a rich interactive experience. Please
53
+        enable JavaScript to continue. If you do not wish to enable JavaScript or are unable to do so,
54
+        you may use the command-line tools to manage your projects and applications instead.</p>
55
+      </div>
56
+    </noscript>
41 57
 
42 58
     <script src="config.js"></script>
43 59
 
... ...
@@ -100,6 +116,7 @@
100 100
     <script src="bower_components/yamljs/bin/yaml.js"></script>
101 101
     <script src="bower_components/clipboard/dist/clipboard.js"></script>
102 102
     <script src="bower_components/pathseg/pathseg.js"></script>
103
+    <script src="bower_components/ansi_up/ansi_up.js"></script>
103 104
     <!-- endbower -->
104 105
     <!-- endbuild -->
105 106
 
... ...
@@ -5,7 +5,8 @@ angular.module('openshiftConsole')
5 5
     return {
6 6
       restrict: 'E',
7 7
       scope: {
8
-        alerts: '='
8
+        alerts: '=',
9
+        hideCloseButton: '=?'
9 10
       },
10 11
       templateUrl: 'views/_alerts.html'
11 12
     };
... ...
@@ -27,7 +27,12 @@ angular.module('openshiftConsole')
27 27
         // this webkit bug with user-select: none;
28 28
         //   https://bugs.webkit.org/show_bug.cgi?id=80159
29 29
         line.firstChild.setAttribute('data-line-number', lineNumber);
30
-        line.lastChild.appendChild(document.createTextNode(text));
30
+
31
+        // Escape ANSI color codes
32
+        var escaped = ansi_up.escape_for_html(text);
33
+        var html = ansi_up.ansi_to_html(escaped);
34
+        var linkifiedHTML = ansi_up.linkify(html);
35
+        line.lastChild.innerHTML = linkifiedHTML;
31 36
 
32 37
         return line;
33 38
       };
... ...
@@ -55,8 +55,13 @@ angular.module('openshiftConsole')
55 55
           $scope.width = 175;
56 56
         }
57 57
 
58
-        // https://github.com/mbostock/d3/wiki/Formatting
59
-        var percentage = d3.format(".2p");
58
+        var percentage = function(value) {
59
+          if (!value) {
60
+            return "0%";
61
+          }
62
+
63
+          return (Number(value) * 100).toFixed(1) + "%";
64
+        };
60 65
 
61 66
         // Chart configuration, see http://c3js.org/reference.html
62 67
         $scope.chartID = _.uniqueId('quota-usage-chart-');
... ...
@@ -790,8 +790,10 @@ angular.module('openshiftConsole')
790 790
 
791 791
       var nameFormatMap = {
792 792
         'configmaps': 'Config Maps',
793
+        'cpu': 'CPU (Request)',
793 794
         'limits.cpu': 'CPU (Limit)',
794 795
         'limits.memory': 'Memory (Limit)',
796
+        'memory': 'Memory (Request)',
795 797
         'openshift.io/imagesize': 'Image Size',
796 798
         'openshift.io/imagestreamsize': 'Image Stream Size',
797 799
         'openshift.io/projectimagessize': 'Project Image Size',
... ...
@@ -44,7 +44,7 @@ angular.module('openshiftConsole')
44 44
                                   description = 'The project ' + context.projectName + ' does not exist or you are not authorized to view it.';
45 45
                                   type = 'access_denied';
46 46
                                 } else if (e.status === 404) {
47
-                                  description = 'The project " + context.projectName + " does not exist.';
47
+                                  description = 'The project ' + context.projectName + ' does not exist.';
48 48
                                   type = 'not_found';
49 49
                                 }
50 50
                                 $location
... ...
@@ -720,17 +720,21 @@ select:invalid {
720 720
 }
721 721
 
722 722
 .attention-message {
723
-  background-color: lighten(@brand-primary, 20%);
723
+  background-color: lighten(@brand-primary, 50%);
724 724
   border: 1px solid darken(@brand-primary, 10%);
725 725
   position: absolute;
726
-  top: 20%;
727 726
   left: 50%;
728
-  transform: translate(-50%, -50%);
727
+  margin-top: 100px;
728
+  transform: translateX(-50%);
729 729
   padding: 1em 1em 2em;
730 730
   min-width: 85%;
731 731
   h1,p {
732 732
     text-align: center;
733 733
   }
734
+  p {
735
+    max-width: 70em;
736
+    margin: auto;
737
+  }
734 738
 }
735 739
 
736 740
 .learn-more-block {
... ...
@@ -5,7 +5,7 @@
5 5
     'alert-success': alert.type === 'success',
6 6
     'alert-info': !alert.type || alert.type === 'info'
7 7
   }">
8
-    <button ng-click="alert.hidden = true" type="button" class="close">
8
+    <button ng-if="!hideCloseButton" ng-click="alert.hidden = true" type="button" class="close">
9 9
       <span class="pficon pficon-close" aria-hidden="true"></span>
10 10
       <span class="sr-only">Close</span>
11 11
     </button>
... ...
@@ -24,7 +24,8 @@
24 24
             </a>
25 25
           </div>
26 26
           <div ng-show="!task.hasErrors || expanded">
27
-            <alerts alerts="task.alerts"></alerts>
27
+            <!-- Don't show a close button for each alert since we have one above for all tasks. -->
28
+            <alerts alerts="task.alerts" hide-close-button="true"></alerts>
28 29
           </div>
29 30
       </div>
30 31
     </div>
... ...
@@ -1,5 +1,8 @@
1 1
 <default-header class="top-header"></default-header>
2 2
 <div class="wrap no-sidebar">
3
+  <div class="sidebar-left collapse navbar-collapse navbar-collapse-2">
4
+    <navbar-utility-mobile></navbar-utility-mobile>
5
+  </div>
3 6
   <div class="middle surface-shaded">
4 7
     <div class="container surface-shaded">
5 8
       <div>
... ...
@@ -36,7 +36,8 @@
36 36
     "yamljs": "0.1.5",
37 37
     "clipboard": "1.5.8",
38 38
     "pathseg": "1.0.2",
39
-    "fontawesome": "4.5.0"
39
+    "fontawesome": "4.5.0",
40
+    "ansi_up": "1.3.0"
40 41
   },
41 42
   "devDependencies": {
42 43
     "angular-mocks": "1.3.8",
... ...
@@ -8,7 +8,7 @@ After=openvswitch.service
8 8
 [Service]
9 9
 EnvironmentFile=/etc/sysconfig/origin-node
10 10
 ExecStartPre=-/usr/bin/docker rm -f origin-node
11
-ExecStart=/usr/bin/docker run --name origin-node --rm --privileged --net=host --pid=host --env-file=/etc/sysconfig/origin-node -v /:/rootfs:ro -v /etc/systemd/system:/host-etc/systemd/system -v /etc/localtime:/etc/localtime:ro -v /etc/machine-id:/etc/machine-id:ro -v /lib/modules:/lib/modules -v /run:/run -v /sys:/sys:ro -v /usr/bin/docker:/usr/bin/docker:ro -v /var/lib/docker:/var/lib/docker -v /etc/origin/node:/etc/origin/node -v /etc/origin/openvswitch:/etc/openvswitch -v /etc/origin/sdn:/etc/openshift-sdn -v /var/lib/origin:/var/lib/origin -v /var/log:/var/log -e HOST=/rootfs -e HOST_ETC=/host-etc openshift/node
11
+ExecStart=/usr/bin/docker run --name origin-node --rm --privileged --net=host --pid=host --env-file=/etc/sysconfig/origin-node -v /:/rootfs:ro -v /etc/systemd/system:/host-etc/systemd/system -v /etc/localtime:/etc/localtime:ro -v /etc/machine-id:/etc/machine-id:ro -v /lib/modules:/lib/modules -v /run:/run -v /sys:/sys:ro -v /usr/bin/docker:/usr/bin/docker:ro -v /var/lib/docker:/var/lib/docker -v /etc/origin/node:/etc/origin/node -v /etc/origin/openvswitch:/etc/openvswitch -v /etc/origin/sdn:/etc/openshift-sdn -v /var/lib/origin:/var/lib/origin -v /var/log:/var/log -v /dev:/dev -e HOST=/rootfs -e HOST_ETC=/host-etc openshift/node
12 12
 ExecStartPost=/usr/bin/sleep 10
13 13
 ExecStop=/usr/bin/docker stop origin-node
14 14
 Restart=always
15 15
new file mode 100644
... ...
@@ -0,0 +1,40 @@
0
+FROM openshift/origin
1
+MAINTAINER Aaron Weitekamp <aweiteka@redhat.com>
2
+
3
+ADD install.sh run.sh uninstall.sh /container/bin/
4
+ADD registry-console-template.yaml \
5
+    registry-login-template.html \
6
+    registry-newproject-template-shared.json \
7
+    registry-newproject-template-unshared.json \
8
+    /container/etc/origin/
9
+
10
+LABEL name="projectatomic/atomic-registry-quickstart" \
11
+      vendor="Project Atomic" \
12
+      url="https://projectatomic.io/registry" \
13
+      summary="Quickstart image for Atomic Registry" \
14
+      description="This is a quickstart image to install Atomic Registry on a single host. It is an open source enterprise registry solution based on the Origin project featuring single sign-on (SSO) user experience, a robust web interface and advanced role-based access control (RBAC)." \
15
+      INSTALL='docker run -it --rm \
16
+                --privileged --net=host \
17
+                -v /var/run:/var/run:rw \
18
+                -v /sys:/sys \
19
+                -v /etc/localtime:/etc/localtime:ro \
20
+                -v /var/lib/docker:/var/lib/docker:rw \
21
+                -v /var/lib/origin/:/var/lib/origin/ \
22
+                -v /etc/origin/:/etc/origin/ \
23
+                -v /:/host \
24
+                -e KUBECONFIG=/etc/origin/master/admin.kubeconfig \
25
+                --entrypoint /container/bin/install.sh \
26
+                $IMAGE' \
27
+      RUN='docker run -it --rm --privileged \
28
+                --net=host \
29
+                -v /:/host \
30
+                -v /var/lib/docker:/var/lib/docker:rw \
31
+                -v /etc/origin:/etc/origin \
32
+                -v /var/lib/registry:/var/lib/registry \
33
+                -e KUBECONFIG=/etc/origin/master/admin.kubeconfig \
34
+                --entrypoint /container/bin/run.sh \
35
+                $IMAGE' \
36
+      UNINSTALL='docker run -it --rm --privileged \
37
+                -v /:/host \
38
+                --entrypoint /container/bin/uninstall.sh \
39
+                $IMAGE'
0 40
new file mode 100644
... ...
@@ -0,0 +1,19 @@
0
+TEST_IMAGE=atomic-registry-quickstart
1
+INSTALLHOST=127.0.0.1
2
+
3
+all: build install test
4
+
5
+build:
6
+	docker build -t atomic-registry-quickstart .
7
+
8
+install:
9
+	sudo yum install -y atomic
10
+	sudo atomic install $(TEST_IMAGE) $(INSTALLHOST)
11
+	# FIXME: docker error: installer container is not removed using --rm
12
+	sudo atomic run $(TEST_IMAGE) $(INSTALLHOST)
13
+
14
+test:
15
+	bash test.sh
16
+
17
+clean:
18
+	sudo atomic uninstall --force $(TEST_IMAGE) ; sudo docker rm $(sudo docker ps -qa)
0 19
new file mode 100644
... ...
@@ -0,0 +1,57 @@
0
+# Getting Started With Atomic Registry
1
+
2
+http://projectatomic.io/registry
3
+
4
+**Requirements**
5
+
6
+- single host (laptop, vm, vagrant, etc.) with Docker
7
+- Open TCP ports 8443, 443, 5000
8
+- The hostname used during install will be the output of the `hostname` command. If that hostname does not resolve with DNS then pass the IP address to the install procedure.
9
+- (optional) atomic cli, available on Red Hat-based systems Fedora, Centos, Red Hat Enterprise Linux, including Atomic host
10
+
11
+## Install and Run
12
+
13
+The install procedure should be run locally.
14
+
15
+### With atomic CLI
16
+
17
+1. Install the system service files and pull images.
18
+
19
+        sudo atomic install projectatomic/atomic-registry-quickstart [hostname]
20
+1. Optional: edit configuration file `/etc/origin/master/master-config.yaml`.
21
+1. Run the application. This will enable and start the docker containers as system services.
22
+
23
+        sudo atomic run projectatomic/atomic-registry-quickstart [hostname]
24
+
25
+### With straight Docker
26
+
27
+Replace steps 1 and 3 above with the output of the inspect command.
28
+
29
+    sudo docker inspect -f '{{ .Config.Labels.INSTALL }}' projectatomic/atomic-registry-quickstart
30
+    sudo docker inspect -f '{{ .Config.Labels.RUN }}' openshift/atomic-registry-quickstart
31
+
32
+This will provide the docker run commands to install and run the registry installation.
33
+
34
+If you make changes to the API  configuration file `/etc/origin/master/master-config.yaml` restart the API service.
35
+
36
+    sudo docker restart origin
37
+
38
+## Try it out
39
+
40
+1. Explore the web UI on https://<hostname>
41
+1. Login with docker using the reference commands, build and push an image.
42
+
43
+## Uninstall
44
+
45
+    sudo atomic uninstall --force projectatomic/atomic-registry-quickstart
46
+
47
+# Optional Setup steps
48
+
49
+1. [Configure authentication](https://docs.openshift.org/latest/install_config/configuring_authentication.html). Restart the origin API server after making changes to the config file: `sudo docker restart origin` 
50
+1. [Configure persistent registry storage](https://docs.openshift.org/latest/install_config/install/docker_registry.html#advanced-overriding-the-registry-configuration)
51
+1. [Assign a user cluster-admin privilege](https://docs.openshift.org/latest/admin_guide/manage_authorization_policy.html#managing-role-bindings)
52
+1. Explore the web UI
53
+
54
+## Reference Documentation
55
+
56
+https://docs.openshift.org/latest/welcome/index.html
0 57
new file mode 100755
... ...
@@ -0,0 +1,40 @@
0
+#!/bin/bash
1
+
2
+IMAGES=(openshift/origin openshift/origin-docker-registry cockpit/kubernetes)
3
+
4
+for IMAGE in "${IMAGES[@]}"
5
+do
6
+  chroot /host docker pull $IMAGE
7
+done
8
+
9
+INSTALL_HOST=${1:-`hostname`}
10
+echo "Installing using hostname ${INSTALL_HOST}"
11
+
12
+# write out configuration
13
+openshift start --write-config /etc/origin/ --etcd-dir /var/lib/origin/etcd --volume-dir /var/lib/origin/volumes --public-master ${INSTALL_HOST}
14
+
15
+echo "Copy files to host"
16
+
17
+set -x
18
+mv /host/etc/origin/node-* /host/etc/origin/node
19
+
20
+mkdir -p /host/etc/origin/registry/bin
21
+mkdir -p /host/etc/origin/master/site
22
+cp /container/bin/* /host/etc/origin/registry/bin/.
23
+cp /container/etc/origin/registry-console-template.yaml /host/etc/origin/registry/.
24
+cp /container/etc/origin/registry-newproject-template-shared.json /host/etc/origin/registry/.
25
+cp /container/etc/origin/registry-newproject-template-unshared.json /host/etc/origin/registry/.
26
+cp /container/etc/origin/registry-login-template.html /host/etc/origin/master/site/.
27
+# Create registry UI service certificates -- TODO: use this cert
28
+cat /etc/origin/master/master.server.crt /etc/origin/master/master.server.key > /etc/origin/registry/master.server.cert
29
+
30
+set +x
31
+
32
+echo "Updating servicesNodePortRange to 443-32767..."
33
+sed -i 's/  servicesNodePortRange:.*$/  servicesNodePortRange: 443-32767/' /etc/origin/master/master-config.yaml
34
+echo "Updating login template"
35
+sed -i 's/  templates: null$/  templates:\n    login: site\/registry-login-template.html/' /etc/origin/master/master-config.yaml
36
+
37
+echo "Optionally edit configuration file /etc/origin/master/master-config.yaml,"
38
+echo "add certificates to /etc/origin/master,"
39
+echo "then run 'atomic run atomic-registry-quickstart'"
0 40
new file mode 100644
... ...
@@ -0,0 +1,127 @@
0
+kind: Template
1
+apiVersion: v1
2
+metadata:
3
+  name: "registry-console-template"
4
+labels:
5
+  createdBy: "registry-console-template"
6
+parameters:
7
+  -
8
+    description: "The public url for the Openshift OAuth Provider"
9
+    name: OPENSHIFT_OAUTH_PROVIDER_URL
10
+    required: true
11
+  -
12
+    description: "The public url for the Openshift OAuth Provider"
13
+    name: COCKPIT_KUBE_URL
14
+    required: true
15
+  -
16
+    description: "The public url for the Openshift OAuth Provider"
17
+    name: COCKPIT_KUBE_INSECURE
18
+    required: false
19
+  -
20
+    description: "Oauth client secret"
21
+    name: OPENSHIFT_OAUTH_CLIENT_SECRET
22
+    from: "user[a-zA-Z0-9]{64}"
23
+    generate: expression
24
+  -
25
+    description: "Oauth client id"
26
+    name: OPENSHIFT_OAUTH_CLIENT_ID
27
+    value: "cockpit-oauth-client"
28
+  -
29
+    description: "Skip kubernetes CA verification"
30
+    name: KUBERNETES_INSECURE
31
+    value: ""
32
+  -
33
+    description: "PEM Encoded certificate to use for CA verification"
34
+    name: KUBERNETES_CA_DATA
35
+    value: ""
36
+  -
37
+    description: "The hostname or IP address of the registry. Do not include http:// or port."
38
+    name: REGISTRY_HOST
39
+    required: true
40
+objects:
41
+  -
42
+    kind: DeploymentConfig
43
+    apiVersion: v1
44
+    metadata:
45
+      name: "registry-console"
46
+    labels:
47
+      name: "registry-console"
48
+    spec:
49
+      replicas: 1
50
+      selector:
51
+        name: "registry-console"
52
+      template:
53
+        metadata:
54
+          labels:
55
+            name: "registry-console"
56
+        spec:
57
+          containers:
58
+            -
59
+              name: "registry-console"
60
+              image: "cockpit/kubernetes"
61
+              ports:
62
+                -
63
+                  containerPort: 9090
64
+                  protocol: TCP
65
+              env:
66
+                -
67
+                  name: OPENSHIFT_OAUTH_PROVIDER_URL
68
+                  value: "${OPENSHIFT_OAUTH_PROVIDER_URL}"
69
+                -
70
+                  name: OPENSHIFT_OAUTH_CLIENT_ID
71
+                  value: "${OPENSHIFT_OAUTH_CLIENT_ID}"
72
+                -
73
+                  name: KUBERNETES_INSECURE
74
+                  value: "${KUBERNETES_INSECURE}"
75
+                -
76
+                  name: KUBERNETES_CA_DATA
77
+                  value: "${KUBERNETES_CA_DATA}"
78
+                -
79
+                  name: COCKPIT_KUBE_INSECURE
80
+                  value: "${COCKPIT_KUBE_INSECURE}"
81
+                -
82
+                  name: REGISTRY_ONLY
83
+                  value: "true"
84
+                -
85
+                  name: REGISTRY_HOST
86
+                  value: "${REGISTRY_HOST}"
87
+  -
88
+    kind: Service
89
+    apiVersion: v1
90
+    metadata:
91
+     name: "registry-console"
92
+     labels:
93
+       name: "registry-console"
94
+    spec:
95
+      type: ClusterIP
96
+      ports:
97
+        -
98
+          protocol: TCP
99
+          port: 9000
100
+          targetPort: 9090
101
+          selector:
102
+            name: "registry-console"
103
+  -
104
+    kind: ImageStream
105
+    apiVersion: v1
106
+    metadata:
107
+      name: registry-console
108
+      annotations:
109
+        description: Atomic Registry console
110
+    spec:
111
+      tags:
112
+        - annotations: null
113
+          from:
114
+            kind: DockerImage
115
+            name: cockpit/kubernetes
116
+          name: latest
117
+  -
118
+    kind: OAuthClient
119
+    apiVersion: v1
120
+    metadata:
121
+      name: "${OPENSHIFT_OAUTH_CLIENT_ID}"
122
+      respondWithChallenges: false
123
+    secret: "${OPENSHIFT_OAUTH_CLIENT_SECRET}"
124
+    redirectURIs:
125
+      -
126
+        "${COCKPIT_KUBE_URL}"
0 127
new file mode 100644
... ...
@@ -0,0 +1,2357 @@
0
+
1
+<!DOCTYPE html>
2
+
3
+
4
+
5
+<html class="login-pf">
6
+
7
+  <head>
8
+    <title>Login - Atomic Registry</title>
9
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
10
+    <link rel="shortcut icon" href="data:image/ico;base64,AAABAAIAEBAAAAEAIAAoBQAAJgAAACAgAAABACAAKBQAAE4FAAAoAAAAEAAAACAAAAABACAAAAAAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP///xP///+E////2v////D////w////7v///8H///9RAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP///1H////5//////////////////////v7+/+wsLL/5ubn/////8L///8TAAAAAAAAAAAAAAAAAAAAAP///2X/////////////////////////////////////Tk5Q9j4+Qfv/////////4f7+/hkAAAAAAAAAAP///y/////////////////////////////////Gxsn/e3yA//////0aGh35HR8g+5ycnv92dnm3AAAAAP///wv///+p/////+bm6P6mp6n68fHy/v/////////9x8TB/H14bP3////9hIJ+9wAAAP4AAAD/AAAA/wAAAEw4ODtWQUFE+oqKjf9ISUz6SUtP+P////+zq576BgAA+iMgGvtHS1f5NTpL+jAzP/oBAAD+AgAA/wAAAP8EBQWrAAAAmgAAAP8AAAD/AAAA++Lc0PltaF37ChAm/BAPWf8fHZT/JB+z/yQfuP8mIMH/Jy6+/x4mSP8CAAD/AAAA5QEBAboHBwv/AgAA/wUAAP8rMEj6Hh6L/SUg3f8jI+3/JSLs/ygg2/8pIdj/KCHb/yUi5/8jI+3/ICdK/wEAAO8BAgK5AgAA/wIAAP8jLXb/JSHj/yMj7f8lIeX/Ji98/yAnRv8lJcX/KCHa/yciyP8mINv/IyPt/yItdP8BAADvAAAAlAIAAP8mL4z/IyPt/yUi6v8lLV//AgAA/wcAAP8iJz//JSLr/ygh2v8nKbn/Jy2j/ycup/8KDRL/AAAA4gAAAE0AAAD1Jy+V/yMj7f8mMHT/BQAA/yUvbv8nLa//JSHl/ycgzv8oIdL/JSHk/xcaJ/8FAAD/AgAA/wEBBqMAAAAEAAAAoQIAAP8UFyH/DxAV/yYtsP8oLMT/JyXM/ycgzv8nINL/JiHi/yUly/8CAAD/AgAA/wIFBv8AAABDAAAAAAAAACIAAADxBQAA/wUFAv8nMJ//ICQ8/yIrZP8lIdX/JyPO/yMj7f8lL2//AgAA/wICBf8AAACoAAAAAAAAAAAAAAAAAAAASgICBf8CAAD/Iixn/yMj7f8lI9L/JiDf/yYuif8iK23/Fxgj/wIAAP8AAAHSAAAACgAAAAAAAAAAAAAAAAAAAAAAAAA4AAAA6AAAAP8ZHS//CgoQ/woKD/8bIDX/CgoL/wIAAP8BAQapAAAABwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAkAAABkAQAAvgAAAO4AAAD1AQAA4QAAAKEAAAA7AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAoAAAAIAAAAEAAAAABACAAAAAAAAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD///8v////nP///9H////j////7/////T////w////5v///9b///+s////RgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD///9c/v7+z///////////////////////////////////////////////////////////////4f7+/nX///8CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD///8P/v7+u////////////////////////////////////////////////////////////////////////////////////9f///8tAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/v7+Ov////////////////////////////////////////////////////////////////////+YmJz0pKSn9//////////////////////+/v5pAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP///1T//////////////////////////////////////////////////////////////////////////7e3uPYAAAD0bm5y+/////7///////////////////+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD+/v46/////f///////////////////////////////////////////////////////////////////////////////05PUPgAAAD3fn6B+u7u7/v///////////////////9sAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA////Bf///+H/////////////////////////////////////////////////////////////////////////////////////////+zo6O/gAAAL/Kywv+f////n////8//////////7q6uolAAAAAAAAAAAAAAAAAAAAAAAAAAD///+W/////////////////////////////////////////////////////////////////////6ysr/8eHiD/NTY3////////////t7e5+AAAAPoAAAD/CgoK+zY4OvhHR0nvREVH/x0dH9AAAAAAAAAAAAAAAAAAAAAA////N///////////////////////////////////////////////////////////////////////////gICD/0dHSf9wcHT/1dXX///////////3HBwc9QAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAF4AAAAAAAAAAAAAAAD///+M//////////////////////////6lpaf3pqap+P/////////////////////////+/////f////z////9//////////z//////////v////pNTU/5AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAAvQAAAAAAAAAAf39/BqOjpt+zsrX/7Ozt+///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AAAAAAAAAAAAAAAAAAQAAABVAAAAowAAANMAAADtAAAA9gAAAPEAAADbAAAArwAAAGkAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=">
11
+      <style>
12
+
13
+@font-face {
14
+  font-family: 'Open Sans';
15
+  src: url(data:application/x-font-woff;charset=utf-8;base64,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) format('woff');
16
+  font-weight: 400;
17
+  font-style: normal;
18
+}
19
+@font-face {
20
+  font-family: 'Open Sans';
21
+  src: url(data:application/x-font-woff;charset=utf-8;base64,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) format('woff');
22
+  font-weight: 700;
23
+  font-style: normal;
24
+}
25
+.alert {
26
+  padding: 7px;
27
+  margin-bottom: 20px;
28
+  border: 1px solid transparent;
29
+  border-radius: 1px;
30
+}
31
+.alert h4 {
32
+  margin-top: 0;
33
+  color: inherit;
34
+}
35
+.alert .alert-link {
36
+  font-weight: 500;
37
+}
38
+.alert > p,
39
+.alert > ul {
40
+  margin-bottom: 0;
41
+}
42
+.alert > p + p {
43
+  margin-top: 5px;
44
+}
45
+.alert-dismissable {
46
+  padding-right: 27px;
47
+}
48
+.alert-dismissable .close {
49
+  position: relative;
50
+  top: -2px;
51
+  right: -21px;
52
+  color: inherit;
53
+}
54
+.alert-success {
55
+  background-color: #ffffff;
56
+  border-color: #5cb75c;
57
+  color: #333333;
58
+}
59
+.alert-success hr {
60
+  border-top-color: #4cad4c;
61
+}
62
+.alert-success .alert-link {
63
+  color: #1a1a1a;
64
+}
65
+.alert-info {
66
+  background-color: #ffffff;
67
+  border-color: #cccccc;
68
+  color: #333333;
69
+}
70
+.alert-info hr {
71
+  border-top-color: #bfbfbf;
72
+}
73
+.alert-info .alert-link {
74
+  color: #1a1a1a;
75
+}
76
+.alert-warning {
77
+  background-color: #ffffff;
78
+  border-color: #eb7720;
79
+  color: #333333;
80
+}
81
+.alert-warning hr {
82
+  border-top-color: #de6a14;
83
+}
84
+.alert-warning .alert-link {
85
+  color: #1a1a1a;
86
+}
87
+.alert-danger {
88
+  background-color: #ffffff;
89
+  border-color: #c90813;
90
+  color: #333333;
91
+}
92
+.alert-danger hr {
93
+  border-top-color: #b00711;
94
+}
95
+.alert-danger .alert-link {
96
+  color: #1a1a1a;
97
+}
98
+.btn {
99
+  display: inline-block;
100
+  margin-bottom: 0;
101
+  font-weight: 600;
102
+  text-align: center;
103
+  vertical-align: middle;
104
+  cursor: pointer;
105
+  background-image: none;
106
+  border: 1px solid transparent;
107
+  white-space: nowrap;
108
+  padding: 2px 6px;
109
+  font-size: 12px;
110
+  line-height: 1.66666667;
111
+  border-radius: 1px;
112
+  -webkit-user-select: none;
113
+  -moz-user-select: none;
114
+  -ms-user-select: none;
115
+  user-select: none;
116
+}
117
+.btn:focus,
118
+.btn:active:focus,
119
+.btn.active:focus {
120
+  outline: thin dotted;
121
+  outline: 5px auto -webkit-focus-ring-color;
122
+  outline-offset: -2px;
123
+}
124
+.btn:hover,
125
+.btn:focus {
126
+  color: #4d5258;
127
+  text-decoration: none;
128
+}
129
+.btn:active,
130
+.btn.active {
131
+  outline: 0;
132
+  background-image: none;
133
+  -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
134
+  box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
135
+}
136
+.btn.disabled,
137
+.btn[disabled],
138
+fieldset[disabled] .btn {
139
+  cursor: not-allowed;
140
+  pointer-events: none;
141
+  opacity: 0.65;
142
+  filter: alpha(opacity=65);
143
+  -webkit-box-shadow: none;
144
+  box-shadow: none;
145
+}
146
+.btn-default {
147
+  color: #4d5258;
148
+  background-color: #eeeeee;
149
+  border-color: #b7b7b7;
150
+}
151
+.btn-default:hover,
152
+.btn-default:focus,
153
+.btn-default:active,
154
+.btn-default.active,
155
+.open .dropdown-toggle.btn-default {
156
+  color: #4d5258;
157
+  background-color: #dadada;
158
+  border-color: #989898;
159
+}
160
+.btn-default:active,
161
+.btn-default.active,
162
+.open .dropdown-toggle.btn-default {
163
+  background-image: none;
164
+}
165
+.btn-default.disabled,
166
+.btn-default[disabled],
167
+fieldset[disabled] .btn-default,
168
+.btn-default.disabled:hover,
169
+.btn-default[disabled]:hover,
170
+fieldset[disabled] .btn-default:hover,
171
+.btn-default.disabled:focus,
172
+.btn-default[disabled]:focus,
173
+fieldset[disabled] .btn-default:focus,
174
+.btn-default.disabled:active,
175
+.btn-default[disabled]:active,
176
+fieldset[disabled] .btn-default:active,
177
+.btn-default.disabled.active,
178
+.btn-default[disabled].active,
179
+fieldset[disabled] .btn-default.active {
180
+  background-color: #eeeeee;
181
+  border-color: #b7b7b7;
182
+}
183
+.btn-default .badge {
184
+  color: #eeeeee;
185
+  background-color: #4d5258;
186
+}
187
+.btn-primary {
188
+  color: #ffffff;
189
+  background-color: #189ad1;
190
+  border-color: #267da1;
191
+}
192
+.btn-primary:hover,
193
+.btn-primary:focus,
194
+.btn-primary:active,
195
+.btn-primary.active,
196
+.open .dropdown-toggle.btn-primary {
197
+  color: #ffffff;
198
+  background-color: #147fac;
199
+  border-color: #1a576f;
200
+}
201
+.btn-primary:active,
202
+.btn-primary.active,
203
+.open .dropdown-toggle.btn-primary {
204
+  background-image: none;
205
+}
206
+.btn-primary.disabled,
207
+.btn-primary[disabled],
208
+fieldset[disabled] .btn-primary,
209
+.btn-primary.disabled:hover,
210
+.btn-primary[disabled]:hover,
211
+fieldset[disabled] .btn-primary:hover,
212
+.btn-primary.disabled:focus,
213
+.btn-primary[disabled]:focus,
214
+fieldset[disabled] .btn-primary:focus,
215
+.btn-primary.disabled:active,
216
+.btn-primary[disabled]:active,
217
+fieldset[disabled] .btn-primary:active,
218
+.btn-primary.disabled.active,
219
+.btn-primary[disabled].active,
220
+fieldset[disabled] .btn-primary.active {
221
+  background-color: #189ad1;
222
+  border-color: #267da1;
223
+}
224
+.btn-primary .badge {
225
+  color: #189ad1;
226
+  background-color: #ffffff;
227
+}
228
+.btn-success {
229
+  color: #ffffff;
230
+  background-color: #5cb75c;
231
+  border-color: #4cad4c;
232
+}
233
+.btn-success:hover,
234
+.btn-success:focus,
235
+.btn-success:active,
236
+.btn-success.active,
237
+.open .dropdown-toggle.btn-success {
238
+  color: #ffffff;
239
+  background-color: #48a248;
240
+  border-color: #3a833a;
241
+}
242
+.btn-success:active,
243
+.btn-success.active,
244
+.open .dropdown-toggle.btn-success {
245
+  background-image: none;
246
+}
247
+.btn-success.disabled,
248
+.btn-success[disabled],
249
+fieldset[disabled] .btn-success,
250
+.btn-success.disabled:hover,
251
+.btn-success[disabled]:hover,
252
+fieldset[disabled] .btn-success:hover,
253
+.btn-success.disabled:focus,
254
+.btn-success[disabled]:focus,
255
+fieldset[disabled] .btn-success:focus,
256
+.btn-success.disabled:active,
257
+.btn-success[disabled]:active,
258
+fieldset[disabled] .btn-success:active,
259
+.btn-success.disabled.active,
260
+.btn-success[disabled].active,
261
+fieldset[disabled] .btn-success.active {
262
+  background-color: #5cb75c;
263
+  border-color: #4cad4c;
264
+}
265
+.btn-success .badge {
266
+  color: #5cb75c;
267
+  background-color: #ffffff;
268
+}
269
+.btn-info {
270
+  color: #ffffff;
271
+  background-color: #27799c;
272
+  border-color: #226988;
273
+}
274
+.btn-info:hover,
275
+.btn-info:focus,
276
+.btn-info:active,
277
+.btn-info.active,
278
+.open .dropdown-toggle.btn-info {
279
+  color: #ffffff;
280
+  background-color: #1f607b;
281
+  border-color: #164357;
282
+}
283
+.btn-info:active,
284
+.btn-info.active,
285
+.open .dropdown-toggle.btn-info {
286
+  background-image: none;
287
+}
288
+.btn-info.disabled,
289
+.btn-info[disabled],
290
+fieldset[disabled] .btn-info,
291
+.btn-info.disabled:hover,
292
+.btn-info[disabled]:hover,
293
+fieldset[disabled] .btn-info:hover,
294
+.btn-info.disabled:focus,
295
+.btn-info[disabled]:focus,
296
+fieldset[disabled] .btn-info:focus,
297
+.btn-info.disabled:active,
298
+.btn-info[disabled]:active,
299
+fieldset[disabled] .btn-info:active,
300
+.btn-info.disabled.active,
301
+.btn-info[disabled].active,
302
+fieldset[disabled] .btn-info.active {
303
+  background-color: #27799c;
304
+  border-color: #226988;
305
+}
306
+.btn-info .badge {
307
+  color: #27799c;
308
+  background-color: #ffffff;
309
+}
310
+.btn-warning {
311
+  color: #ffffff;
312
+  background-color: #eb7720;
313
+  border-color: #de6a14;
314
+}
315
+.btn-warning:hover,
316
+.btn-warning:focus,
317
+.btn-warning:active,
318
+.btn-warning.active,
319
+.open .dropdown-toggle.btn-warning {
320
+  color: #ffffff;
321
+  background-color: #d06413;
322
+  border-color: #a54f0f;
323
+}
324
+.btn-warning:active,
325
+.btn-warning.active,
326
+.open .dropdown-toggle.btn-warning {
327
+  background-image: none;
328
+}
329
+.btn-warning.disabled,
330
+.btn-warning[disabled],
331
+fieldset[disabled] .btn-warning,
332
+.btn-warning.disabled:hover,
333
+.btn-warning[disabled]:hover,
334
+fieldset[disabled] .btn-warning:hover,
335
+.btn-warning.disabled:focus,
336
+.btn-warning[disabled]:focus,
337
+fieldset[disabled] .btn-warning:focus,
338
+.btn-warning.disabled:active,
339
+.btn-warning[disabled]:active,
340
+fieldset[disabled] .btn-warning:active,
341
+.btn-warning.disabled.active,
342
+.btn-warning[disabled].active,
343
+fieldset[disabled] .btn-warning.active {
344
+  background-color: #eb7720;
345
+  border-color: #de6a14;
346
+}
347
+.btn-warning .badge {
348
+  color: #eb7720;
349
+  background-color: #ffffff;
350
+}
351
+.btn-danger {
352
+  color: #ffffff;
353
+  background-color: #ab070f;
354
+  border-color: #781919;
355
+}
356
+.btn-danger:hover,
357
+.btn-danger:focus,
358
+.btn-danger:active,
359
+.btn-danger.active,
360
+.open .dropdown-toggle.btn-danger {
361
+  color: #ffffff;
362
+  background-color: #84050c;
363
+  border-color: #450e0e;
364
+}
365
+.btn-danger:active,
366
+.btn-danger.active,
367
+.open .dropdown-toggle.btn-danger {
368
+  background-image: none;
369
+}
370
+.btn-danger.disabled,
371
+.btn-danger[disabled],
372
+fieldset[disabled] .btn-danger,
373
+.btn-danger.disabled:hover,
374
+.btn-danger[disabled]:hover,
375
+fieldset[disabled] .btn-danger:hover,
376
+.btn-danger.disabled:focus,
377
+.btn-danger[disabled]:focus,
378
+fieldset[disabled] .btn-danger:focus,
379
+.btn-danger.disabled:active,
380
+.btn-danger[disabled]:active,
381
+fieldset[disabled] .btn-danger:active,
382
+.btn-danger.disabled.active,
383
+.btn-danger[disabled].active,
384
+fieldset[disabled] .btn-danger.active {
385
+  background-color: #ab070f;
386
+  border-color: #781919;
387
+}
388
+.btn-danger .badge {
389
+  color: #ab070f;
390
+  background-color: #ffffff;
391
+}
392
+.btn-link {
393
+  color: #0099d3;
394
+  font-weight: normal;
395
+  cursor: pointer;
396
+  border-radius: 0;
397
+}
398
+.btn-link,
399
+.btn-link:active,
400
+.btn-link[disabled],
401
+fieldset[disabled] .btn-link {
402
+  background-color: transparent;
403
+  -webkit-box-shadow: none;
404
+  box-shadow: none;
405
+}
406
+.btn-link,
407
+.btn-link:hover,
408
+.btn-link:focus,
409
+.btn-link:active {
410
+  border-color: transparent;
411
+}
412
+.btn-link:hover,
413
+.btn-link:focus {
414
+  color: #00618a;
415
+  text-decoration: underline;
416
+  background-color: transparent;
417
+}
418
+.btn-link[disabled]:hover,
419
+fieldset[disabled] .btn-link:hover,
420
+.btn-link[disabled]:focus,
421
+fieldset[disabled] .btn-link:focus {
422
+  color: #999999;
423
+  text-decoration: none;
424
+}
425
+.btn-lg {
426
+  padding: 6px 10px;
427
+  font-size: 14px;
428
+  line-height: 1.33;
429
+  border-radius: 1px;
430
+}
431
+.btn-sm {
432
+  padding: 2px 6px;
433
+  font-size: 11px;
434
+  line-height: 1.5;
435
+  border-radius: 1px;
436
+}
437
+.btn-xs {
438
+  padding: 1px 5px;
439
+  font-size: 11px;
440
+  line-height: 1.5;
441
+  border-radius: 1px;
442
+}
443
+.btn-block {
444
+  display: block;
445
+  width: 100%;
446
+  padding-left: 0;
447
+  padding-right: 0;
448
+}
449
+.btn-block + .btn-block {
450
+  margin-top: 5px;
451
+}
452
+input[type="submit"].btn-block,
453
+input[type="reset"].btn-block,
454
+input[type="button"].btn-block {
455
+  width: 100%;
456
+}
457
+.fade {
458
+  opacity: 0;
459
+  -webkit-transition: opacity 0.15s linear;
460
+  transition: opacity 0.15s linear;
461
+}
462
+.fade.in {
463
+  opacity: 1;
464
+}
465
+.collapse {
466
+  display: none;
467
+}
468
+.collapse.in {
469
+  display: block;
470
+}
471
+.collapsing {
472
+  position: relative;
473
+  height: 0;
474
+  overflow: hidden;
475
+  -webkit-transition: height 0.35s ease;
476
+  transition: height 0.35s ease;
477
+}
478
+fieldset {
479
+  padding: 0;
480
+  margin: 0;
481
+  border: 0;
482
+  min-width: 0;
483
+}
484
+legend {
485
+  display: block;
486
+  width: 100%;
487
+  padding: 0;
488
+  margin-bottom: 20px;
489
+  font-size: 18px;
490
+  line-height: inherit;
491
+  color: #333333;
492
+  border: 0;
493
+  border-bottom: 1px solid #e5e5e5;
494
+}
495
+label {
496
+  display: inline-block;
497
+  margin-bottom: 5px;
498
+  font-weight: bold;
499
+}
500
+input[type="search"] {
501
+  -webkit-box-sizing: border-box;
502
+  -moz-box-sizing: border-box;
503
+  box-sizing: border-box;
504
+}
505
+input[type="radio"],
506
+input[type="checkbox"] {
507
+  margin: 4px 0 0;
508
+  margin-top: 1px \9;
509
+
510
+  line-height: normal;
511
+}
512
+input[type="file"] {
513
+  display: block;
514
+}
515
+input[type="range"] {
516
+  display: block;
517
+  width: 100%;
518
+}
519
+select[multiple],
520
+select[size] {
521
+  height: auto;
522
+}
523
+input[type="file"]:focus,
524
+input[type="radio"]:focus,
525
+input[type="checkbox"]:focus {
526
+  outline: thin dotted;
527
+  outline: 5px auto -webkit-focus-ring-color;
528
+  outline-offset: -2px;
529
+}
530
+output {
531
+  display: block;
532
+  padding-top: 3px;
533
+  font-size: 12px;
534
+  line-height: 1.66666667;
535
+  color: #333333;
536
+}
537
+.form-control {
538
+  display: block;
539
+  width: 100%;
540
+  height: 26px;
541
+  padding: 2px 6px;
542
+  font-size: 12px;
543
+  line-height: 1.66666667;
544
+  color: #333333;
545
+  background-color: #ffffff;
546
+  background-image: none;
547
+  border: 1px solid #bababa;
548
+  border-radius: 1px;
549
+  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
550
+  box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
551
+  -webkit-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;
552
+  transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;
553
+}
554
+.form-control:focus {
555
+  border-color: #66afe9;
556
+  outline: 0;
557
+  -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, 0.6);
558
+  box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, 0.6);
559
+}
560
+.form-control::-moz-placeholder {
561
+  color: #999999;
562
+  opacity: 1;
563
+}
564
+.form-control:-ms-input-placeholder {
565
+  color: #999999;
566
+}
567
+.form-control::-webkit-input-placeholder {
568
+  color: #999999;
569
+}
570
+.form-control:-moz-placeholder {
571
+  color: #999999;
572
+  font-style: italic;
573
+}
574
+.form-control::-moz-placeholder {
575
+  color: #999999;
576
+  font-style: italic;
577
+}
578
+.form-control:-ms-input-placeholder {
579
+  color: #999999;
580
+  font-style: italic;
581
+}
582
+.form-control::-webkit-input-placeholder {
583
+  color: #999999;
584
+  font-style: italic;
585
+}
586
+.form-control[disabled],
587
+.form-control[readonly],
588
+fieldset[disabled] .form-control {
589
+  cursor: not-allowed;
590
+  background-color: #f8f8f8;
591
+  opacity: 1;
592
+}
593
+textarea.form-control {
594
+  height: auto;
595
+}
596
+input[type="search"] {
597
+  -webkit-appearance: none;
598
+}
599
+input[type="date"] {
600
+  line-height: 26px;
601
+}
602
+.form-group {
603
+  margin-bottom: 15px;
604
+}
605
+.radio,
606
+.checkbox {
607
+  display: block;
608
+  min-height: 20px;
609
+  margin-top: 10px;
610
+  margin-bottom: 10px;
611
+  padding-left: 20px;
612
+}
613
+.radio label,
614
+.checkbox label {
615
+  display: inline;
616
+  font-weight: normal;
617
+  cursor: pointer;
618
+}
619
+.radio input[type="radio"],
620
+.radio-inline input[type="radio"],
621
+.checkbox input[type="checkbox"],
622
+.checkbox-inline input[type="checkbox"] {
623
+  float: left;
624
+  margin-left: -20px;
625
+}
626
+.radio + .radio,
627
+.checkbox + .checkbox {
628
+  margin-top: -5px;
629
+}
630
+.radio-inline,
631
+.checkbox-inline {
632
+  display: inline-block;
633
+  padding-left: 20px;
634
+  margin-bottom: 0;
635
+  vertical-align: middle;
636
+  font-weight: normal;
637
+  cursor: pointer;
638
+}
639
+.radio-inline + .radio-inline,
640
+.checkbox-inline + .checkbox-inline {
641
+  margin-top: 0;
642
+  margin-left: 10px;
643
+}
644
+input[type="radio"][disabled],
645
+input[type="checkbox"][disabled],
646
+.radio[disabled],
647
+.radio-inline[disabled],
648
+.checkbox[disabled],
649
+.checkbox-inline[disabled],
650
+fieldset[disabled] input[type="radio"],
651
+fieldset[disabled] input[type="checkbox"],
652
+fieldset[disabled] .radio,
653
+fieldset[disabled] .radio-inline,
654
+fieldset[disabled] .checkbox,
655
+fieldset[disabled] .checkbox-inline {
656
+  cursor: not-allowed;
657
+}
658
+.input-sm {
659
+  height: 22px;
660
+  padding: 2px 6px;
661
+  font-size: 11px;
662
+  line-height: 1.5;
663
+  border-radius: 1px;
664
+}
665
+select.input-sm {
666
+  height: 22px;
667
+  line-height: 22px;
668
+}
669
+textarea.input-sm,
670
+select[multiple].input-sm {
671
+  height: auto;
672
+}
673
+.input-lg {
674
+  height: 33px;
675
+  padding: 6px 10px;
676
+  font-size: 14px;
677
+  line-height: 1.33;
678
+  border-radius: 1px;
679
+}
680
+select.input-lg {
681
+  height: 33px;
682
+  line-height: 33px;
683
+}
684
+textarea.input-lg,
685
+select[multiple].input-lg {
686
+  height: auto;
687
+}
688
+.has-feedback {
689
+  position: relative;
690
+}
691
+.has-feedback .form-control {
692
+  padding-right: 32.5px;
693
+}
694
+.has-feedback .form-control-feedback {
695
+  position: absolute;
696
+  top: 25px;
697
+  right: 0;
698
+  display: block;
699
+  width: 26px;
700
+  height: 26px;
701
+  line-height: 26px;
702
+  text-align: center;
703
+}
704
+.has-success .help-block,
705
+.has-success .control-label,
706
+.has-success .radio,
707
+.has-success .checkbox,
708
+.has-success .radio-inline,
709
+.has-success .checkbox-inline {
710
+  color: #3c763d;
711
+}
712
+.has-success .form-control {
713
+  border-color: #3c763d;
714
+  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
715
+  box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
716
+}
717
+.has-success .form-control:focus {
718
+  border-color: #2b542c;
719
+  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #67b168;
720
+  box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #67b168;
721
+}
722
+.has-success .input-group-addon {
723
+  color: #3c763d;
724
+  border-color: #3c763d;
725
+  background-color: #dff0d8;
726
+}
727
+.has-success .form-control-feedback {
728
+  color: #3c763d;
729
+}
730
+.has-warning .help-block,
731
+.has-warning .control-label,
732
+.has-warning .radio,
733
+.has-warning .checkbox,
734
+.has-warning .radio-inline,
735
+.has-warning .checkbox-inline {
736
+  color: #8a6d3b;
737
+}
738
+.has-warning .form-control {
739
+  border-color: #8a6d3b;
740
+  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
741
+  box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
742
+}
743
+.has-warning .form-control:focus {
744
+  border-color: #66512c;
745
+  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #c0a16b;
746
+  box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #c0a16b;
747
+}
748
+.has-warning .input-group-addon {
749
+  color: #8a6d3b;
750
+  border-color: #8a6d3b;
751
+  background-color: #fcf8e3;
752
+}
753
+.has-warning .form-control-feedback {
754
+  color: #8a6d3b;
755
+}
756
+.has-error .help-block,
757
+.has-error .control-label,
758
+.has-error .radio,
759
+.has-error .checkbox,
760
+.has-error .radio-inline,
761
+.has-error .checkbox-inline {
762
+  color: #a94442;
763
+}
764
+.has-error .form-control {
765
+  border-color: #a94442;
766
+  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
767
+  box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
768
+}
769
+.has-error .form-control:focus {
770
+  border-color: #843534;
771
+  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ce8483;
772
+  box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ce8483;
773
+}
774
+.has-error .input-group-addon {
775
+  color: #a94442;
776
+  border-color: #a94442;
777
+  background-color: #f2dede;
778
+}
779
+.has-error .form-control-feedback {
780
+  color: #a94442;
781
+}
782
+.form-control-static {
783
+  margin-bottom: 0;
784
+}
785
+.help-block {
786
+  display: block;
787
+  margin-top: 5px;
788
+  margin-bottom: 10px;
789
+  color: #737373;
790
+}
791
+@media (min-width: 768px) {
792
+  .form-inline .form-group {
793
+    display: inline-block;
794
+    margin-bottom: 0;
795
+    vertical-align: middle;
796
+  }
797
+  .form-inline .form-control {
798
+    display: inline-block;
799
+    width: auto;
800
+    vertical-align: middle;
801
+  }
802
+  .form-inline .input-group > .form-control {
803
+    width: 100%;
804
+  }
805
+  .form-inline .control-label {
806
+    margin-bottom: 0;
807
+    vertical-align: middle;
808
+  }
809
+  .form-inline .radio,
810
+  .form-inline .checkbox {
811
+    display: inline-block;
812
+    margin-top: 0;
813
+    margin-bottom: 0;
814
+    padding-left: 0;
815
+    vertical-align: middle;
816
+  }
817
+  .form-inline .radio input[type="radio"],
818
+  .form-inline .checkbox input[type="checkbox"] {
819
+    float: none;
820
+    margin-left: 0;
821
+  }
822
+  .form-inline .has-feedback .form-control-feedback {
823
+    top: 0;
824
+  }
825
+}
826
+.form-horizontal .control-label,
827
+.form-horizontal .radio,
828
+.form-horizontal .checkbox,
829
+.form-horizontal .radio-inline,
830
+.form-horizontal .checkbox-inline {
831
+  margin-top: 0;
832
+  margin-bottom: 0;
833
+  padding-top: 3px;
834
+}
835
+.form-horizontal .radio,
836
+.form-horizontal .checkbox {
837
+  min-height: 23px;
838
+}
839
+.form-horizontal .form-group {
840
+  margin-left: -20px;
841
+  margin-right: -20px;
842
+}
843
+.form-horizontal .form-control-static {
844
+  padding-top: 3px;
845
+}
846
+@media (min-width: 768px) {
847
+  .form-horizontal .control-label {
848
+    text-align: right;
849
+  }
850
+}
851
+.form-horizontal .has-feedback .form-control-feedback {
852
+  top: 0;
853
+  right: 20px;
854
+}
855
+.container {
856
+  margin-right: auto;
857
+  margin-left: auto;
858
+  padding-left: 20px;
859
+  padding-right: 20px;
860
+}
861
+@media (min-width: 768px) {
862
+  .container {
863
+    width: 760px;
864
+  }
865
+}
866
+@media (min-width: 992px) {
867
+  .container {
868
+    width: 980px;
869
+  }
870
+}
871
+@media (min-width: 1200px) {
872
+  .container {
873
+    width: 1180px;
874
+  }
875
+}
876
+.container-fluid {
877
+  margin-right: auto;
878
+  margin-left: auto;
879
+  padding-left: 20px;
880
+  padding-right: 20px;
881
+}
882
+.row {
883
+  margin-left: -20px;
884
+  margin-right: -20px;
885
+}
886
+.col-xs-1, .col-sm-1, .col-md-1, .col-lg-1, .col-xs-2, .col-sm-2, .col-md-2, .col-lg-2, .col-xs-3, .col-sm-3, .col-md-3, .col-lg-3, .col-xs-4, .col-sm-4, .col-md-4, .col-lg-4, .col-xs-5, .col-sm-5, .col-md-5, .col-lg-5, .col-xs-6, .col-sm-6, .col-md-6, .col-lg-6, .col-xs-7, .col-sm-7, .col-md-7, .col-lg-7, .col-xs-8, .col-sm-8, .col-md-8, .col-lg-8, .col-xs-9, .col-sm-9, .col-md-9, .col-lg-9, .col-xs-10, .col-sm-10, .col-md-10, .col-lg-10, .col-xs-11, .col-sm-11, .col-md-11, .col-lg-11, .col-xs-12, .col-sm-12, .col-md-12, .col-lg-12 {
887
+  position: relative;
888
+  min-height: 1px;
889
+  padding-left: 20px;
890
+  padding-right: 20px;
891
+}
892
+.col-xs-1, .col-xs-2, .col-xs-3, .col-xs-4, .col-xs-5, .col-xs-6, .col-xs-7, .col-xs-8, .col-xs-9, .col-xs-10, .col-xs-11, .col-xs-12 {
893
+  float: left;
894
+}
895
+.col-xs-12 {
896
+  width: 100%;
897
+}
898
+.col-xs-11 {
899
+  width: 91.66666666666666%;
900
+}
901
+.col-xs-10 {
902
+  width: 83.33333333333334%;
903
+}
904
+.col-xs-9 {
905
+  width: 75%;
906
+}
907
+.col-xs-8 {
908
+  width: 66.66666666666666%;
909
+}
910
+.col-xs-7 {
911
+  width: 58.333333333333336%;
912
+}
913
+.col-xs-6 {
914
+  width: 50%;
915
+}
916
+.col-xs-5 {
917
+  width: 41.66666666666667%;
918
+}
919
+.col-xs-4 {
920
+  width: 33.33333333333333%;
921
+}
922
+.col-xs-3 {
923
+  width: 25%;
924
+}
925
+.col-xs-2 {
926
+  width: 16.666666666666664%;
927
+}
928
+.col-xs-1 {
929
+  width: 8.333333333333332%;
930
+}
931
+.col-xs-pull-12 {
932
+  right: 100%;
933
+}
934
+.col-xs-pull-11 {
935
+  right: 91.66666666666666%;
936
+}
937
+.col-xs-pull-10 {
938
+  right: 83.33333333333334%;
939
+}
940
+.col-xs-pull-9 {
941
+  right: 75%;
942
+}
943
+.col-xs-pull-8 {
944
+  right: 66.66666666666666%;
945
+}
946
+.col-xs-pull-7 {
947
+  right: 58.333333333333336%;
948
+}
949
+.col-xs-pull-6 {
950
+  right: 50%;
951
+}
952
+.col-xs-pull-5 {
953
+  right: 41.66666666666667%;
954
+}
955
+.col-xs-pull-4 {
956
+  right: 33.33333333333333%;
957
+}
958
+.col-xs-pull-3 {
959
+  right: 25%;
960
+}
961
+.col-xs-pull-2 {
962
+  right: 16.666666666666664%;
963
+}
964
+.col-xs-pull-1 {
965
+  right: 8.333333333333332%;
966
+}
967
+.col-xs-pull-0 {
968
+  right: 0%;
969
+}
970
+.col-xs-push-12 {
971
+  left: 100%;
972
+}
973
+.col-xs-push-11 {
974
+  left: 91.66666666666666%;
975
+}
976
+.col-xs-push-10 {
977
+  left: 83.33333333333334%;
978
+}
979
+.col-xs-push-9 {
980
+  left: 75%;
981
+}
982
+.col-xs-push-8 {
983
+  left: 66.66666666666666%;
984
+}
985
+.col-xs-push-7 {
986
+  left: 58.333333333333336%;
987
+}
988
+.col-xs-push-6 {
989
+  left: 50%;
990
+}
991
+.col-xs-push-5 {
992
+  left: 41.66666666666667%;
993
+}
994
+.col-xs-push-4 {
995
+  left: 33.33333333333333%;
996
+}
997
+.col-xs-push-3 {
998
+  left: 25%;
999
+}
1000
+.col-xs-push-2 {
1001
+  left: 16.666666666666664%;
1002
+}
1003
+.col-xs-push-1 {
1004
+  left: 8.333333333333332%;
1005
+}
1006
+.col-xs-push-0 {
1007
+  left: 0%;
1008
+}
1009
+.col-xs-offset-12 {
1010
+  margin-left: 100%;
1011
+}
1012
+.col-xs-offset-11 {
1013
+  margin-left: 91.66666666666666%;
1014
+}
1015
+.col-xs-offset-10 {
1016
+  margin-left: 83.33333333333334%;
1017
+}
1018
+.col-xs-offset-9 {
1019
+  margin-left: 75%;
1020
+}
1021
+.col-xs-offset-8 {
1022
+  margin-left: 66.66666666666666%;
1023
+}
1024
+.col-xs-offset-7 {
1025
+  margin-left: 58.333333333333336%;
1026
+}
1027
+.col-xs-offset-6 {
1028
+  margin-left: 50%;
1029
+}
1030
+.col-xs-offset-5 {
1031
+  margin-left: 41.66666666666667%;
1032
+}
1033
+.col-xs-offset-4 {
1034
+  margin-left: 33.33333333333333%;
1035
+}
1036
+.col-xs-offset-3 {
1037
+  margin-left: 25%;
1038
+}
1039
+.col-xs-offset-2 {
1040
+  margin-left: 16.666666666666664%;
1041
+}
1042
+.col-xs-offset-1 {
1043
+  margin-left: 8.333333333333332%;
1044
+}
1045
+.col-xs-offset-0 {
1046
+  margin-left: 0%;
1047
+}
1048
+@media (min-width: 768px) {
1049
+  .col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12 {
1050
+    float: left;
1051
+  }
1052
+  .col-sm-12 {
1053
+    width: 100%;
1054
+  }
1055
+  .col-sm-11 {
1056
+    width: 91.66666666666666%;
1057
+  }
1058
+  .col-sm-10 {
1059
+    width: 83.33333333333334%;
1060
+  }
1061
+  .col-sm-9 {
1062
+    width: 75%;
1063
+  }
1064
+  .col-sm-8 {
1065
+    width: 66.66666666666666%;
1066
+  }
1067
+  .col-sm-7 {
1068
+    width: 58.333333333333336%;
1069
+  }
1070
+  .col-sm-6 {
1071
+    width: 50%;
1072
+  }
1073
+  .col-sm-5 {
1074
+    width: 41.66666666666667%;
1075
+  }
1076
+  .col-sm-4 {
1077
+    width: 33.33333333333333%;
1078
+  }
1079
+  .col-sm-3 {
1080
+    width: 25%;
1081
+  }
1082
+  .col-sm-2 {
1083
+    width: 16.666666666666664%;
1084
+  }
1085
+  .col-sm-1 {
1086
+    width: 8.333333333333332%;
1087
+  }
1088
+  .col-sm-pull-12 {
1089
+    right: 100%;
1090
+  }
1091
+  .col-sm-pull-11 {
1092
+    right: 91.66666666666666%;
1093
+  }
1094
+  .col-sm-pull-10 {
1095
+    right: 83.33333333333334%;
1096
+  }
1097
+  .col-sm-pull-9 {
1098
+    right: 75%;
1099
+  }
1100
+  .col-sm-pull-8 {
1101
+    right: 66.66666666666666%;
1102
+  }
1103
+  .col-sm-pull-7 {
1104
+    right: 58.333333333333336%;
1105
+  }
1106
+  .col-sm-pull-6 {
1107
+    right: 50%;
1108
+  }
1109
+  .col-sm-pull-5 {
1110
+    right: 41.66666666666667%;
1111
+  }
1112
+  .col-sm-pull-4 {
1113
+    right: 33.33333333333333%;
1114
+  }
1115
+  .col-sm-pull-3 {
1116
+    right: 25%;
1117
+  }
1118
+  .col-sm-pull-2 {
1119
+    right: 16.666666666666664%;
1120
+  }
1121
+  .col-sm-pull-1 {
1122
+    right: 8.333333333333332%;
1123
+  }
1124
+  .col-sm-pull-0 {
1125
+    right: 0%;
1126
+  }
1127
+  .col-sm-push-12 {
1128
+    left: 100%;
1129
+  }
1130
+  .col-sm-push-11 {
1131
+    left: 91.66666666666666%;
1132
+  }
1133
+  .col-sm-push-10 {
1134
+    left: 83.33333333333334%;
1135
+  }
1136
+  .col-sm-push-9 {
1137
+    left: 75%;
1138
+  }
1139
+  .col-sm-push-8 {
1140
+    left: 66.66666666666666%;
1141
+  }
1142
+  .col-sm-push-7 {
1143
+    left: 58.333333333333336%;
1144
+  }
1145
+  .col-sm-push-6 {
1146
+    left: 50%;
1147
+  }
1148
+  .col-sm-push-5 {
1149
+    left: 41.66666666666667%;
1150
+  }
1151
+  .col-sm-push-4 {
1152
+    left: 33.33333333333333%;
1153
+  }
1154
+  .col-sm-push-3 {
1155
+    left: 25%;
1156
+  }
1157
+  .col-sm-push-2 {
1158
+    left: 16.666666666666664%;
1159
+  }
1160
+  .col-sm-push-1 {
1161
+    left: 8.333333333333332%;
1162
+  }
1163
+  .col-sm-push-0 {
1164
+    left: 0%;
1165
+  }
1166
+  .col-sm-offset-12 {
1167
+    margin-left: 100%;
1168
+  }
1169
+  .col-sm-offset-11 {
1170
+    margin-left: 91.66666666666666%;
1171
+  }
1172
+  .col-sm-offset-10 {
1173
+    margin-left: 83.33333333333334%;
1174
+  }
1175
+  .col-sm-offset-9 {
1176
+    margin-left: 75%;
1177
+  }
1178
+  .col-sm-offset-8 {
1179
+    margin-left: 66.66666666666666%;
1180
+  }
1181
+  .col-sm-offset-7 {
1182
+    margin-left: 58.333333333333336%;
1183
+  }
1184
+  .col-sm-offset-6 {
1185
+    margin-left: 50%;
1186
+  }
1187
+  .col-sm-offset-5 {
1188
+    margin-left: 41.66666666666667%;
1189
+  }
1190
+  .col-sm-offset-4 {
1191
+    margin-left: 33.33333333333333%;
1192
+  }
1193
+  .col-sm-offset-3 {
1194
+    margin-left: 25%;
1195
+  }
1196
+  .col-sm-offset-2 {
1197
+    margin-left: 16.666666666666664%;
1198
+  }
1199
+  .col-sm-offset-1 {
1200
+    margin-left: 8.333333333333332%;
1201
+  }
1202
+  .col-sm-offset-0 {
1203
+    margin-left: 0%;
1204
+  }
1205
+}
1206
+@media (min-width: 992px) {
1207
+  .col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12 {
1208
+    float: left;
1209
+  }
1210
+  .col-md-12 {
1211
+    width: 100%;
1212
+  }
1213
+  .col-md-11 {
1214
+    width: 91.66666666666666%;
1215
+  }
1216
+  .col-md-10 {
1217
+    width: 83.33333333333334%;
1218
+  }
1219
+  .col-md-9 {
1220
+    width: 75%;
1221
+  }
1222
+  .col-md-8 {
1223
+    width: 66.66666666666666%;
1224
+  }
1225
+  .col-md-7 {
1226
+    width: 58.333333333333336%;
1227
+  }
1228
+  .col-md-6 {
1229
+    width: 50%;
1230
+  }
1231
+  .col-md-5 {
1232
+    width: 41.66666666666667%;
1233
+  }
1234
+  .col-md-4 {
1235
+    width: 33.33333333333333%;
1236
+  }
1237
+  .col-md-3 {
1238
+    width: 25%;
1239
+  }
1240
+  .col-md-2 {
1241
+    width: 16.666666666666664%;
1242
+  }
1243
+  .col-md-1 {
1244
+    width: 8.333333333333332%;
1245
+  }
1246
+  .col-md-pull-12 {
1247
+    right: 100%;
1248
+  }
1249
+  .col-md-pull-11 {
1250
+    right: 91.66666666666666%;
1251
+  }
1252
+  .col-md-pull-10 {
1253
+    right: 83.33333333333334%;
1254
+  }
1255
+  .col-md-pull-9 {
1256
+    right: 75%;
1257
+  }
1258
+  .col-md-pull-8 {
1259
+    right: 66.66666666666666%;
1260
+  }
1261
+  .col-md-pull-7 {
1262
+    right: 58.333333333333336%;
1263
+  }
1264
+  .col-md-pull-6 {
1265
+    right: 50%;
1266
+  }
1267
+  .col-md-pull-5 {
1268
+    right: 41.66666666666667%;
1269
+  }
1270
+  .col-md-pull-4 {
1271
+    right: 33.33333333333333%;
1272
+  }
1273
+  .col-md-pull-3 {
1274
+    right: 25%;
1275
+  }
1276
+  .col-md-pull-2 {
1277
+    right: 16.666666666666664%;
1278
+  }
1279
+  .col-md-pull-1 {
1280
+    right: 8.333333333333332%;
1281
+  }
1282
+  .col-md-pull-0 {
1283
+    right: 0%;
1284
+  }
1285
+  .col-md-push-12 {
1286
+    left: 100%;
1287
+  }
1288
+  .col-md-push-11 {
1289
+    left: 91.66666666666666%;
1290
+  }
1291
+  .col-md-push-10 {
1292
+    left: 83.33333333333334%;
1293
+  }
1294
+  .col-md-push-9 {
1295
+    left: 75%;
1296
+  }
1297
+  .col-md-push-8 {
1298
+    left: 66.66666666666666%;
1299
+  }
1300
+  .col-md-push-7 {
1301
+    left: 58.333333333333336%;
1302
+  }
1303
+  .col-md-push-6 {
1304
+    left: 50%;
1305
+  }
1306
+  .col-md-push-5 {
1307
+    left: 41.66666666666667%;
1308
+  }
1309
+  .col-md-push-4 {
1310
+    left: 33.33333333333333%;
1311
+  }
1312
+  .col-md-push-3 {
1313
+    left: 25%;
1314
+  }
1315
+  .col-md-push-2 {
1316
+    left: 16.666666666666664%;
1317
+  }
1318
+  .col-md-push-1 {
1319
+    left: 8.333333333333332%;
1320
+  }
1321
+  .col-md-push-0 {
1322
+    left: 0%;
1323
+  }
1324
+  .col-md-offset-12 {
1325
+    margin-left: 100%;
1326
+  }
1327
+  .col-md-offset-11 {
1328
+    margin-left: 91.66666666666666%;
1329
+  }
1330
+  .col-md-offset-10 {
1331
+    margin-left: 83.33333333333334%;
1332
+  }
1333
+  .col-md-offset-9 {
1334
+    margin-left: 75%;
1335
+  }
1336
+  .col-md-offset-8 {
1337
+    margin-left: 66.66666666666666%;
1338
+  }
1339
+  .col-md-offset-7 {
1340
+    margin-left: 58.333333333333336%;
1341
+  }
1342
+  .col-md-offset-6 {
1343
+    margin-left: 50%;
1344
+  }
1345
+  .col-md-offset-5 {
1346
+    margin-left: 41.66666666666667%;
1347
+  }
1348
+  .col-md-offset-4 {
1349
+    margin-left: 33.33333333333333%;
1350
+  }
1351
+  .col-md-offset-3 {
1352
+    margin-left: 25%;
1353
+  }
1354
+  .col-md-offset-2 {
1355
+    margin-left: 16.666666666666664%;
1356
+  }
1357
+  .col-md-offset-1 {
1358
+    margin-left: 8.333333333333332%;
1359
+  }
1360
+  .col-md-offset-0 {
1361
+    margin-left: 0%;
1362
+  }
1363
+}
1364
+@media (min-width: 1200px) {
1365
+  .col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12 {
1366
+    float: left;
1367
+  }
1368
+  .col-lg-12 {
1369
+    width: 100%;
1370
+  }
1371
+  .col-lg-11 {
1372
+    width: 91.66666666666666%;
1373
+  }
1374
+  .col-lg-10 {
1375
+    width: 83.33333333333334%;
1376
+  }
1377
+  .col-lg-9 {
1378
+    width: 75%;
1379
+  }
1380
+  .col-lg-8 {
1381
+    width: 66.66666666666666%;
1382
+  }
1383
+  .col-lg-7 {
1384
+    width: 58.333333333333336%;
1385
+  }
1386
+  .col-lg-6 {
1387
+    width: 50%;
1388
+  }
1389
+  .col-lg-5 {
1390
+    width: 41.66666666666667%;
1391
+  }
1392
+  .col-lg-4 {
1393
+    width: 33.33333333333333%;
1394
+  }
1395
+  .col-lg-3 {
1396
+    width: 25%;
1397
+  }
1398
+  .col-lg-2 {
1399
+    width: 16.666666666666664%;
1400
+  }
1401
+  .col-lg-1 {
1402
+    width: 8.333333333333332%;
1403
+  }
1404
+  .col-lg-pull-12 {
1405
+    right: 100%;
1406
+  }
1407
+  .col-lg-pull-11 {
1408
+    right: 91.66666666666666%;
1409
+  }
1410
+  .col-lg-pull-10 {
1411
+    right: 83.33333333333334%;
1412
+  }
1413
+  .col-lg-pull-9 {
1414
+    right: 75%;
1415
+  }
1416
+  .col-lg-pull-8 {
1417
+    right: 66.66666666666666%;
1418
+  }
1419
+  .col-lg-pull-7 {
1420
+    right: 58.333333333333336%;
1421
+  }
1422
+  .col-lg-pull-6 {
1423
+    right: 50%;
1424
+  }
1425
+  .col-lg-pull-5 {
1426
+    right: 41.66666666666667%;
1427
+  }
1428
+  .col-lg-pull-4 {
1429
+    right: 33.33333333333333%;
1430
+  }
1431
+  .col-lg-pull-3 {
1432
+    right: 25%;
1433
+  }
1434
+  .col-lg-pull-2 {
1435
+    right: 16.666666666666664%;
1436
+  }
1437
+  .col-lg-pull-1 {
1438
+    right: 8.333333333333332%;
1439
+  }
1440
+  .col-lg-pull-0 {
1441
+    right: 0%;
1442
+  }
1443
+  .col-lg-push-12 {
1444
+    left: 100%;
1445
+  }
1446
+  .col-lg-push-11 {
1447
+    left: 91.66666666666666%;
1448
+  }
1449
+  .col-lg-push-10 {
1450
+    left: 83.33333333333334%;
1451
+  }
1452
+  .col-lg-push-9 {
1453
+    left: 75%;
1454
+  }
1455
+  .col-lg-push-8 {
1456
+    left: 66.66666666666666%;
1457
+  }
1458
+  .col-lg-push-7 {
1459
+    left: 58.333333333333336%;
1460
+  }
1461
+  .col-lg-push-6 {
1462
+    left: 50%;
1463
+  }
1464
+  .col-lg-push-5 {
1465
+    left: 41.66666666666667%;
1466
+  }
1467
+  .col-lg-push-4 {
1468
+    left: 33.33333333333333%;
1469
+  }
1470
+  .col-lg-push-3 {
1471
+    left: 25%;
1472
+  }
1473
+  .col-lg-push-2 {
1474
+    left: 16.666666666666664%;
1475
+  }
1476
+  .col-lg-push-1 {
1477
+    left: 8.333333333333332%;
1478
+  }
1479
+  .col-lg-push-0 {
1480
+    left: 0%;
1481
+  }
1482
+  .col-lg-offset-12 {
1483
+    margin-left: 100%;
1484
+  }
1485
+  .col-lg-offset-11 {
1486
+    margin-left: 91.66666666666666%;
1487
+  }
1488
+  .col-lg-offset-10 {
1489
+    margin-left: 83.33333333333334%;
1490
+  }
1491
+  .col-lg-offset-9 {
1492
+    margin-left: 75%;
1493
+  }
1494
+  .col-lg-offset-8 {
1495
+    margin-left: 66.66666666666666%;
1496
+  }
1497
+  .col-lg-offset-7 {
1498
+    margin-left: 58.333333333333336%;
1499
+  }
1500
+  .col-lg-offset-6 {
1501
+    margin-left: 50%;
1502
+  }
1503
+  .col-lg-offset-5 {
1504
+    margin-left: 41.66666666666667%;
1505
+  }
1506
+  .col-lg-offset-4 {
1507
+    margin-left: 33.33333333333333%;
1508
+  }
1509
+  .col-lg-offset-3 {
1510
+    margin-left: 25%;
1511
+  }
1512
+  .col-lg-offset-2 {
1513
+    margin-left: 16.666666666666664%;
1514
+  }
1515
+  .col-lg-offset-1 {
1516
+    margin-left: 8.333333333333332%;
1517
+  }
1518
+  .col-lg-offset-0 {
1519
+    margin-left: 0%;
1520
+  }
1521
+}
1522
+
1523
+html {
1524
+  font-family: sans-serif;
1525
+  -ms-text-size-adjust: 100%;
1526
+  -webkit-text-size-adjust: 100%;
1527
+}
1528
+body {
1529
+  margin: 0;
1530
+}
1531
+article,
1532
+aside,
1533
+details,
1534
+figcaption,
1535
+figure,
1536
+footer,
1537
+header,
1538
+hgroup,
1539
+main,
1540
+nav,
1541
+section,
1542
+summary {
1543
+  display: block;
1544
+}
1545
+audio,
1546
+canvas,
1547
+progress,
1548
+video {
1549
+  display: inline-block;
1550
+  vertical-align: baseline;
1551
+}
1552
+audio:not([controls]) {
1553
+  display: none;
1554
+  height: 0;
1555
+}
1556
+[hidden],
1557
+template {
1558
+  display: none;
1559
+}
1560
+a {
1561
+  background: transparent;
1562
+}
1563
+a:active,
1564
+a:hover {
1565
+  outline: 0;
1566
+}
1567
+abbr[title] {
1568
+  border-bottom: 1px dotted;
1569
+}
1570
+b,
1571
+strong {
1572
+  font-weight: bold;
1573
+}
1574
+dfn {
1575
+  font-style: italic;
1576
+}
1577
+h1 {
1578
+  font-size: 2em;
1579
+  margin: 0.67em 0;
1580
+}
1581
+mark {
1582
+  background: #ff0;
1583
+  color: #000;
1584
+}
1585
+small {
1586
+  font-size: 80%;
1587
+}
1588
+sub,
1589
+sup {
1590
+  font-size: 75%;
1591
+  line-height: 0;
1592
+  position: relative;
1593
+  vertical-align: baseline;
1594
+}
1595
+sup {
1596
+  top: -0.5em;
1597
+}
1598
+sub {
1599
+  bottom: -0.25em;
1600
+}
1601
+img {
1602
+  border: 0;
1603
+}
1604
+svg:not(:root) {
1605
+  overflow: hidden;
1606
+}
1607
+figure {
1608
+  margin: 1em 40px;
1609
+}
1610
+hr {
1611
+  -moz-box-sizing: content-box;
1612
+  box-sizing: content-box;
1613
+  height: 0;
1614
+}
1615
+pre {
1616
+  overflow: auto;
1617
+}
1618
+code,
1619
+kbd,
1620
+pre,
1621
+samp {
1622
+  font-family: monospace, monospace;
1623
+  font-size: 1em;
1624
+}
1625
+button,
1626
+input,
1627
+optgroup,
1628
+select,
1629
+textarea {
1630
+  color: inherit;
1631
+  font: inherit;
1632
+  margin: 0;
1633
+}
1634
+button {
1635
+  overflow: visible;
1636
+}
1637
+button,
1638
+select {
1639
+  text-transform: none;
1640
+}
1641
+button,
1642
+html input[type="button"],
1643
+input[type="reset"],
1644
+input[type="submit"] {
1645
+  -webkit-appearance: button;
1646
+  cursor: pointer;
1647
+}
1648
+button[disabled],
1649
+html input[disabled] {
1650
+  cursor: default;
1651
+}
1652
+button::-moz-focus-inner,
1653
+input::-moz-focus-inner {
1654
+  border: 0;
1655
+  padding: 0;
1656
+}
1657
+input {
1658
+  line-height: normal;
1659
+}
1660
+input[type="checkbox"],
1661
+input[type="radio"] {
1662
+  box-sizing: border-box;
1663
+  padding: 0;
1664
+}
1665
+input[type="number"]::-webkit-inner-spin-button,
1666
+input[type="number"]::-webkit-outer-spin-button {
1667
+  height: auto;
1668
+}
1669
+input[type="search"] {
1670
+  -webkit-appearance: textfield;
1671
+  -moz-box-sizing: content-box;
1672
+  -webkit-box-sizing: content-box;
1673
+  box-sizing: content-box;
1674
+}
1675
+input[type="search"]::-webkit-search-cancel-button,
1676
+input[type="search"]::-webkit-search-decoration {
1677
+  -webkit-appearance: none;
1678
+}
1679
+fieldset {
1680
+  border: 1px solid #c0c0c0;
1681
+  margin: 0 2px;
1682
+  padding: 0.35em 0.625em 0.75em;
1683
+}
1684
+legend {
1685
+  border: 0;
1686
+  padding: 0;
1687
+}
1688
+textarea {
1689
+  overflow: auto;
1690
+}
1691
+optgroup {
1692
+  font-weight: bold;
1693
+}
1694
+table {
1695
+  border-collapse: collapse;
1696
+  border-spacing: 0;
1697
+}
1698
+td,
1699
+th {
1700
+  padding: 0;
1701
+}
1702
+@-ms-viewport {
1703
+  width: device-width;
1704
+}
1705
+.visible-xs,
1706
+.visible-sm,
1707
+.visible-md,
1708
+.visible-lg {
1709
+  display: none !important;
1710
+}
1711
+@media (max-width: 767px) {
1712
+  .visible-xs {
1713
+    display: block !important;
1714
+  }
1715
+  table.visible-xs {
1716
+    display: table;
1717
+  }
1718
+  tr.visible-xs {
1719
+    display: table-row !important;
1720
+  }
1721
+  th.visible-xs,
1722
+  td.visible-xs {
1723
+    display: table-cell !important;
1724
+  }
1725
+}
1726
+@media (min-width: 768px) and (max-width: 991px) {
1727
+  .visible-sm {
1728
+    display: block !important;
1729
+  }
1730
+  table.visible-sm {
1731
+    display: table;
1732
+  }
1733
+  tr.visible-sm {
1734
+    display: table-row !important;
1735
+  }
1736
+  th.visible-sm,
1737
+  td.visible-sm {
1738
+    display: table-cell !important;
1739
+  }
1740
+}
1741
+@media (min-width: 992px) and (max-width: 1199px) {
1742
+  .visible-md {
1743
+    display: block !important;
1744
+  }
1745
+  table.visible-md {
1746
+    display: table;
1747
+  }
1748
+  tr.visible-md {
1749
+    display: table-row !important;
1750
+  }
1751
+  th.visible-md,
1752
+  td.visible-md {
1753
+    display: table-cell !important;
1754
+  }
1755
+}
1756
+@media (min-width: 1200px) {
1757
+  .visible-lg {
1758
+    display: block !important;
1759
+  }
1760
+  table.visible-lg {
1761
+    display: table;
1762
+  }
1763
+  tr.visible-lg {
1764
+    display: table-row !important;
1765
+  }
1766
+  th.visible-lg,
1767
+  td.visible-lg {
1768
+    display: table-cell !important;
1769
+  }
1770
+}
1771
+@media (max-width: 767px) {
1772
+  .hidden-xs {
1773
+    display: none !important;
1774
+  }
1775
+}
1776
+@media (min-width: 768px) and (max-width: 991px) {
1777
+  .hidden-sm {
1778
+    display: none !important;
1779
+  }
1780
+}
1781
+@media (min-width: 992px) and (max-width: 1199px) {
1782
+  .hidden-md {
1783
+    display: none !important;
1784
+  }
1785
+}
1786
+@media (min-width: 1200px) {
1787
+  .hidden-lg {
1788
+    display: none !important;
1789
+  }
1790
+}
1791
+.visible-print {
1792
+  display: none !important;
1793
+}
1794
+@media print {
1795
+  .visible-print {
1796
+    display: block !important;
1797
+  }
1798
+  table.visible-print {
1799
+    display: table;
1800
+  }
1801
+  tr.visible-print {
1802
+    display: table-row !important;
1803
+  }
1804
+  th.visible-print,
1805
+  td.visible-print {
1806
+    display: table-cell !important;
1807
+  }
1808
+}
1809
+@media print {
1810
+  .hidden-print {
1811
+    display: none !important;
1812
+  }
1813
+}
1814
+* {
1815
+  -webkit-box-sizing: border-box;
1816
+  -moz-box-sizing: border-box;
1817
+  box-sizing: border-box;
1818
+}
1819
+*:before,
1820
+*:after {
1821
+  -webkit-box-sizing: border-box;
1822
+  -moz-box-sizing: border-box;
1823
+  box-sizing: border-box;
1824
+}
1825
+html {
1826
+  font-size: 62.5%;
1827
+  -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
1828
+}
1829
+body {
1830
+  font-family: "Open Sans", Helvetica, Arial, sans-serif;
1831
+  font-size: 12px;
1832
+  line-height: 1.66666667;
1833
+  color: #333333;
1834
+  background-color: #ffffff;
1835
+}
1836
+input,
1837
+button,
1838
+select,
1839
+textarea {
1840
+  font-family: inherit;
1841
+  font-size: inherit;
1842
+  line-height: inherit;
1843
+}
1844
+a {
1845
+  color: #0099d3;
1846
+  text-decoration: none;
1847
+}
1848
+a:hover,
1849
+a:focus {
1850
+  color: #00618a;
1851
+  text-decoration: underline;
1852
+}
1853
+a:focus {
1854
+  outline: thin dotted;
1855
+  outline: 5px auto -webkit-focus-ring-color;
1856
+  outline-offset: -2px;
1857
+}
1858
+figure {
1859
+  margin: 0;
1860
+}
1861
+img {
1862
+  vertical-align: middle;
1863
+}
1864
+.img-responsive {
1865
+  display: block;
1866
+  max-width: 100%;
1867
+  height: auto;
1868
+}
1869
+.img-rounded {
1870
+  border-radius: 1px;
1871
+}
1872
+.img-thumbnail {
1873
+  padding: 4px;
1874
+  line-height: 1.66666667;
1875
+  background-color: #ffffff;
1876
+  border: 1px solid #dddddd;
1877
+  border-radius: 1px;
1878
+  -webkit-transition: all 0.2s ease-in-out;
1879
+  transition: all 0.2s ease-in-out;
1880
+  display: inline-block;
1881
+  max-width: 100%;
1882
+  height: auto;
1883
+}
1884
+.img-circle {
1885
+  border-radius: 50%;
1886
+}
1887
+hr {
1888
+  margin-top: 20px;
1889
+  margin-bottom: 20px;
1890
+  border: 0;
1891
+  border-top: 1px solid #eeeeee;
1892
+}
1893
+.sr-only {
1894
+  position: absolute;
1895
+  width: 1px;
1896
+  height: 1px;
1897
+  margin: -1px;
1898
+  padding: 0;
1899
+  overflow: hidden;
1900
+  clip: rect(0, 0, 0, 0);
1901
+  border: 0;
1902
+}
1903
+.clearfix:before,
1904
+.clearfix:after,
1905
+.form-horizontal .form-group:before,
1906
+.form-horizontal .form-group:after,
1907
+.container:before,
1908
+.container:after,
1909
+.container-fluid:before,
1910
+.container-fluid:after,
1911
+.row:before,
1912
+.row:after {
1913
+  content: " ";
1914
+  display: table;
1915
+}
1916
+.clearfix:after,
1917
+.form-horizontal .form-group:after,
1918
+.container:after,
1919
+.container-fluid:after,
1920
+.row:after {
1921
+  clear: both;
1922
+}
1923
+.center-block {
1924
+  display: block;
1925
+  margin-left: auto;
1926
+  margin-right: auto;
1927
+}
1928
+.pull-right {
1929
+  float: right !important;
1930
+}
1931
+.pull-left {
1932
+  float: left !important;
1933
+}
1934
+.hide {
1935
+  display: none !important;
1936
+}
1937
+.show {
1938
+  display: block !important;
1939
+}
1940
+.invisible {
1941
+  visibility: hidden;
1942
+}
1943
+.text-hide {
1944
+  font: 0/0 a;
1945
+  color: transparent;
1946
+  text-shadow: none;
1947
+  background-color: transparent;
1948
+  border: 0;
1949
+}
1950
+.hidden {
1951
+  display: none !important;
1952
+  visibility: hidden !important;
1953
+}
1954
+.affix {
1955
+  position: fixed;
1956
+}
1957
+
1958
+
1959
+
1960
+.alert {
1961
+  border-width: 2px;
1962
+  padding-left: 34px;
1963
+  position: relative;
1964
+}
1965
+.alert .alert-link {
1966
+  color: #0099d3;
1967
+}
1968
+.alert .alert-link:hover {
1969
+  color: #00618a;
1970
+}
1971
+.alert > .pficon,
1972
+.alert > .pficon-layered {
1973
+  font-size: 20px;
1974
+  position: absolute;
1975
+  left: 7px;
1976
+  top: 7px;
1977
+}
1978
+.alert .pficon-info {
1979
+  color: #72767b;
1980
+}
1981
+.alert-dismissable .close {
1982
+  right: -16px;
1983
+  top: 1px;
1984
+}
1985
+
1986
+
1987
+.login-pf {
1988
+  height: 100%;
1989
+}
1990
+.login-pf #brand {
1991
+  position: relative;
1992
+  top: -70px;
1993
+}
1994
+.login-pf #brand img {
1995
+  display: block;
1996
+  height: 18px;
1997
+  margin: 0 auto;
1998
+  max-width: 100%;
1999
+}
2000
+@media (min-width: 768px) {
2001
+  .login-pf #brand img {
2002
+    margin: 0;
2003
+    text-align: left;
2004
+  }
2005
+}
2006
+.login-pf #badge {
2007
+  display: block;
2008
+  margin: 20px auto 70px;
2009
+  position: relative;
2010
+  text-align: center;
2011
+}
2012
+@media (min-width: 768px) {
2013
+  .login-pf #badge {
2014
+    float: right;
2015
+    margin-right: 64px;
2016
+    margin-top: 50px;
2017
+  }
2018
+}
2019
+.login-pf body {
2020
+  background: #1a1a1a url("../img/bg-login.png") repeat-x 50% 0;
2021
+  background-size: auto;
2022
+}
2023
+@media (min-width: 768px) {
2024
+  .login-pf body {
2025
+    background-size: 100% auto;
2026
+  }
2027
+}
2028
+.login-pf .container {
2029
+  background-color: transparent;
2030
+  clear: right;
2031
+  color: #fff;
2032
+  padding-bottom: 40px;
2033
+  padding-top: 20px;
2034
+  width: auto;
2035
+}
2036
+@media (min-width: 768px) {
2037
+  .login-pf .container {
2038
+    bottom: 13%;
2039
+    padding-left: 80px;
2040
+    position: absolute;
2041
+    width: 100%;
2042
+  }
2043
+}
2044
+.login-pf .container [class^='alert'] {
2045
+  background: transparent;
2046
+  color: #fff;
2047
+}
2048
+.login-pf .container .details p:first-child {
2049
+  border-top: 1px solid #474747;
2050
+  padding-top: 25px;
2051
+  margin-top: 25px;
2052
+}
2053
+@media (min-width: 768px) {
2054
+  .login-pf .container .details {
2055
+    border-left: 1px solid #474747;
2056
+    padding-left: 40px;
2057
+  }
2058
+  .login-pf .container .details p:first-child {
2059
+    border-top: 0;
2060
+    padding-top: 0;
2061
+    margin-top: 0;
2062
+  }
2063
+}
2064
+.login-pf .container .details p {
2065
+  margin-bottom: 2px;
2066
+}
2067
+.login-pf .container .form-horizontal .control-label {
2068
+  font-size: 13px;
2069
+  font-weight: 400;
2070
+  text-align: left;
2071
+}
2072
+.login-pf .container .form-horizontal .form-group:last-child,
2073
+.login-pf .container .form-horizontal .form-group:last-child .help-block:last-child {
2074
+  margin-bottom: 0;
2075
+}
2076
+.login-pf .container .help-block {
2077
+  color: #fff;
2078
+}
2079
+@media (min-width: 768px) {
2080
+  .login-pf .container .login {
2081
+    padding-right: 40px;
2082
+  }
2083
+}
2084
+.login-pf .container .submit {
2085
+  text-align: right;
2086
+}
2087
+.ie8.login-pf #badge {
2088
+  background: url('../img/logo.png') no-repeat;
2089
+  height: 44px;
2090
+  width: 137px;
2091
+}
2092
+.ie8.login-pf #badge img {
2093
+  width: 0;
2094
+}
2095
+.ie8.login-pf #brand {
2096
+  background: url('../img/brand-lg.png') no-repeat center;
2097
+  background-size: cover auto;
2098
+}
2099
+@media (min-width: 768px) {
2100
+  .ie8.login-pf #brand {
2101
+    background-position: 0 0;
2102
+  }
2103
+}
2104
+.ie8.login-pf #brand img {
2105
+  width: 0;
2106
+}
2107
+
2108
+
2109
+
2110
+@font-face {
2111
+  font-family: 'PatternFlyIcons-webfont';
2112
+  src: url(data:application/font-woff;charset=utf-8;base64,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) format('woff');
2113
+  font-weight: normal;
2114
+  font-style: normal;
2115
+}
2116
+[class*="-exclamation"] {
2117
+  color: #fff;
2118
+}
2119
+[class^="pficon-"],
2120
+[class*=" pficon-"] {
2121
+  display: inline-block;
2122
+  font-family: 'PatternFlyIcons-webfont';
2123
+  font-style: normal;
2124
+  font-variant: normal;
2125
+  font-weight: normal;
2126
+  line-height: 1;
2127
+  speak: none;
2128
+  text-transform: none;
2129
+
2130
+  -webkit-font-smoothing: antialiased;
2131
+  -moz-osx-font-smoothing: grayscale;
2132
+}
2133
+.pficon-layered {
2134
+  position: relative;
2135
+}
2136
+.pficon-layered .pficon:first-child {
2137
+  position: absolute;
2138
+  z-index: 1;
2139
+}
2140
+.pficon-layered .pficon:first-child + .pficon {
2141
+  position: relative;
2142
+  z-index: 2;
2143
+}
2144
+.pficon-warning-exclamation:before {
2145
+  content: "\e60d";
2146
+}
2147
+.pficon-screen:before {
2148
+  content: "\e600";
2149
+}
2150
+.pficon-save:before {
2151
+  content: "\e601";
2152
+}
2153
+.pficon-ok:before {
2154
+  color: #57a81c;
2155
+  content: "\e602";
2156
+}
2157
+.pficon-messages:before {
2158
+  content: "\e603";
2159
+}
2160
+.pficon-info:before {
2161
+  content: "\e604";
2162
+}
2163
+.pficon-help:before {
2164
+  content: "\e605";
2165
+}
2166
+.pficon-folder-open:before {
2167
+  content: "\e606";
2168
+}
2169
+.pficon-folder-close:before {
2170
+  content: "\e607";
2171
+}
2172
+.pficon-error-exclamation:before {
2173
+  content: "\e608";
2174
+}
2175
+.pficon-error-octagon:before {
2176
+  color: #c90813;
2177
+  content: "\e609";
2178
+}
2179
+.pficon-edit:before {
2180
+  content: "\e60a";
2181
+}
2182
+.pficon-close:before {
2183
+  content: "\e60b";
2184
+}
2185
+.pficon-warning-triangle:before {
2186
+  color: #eb7720;
2187
+  content: "\e60c";
2188
+}
2189
+.pficon-user:before {
2190
+  content: "\e60e";
2191
+}
2192
+.pficon-users:before {
2193
+  content: "\e60f";
2194
+}
2195
+.pficon-settings:before {
2196
+  content: "\e610";
2197
+}
2198
+.pficon-delete:before {
2199
+  content: "\e611";
2200
+}
2201
+.pficon-print:before {
2202
+  content: "\e612";
2203
+}
2204
+.pficon-refresh:before {
2205
+  content: "\e613";
2206
+}
2207
+.pficon-running:before {
2208
+  content: "\e614";
2209
+}
2210
+.pficon-import:before {
2211
+  content: "\e615";
2212
+}
2213
+.pficon-export:before {
2214
+  content: "\e616";
2215
+}
2216
+.pficon-history:before {
2217
+  content: "\e617";
2218
+}
2219
+.pficon-home:before {
2220
+  content: "\e618";
2221
+}
2222
+.pficon-remove:before {
2223
+  content: "\e619";
2224
+}
2225
+.pficon-add:before {
2226
+  content: "\e61a";
2227
+}
2228
+
2229
+.login-pf {
2230
+  background-color: #1a1a1a;
2231
+}
2232
+@media (min-width: 768px) {
2233
+  .login-pf {
2234
+    background-image: url("../img/bg-login-2.png");
2235
+    background-position: 100% 100%;
2236
+    background-repeat: no-repeat;
2237
+    background-size: 30%;
2238
+  }
2239
+}
2240
+@media (min-width: 992px) {
2241
+  .login-pf {
2242
+    background-size: auto;
2243
+  }
2244
+}
2245
+.login-pf #badge {
2246
+  margin-bottom: 50px;
2247
+}
2248
+.login-pf body {
2249
+  background: transparent;
2250
+}
2251
+@media (min-width: 768px) {
2252
+  .login-pf body {
2253
+    background-image: url("../img/bg-login.png");
2254
+    background-repeat: no-repeat;
2255
+    background-size: 30%;
2256
+    height: 100%;
2257
+  }
2258
+}
2259
+@media (min-width: 992px) {
2260
+  .login-pf body {
2261
+    background-size: auto;
2262
+  }
2263
+}
2264
+.login-pf #brand {
2265
+  top: -30px;
2266
+}
2267
+@media (min-width: 768px) {
2268
+  .login-pf #brand {
2269
+    top: -40px;
2270
+  }
2271
+  .login-pf #brand + .alert {
2272
+    margin-top: -20px;
2273
+  }
2274
+}
2275
+.login-pf .container {
2276
+  padding-top: 0;
2277
+}
2278
+@media (min-width: 992px) {
2279
+  .login-pf .container {
2280
+    bottom: 20%;
2281
+    padding-right: 120px;
2282
+  }
2283
+}
2284
+@media (max-width: 767px) {
2285
+  .login-pf #badge {
2286
+    margin-bottom: 20px;
2287
+  }
2288
+  .login-pf #brand {
2289
+    display: none;
2290
+  }
2291
+}
2292
+@media (min-width: 768px) {
2293
+  .login-pf {
2294
+    background-image: none;
2295
+  }
2296
+  .login-pf body {
2297
+    background-image: none;
2298
+  }
2299
+}
2300
+      </style>
2301
+  </head>
2302
+  <body>
2303
+    <span id="badge">
2304
+      <img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQQAAAEECAYAAADOCEoKAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAHMwAABzMBXAgunAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAACAASURBVHic7Z15fBvVtcd/50pymo0kECdQ9p1Ak7CEQKGsIaENhZY+DKVQSkNR7ZlRiGleSxeKoNDSljYJ0oyDKWEpFEoohVfWFB5L2ROWJOwQ9gfECiQhG7GkOe8PyWDkmdFIGsmWfb6fjz+QuXfuPR5Lv7nLuecAgiAIgiAIgiAIgiAIgiAIgiAIgiAIgiAIgiAIgiAIgiAIgiAIgiAIgiAIgiAIgiAIgiAIgiAIgiAIgiAIgiAIgiAIgiAIgiAIgiAIgiAIgiAIAw7qbQPqnVgsdlg2m51MRI0AQEQfENEz2Wz2Wcuy1ve2ff2RGTNmDB88ePB+zHwAEY3NX+4goieTyeSjvWpcnSOCUCbnnHPODul0+ioiOsalymYAdxHRdaNHj74rHo931tK+/kYsFhvEzN+0bft0IpoOoMGpHhEtsm37R5ZlvVtjE/sFIghloGna1kT0BIAdfd7yPjMnmPmKtra21dW0rb/R2tq6ZWdnZzOAGICtfd72plLqoEQikaqiaf2SUG8bUI9Mnjx5DoCjS7hlOBEdQ0T65MmTh4wfP37Js88+KyMGD2Kx2BYHHnjgBdls9iYAXwcwrITbRzHzsMWLF99ZJfP6LTJCKJHZs2cP3bRpUwrA4AqaWQngwsbGxivj8XgmINP6BfF4PNzR0dFMRL8G0FhBU+s3bNjQeM0113walG0DAdXbBtQbGzduPAyViQEAjAVgpVKpxbquHxSAWf0CXde/mkqlniaiBCoTAwAYNmTIkK8GYddAItzbBtQbSqndmTmo5vYF8Jiu61ek0+mft7e3rw2q4Xpi1qxZI9Pp9KUAzkaALyml1J4AHgiqvYGAjBBKhJnHBNykAtASiUSWtbS0HBFw230eTdOOSqfTywD8GAF/Hqvwt+r3yAihRJiZiKqy9LKDUuoBXdevTKfTre3t7Rur0UlfIb8W82fkRgVVeaDMLGtkJSIjhBIhIq9FKmbmq4hoim3bkwAcz8yXAHjZb/MAog0NDQ9rmrZbxcb2UXRd32PTpk2PAIjCvxi8RES/AXB8/tkeA+BqAK7zN6XUpoqNHWDICKF0PvQoi1uWdVHBtTsA/MowjCMBzGbm6SjyJch74D1tGEY0mUz+vTJz+xaGYXyPmecDGO6jOiP3/C4zTfNhh/L7DcN4h5kvcLyZeWUFpg5IRBBKhJnfc5kybGxoaPij233JZPJBAA/quv5VAH8CUGwFfAtmvknTtCM3btzYWu/bZ62trYM7OzsvZ+Yf+bzlUaXUuYlE4imvSp2dnX+IRCI/A/Alh+L3SjZ0gCNThhKxbXu5S9Fzc+bMKTpENU3zcdM0D2XmHwD4qFh9ImoeOnToQ4ZhfLlUW/sKsVhsu3Q6/R8AfsRgFTOfbprm14qJAQDk11qWOZWFw+HnSzR1wCOCUCLz58//Pzh8kYmoFJdktizrunA4vA+AhT7qT2bmpzRNO7CEPvoELS0tBzPzU8x8gI/qNyml9rYs64ZS+mBmJ2H9cN68eTJlKBERhPJ4vPACM48qtZF58+atNE3zZCKaAaDYychtieghTdNOLbWf3kLX9e8rpR5g5m2KVF0H4AzTNE8t5/wBEfVwFGPmJ0ptRxBBKAsi+o/D5R3KbS+ZTF6dXzl/rkjVwUR0g6ZpF6Bvu52Truu/AXAdnOf2n8HMz4ZCoQNM0/xrBf31OPTk8jcSiiCCUAa2bTt5v20TjUYj5bbZ1tb2SjqdPhTATUWqEhHFDcO4Jh6POx4B7k1isdggXdf/CuBXPqrfMGjQoEMvv/zy18rtL/8MdnUoerDcNgcyffkt06fRdf0NADsXXD7YNM0nK23bMIwoMycBeAoMMz9h2/a35s+f31Fpn0FwzjnnjM1kMrcDKHY+o5OIYslksr3SPg3DOJSZHym4vMI0zX7rx1FNZIRQPv9yuDYliIaTyWQ7M58AwHOhkogODoVCDzQ3N28bRL+VoGna9plM5kEUF4OPAXwzCDEAAGZ2eua3B9H2QEQEoUyI6GaHy8cG1b5lWfcopSYAWFyk6t7hcHhxc3PzV4Lqu1QMwxhPRE8B2KtI1aey2ewE0zT/HWD3Ts/c6W8j+EAEoUxWrlz5BHJxDbpzsKZppQTy8CSRSLy3adOmKcx8t1c9Zt4mHA4v6g1RaGlpmcDMi1A8mtG/mHlKfts2EGKx2BYACrdi329sbCwmooILIghlsnDhwiwR/aPgcgOAk4LsZ8GCBetSqdTxADyH2My8TSgUekTTtK8F2b8Xuq5PUUo9iiJiwMzzOzo6Tgw66CwzN6HnOsst8XjcDrKfgYQIQmX0cCoiotMD72Thwqxpms1ENKdI1RFEdFf+3ERV0XV9CoDbUDy02R8ty9IWLlyYrYIZ3y+84DKVE3wiuwwVouv6MgDju11ipdTuiURiRTX60zTNIKJ58BbzTiI6PZlM+vGCLBld108GcD28d0FsZo5ZlmVVw4aWlpY9lVJfOEVKRE8nk8lJ1ehvoCAjhArJn9zrDjFzjzdXUFiWlSSi7wNIe1RrYOYbDcM4K+j+NU07G8Df4C0GnwI4qVpiAAD5Z/AFbNu+olr9DRREECokk8ncAGBD92vMfEZTU1PVIlonk8m/AWgC4HWYKsTM7YZhRIPqV9M0jYjmwzta9yYATaZp/jOofguJx+NhB0FY9+mnnxZz6hKKIIJQIe3t7WuJ6G8Fl3ceO3bst6vZr2mat+cTlmzwqKaYeX7+rV4Ruq63EFES3p+Z9QC+bprmHZX258WqVav+CwWu4kT01wULFqyrZr8DARGEACCiuSiI3MPM51W732Qy+aBt20fA24GJiOgKXddbyu1H0zQNgAnvNaePARzhEsgkUJj5p4WXbNtOVrvfgYAIQgAkEokXARSerpvU0tJycLX7bmtre1op9S14n5YkAAnDME4ptX3DML5HRJfDWww+IaITTNN8ptT2y7DnUAD7d7/GzI9YlvVStfseCIggBAQz/67wWigU+u9a9J1IJP4D4Ajk3tJuhJj5BsMwvue3XU3TTmPmv8J7zeAjpdSRtUqyysw9nqlSqsezF8pDth2DgwzDWMbM3b0FM0qpvaq1BVmIpmmHENHdALbwqNYJ4ETTNO/yakvX9W8C+AdckqrmWQvg2CAOdPlh5syZu2ez2ZfwRYF6zjTN/eERbFXwj4wQgoMBFAZYDdu2XXitaliW9RgzHwHv0GwNAP6padrxbhXyZbfCWwxWKaWOqJUYAEAmk/kNeo5WLoKIQWCIIATIypUrb2XmwrP9pxiGUezQT2BYlvWcUmo6gDUe1RqI6CZd1w8vLGhpaTmCiG6Ct5/BaiL6RiKRWFqpvX7RdX0fImoquPxyY2OjnGwMEBGEAMm7515ccDkE4JJa2pFIJJ5i5mnIhSZzYwiAu7u7OWuadpRS6u58mRtrlFJHJZPJJcFY65tLUPB5JaLfyLmFYJE1hICJRqORSCTyGoAdu122lVL71/KNCgCapn2diG6H99B/jVLqaNu2CcD/AhjhUbcTwDcDPr5cFF3X9wewBF/8vL7R2Ni4p2TPDhYZIQRMe3t7mpn/UHBZ1XItoQvLsu4hojMAeB0sGmnb9t0A7oG3GGSZ+bRaiwEAENFFKHh5EdHvRQyCRwShCmQymSsd1hJO0HV9aq1tSSaTfyeiMwF4Da3Hwjv9us3MP7As65ZAjfOBpmnTmPm4gssvrly58qpa2zIQEEGoAu3t7WkATunFfhuPx2v+zJPJ5PXM/MsKmjiv1FwJQRCPxxUROfkYXFil49QDHhGEKmFZ1o0AHiu4PGnVqlU/7CV7Ls0nSy0JIrrQNE3XFHXVpKOj4ywUeCUCeNQ0TYl5UCVEEKqIUqrHeQZm/m0+9FfNSSaTv/YRZKU7f04mk/Fq2eNFLBbbgogKd2wYwE96w56BgghCFcm7FBd6BI5h5nN7wx4AWLly5X8DeNFH1Rc6OjoKDxHVDNu2fwJgTMHlO2rpCDUQEUGoMtls9mcoWOVn5tm9FTq9sbHxxwD29lF1n7Fjx1Z8bLocYrHYdgBmF1zOAvh5L5gzoBBBqDLz589/npmvKbg8NBQK/aDWthiG8Y18+DVfMHPCMIzAQsv7xbbtM9HTOepq0zRfqLUtAw0RhBpg2/YF6Hk8eVAtbciHS/87gHAJt4WZ+eZeCO9e+GzWE5HTro0QMCIINSCfi+BsAJsBgIg+ALCgVv1Ho9FtlFL/AjC8jNu3CIVCd5xzzjljg7bLDSJakH9GQO6ZnZVMJt+vVf8DGXFdriGGYXyZmccx85NB5yhwIxqNDolEIg8B8IpG3LXG4RX34MmGhoaj5syZ4xXHMTA0TRtGRAel0+kX29vbPyh+hxAEIgj9mHg8rlKp1C0ATixS1WBmIqJEkXq3NDY2niIHivovMmXox6RSqYtRRAyYeZ5pmqZlWUkAlxdp8qRUKnVhYAYKfQ4RhH6KYRg/RJFtOma+bcyYMZ/5RDQ2NrYy821Fmv6VpmmnBmGj0PeQKUM/RNO0A4noYQBf8qj2VENDw5GFawKtra2DOzs7H0LPJKrd+ZSIDuuFmAhClRFB6Gc0NzePCYVCSwBs71HtvXQ6PdltsS6/+PkkgO082nhHKTUpkUikKrFX6FvIlKEfEY/HG0Kh0K3wFoMNSqkTvFbu81t834J3EpgdbNu+NR6PewVfEeoMEYR+RCqV+i2AQ73qEFE0kUg8W6ytfI6FYsldvpZfuBT6CVXLPyjUlny+hcu86jDzZaZp/tlvm4sXL1520EEHjQDwVY9qhx544IGvLl68+Hm/7Qp9l1LcWIUSicVigzKZzE6hUGgX27Z3BrCzUmoX27Z3JqJdAAxl5sczmcwplTjf5N2Sr/Sqw8z3pVKpktPLjR49+qepVGoigKPd6hDRXwzDeD6ZTC4vtf0umpubtw2FQn8HcBCA9cz8JhG9wcxvAngTwBvM/GY4HH4rkUhsLrcfwRtZVAyAWbNmjcxms+Nt254AYAIzjyOinQFsCx/PmJmvsizrR+X0HYvFtshms0uIaHePam8BmGSaple+Blei0ejoSCSyBF8MHFvIq0qpAxOJxCfl9KHr+gIAfoLH2ADeB/AGgJeZeWkoFFq+efPmZe3t7WvL6Vv4HBkhlEBTU1No7NixuzPzBAATAYwHMCGdTn/hi0JUms4S0bgyTSLbthcUEYONzHyiZVlliQEAtLe3r4rFYifatv0ogMEu1fawbfsvAE4usxs/R7KB3LrXdvmfw4kItm0jEolA1/W3ACxj5uVKqaXZbHbZqlWrXpdwa/6REYIH8Xg8nEqlDgRweP7na/BOk1Yu55umWfLinKZprUTkuSZARKclk8nCdPVloWnaaUR0fZH+ZiWTSd9HrLu1fQERxcs2zp21RPQIgIez2ezDY8eOXSLRmt0RQehGLBYbBGByPsX64QAOATC0Cl2lAbzHzCsA3JNKpeaW+hbL53F8EN4Zlv5smmagIccMw/gzM7d6VEnbtn14W1tbYTZsT5qamkJjxow5l5mnEdGuyI0AvH63cllPRI8BeNi27YdCodBiWZP4nAEvCLFYbDtmnp4P9T0FwQgAIzdvf5WI3gHwNoC3bdt+KxQKvfXhhx9+UMkwNhaLNdq2/Qw8HIeY+T+ZTGZKPgK0K0e+iS8NWocLbYDTwxF/cGd86lU/n4jmAXhvb76bTqf3b29vX+X5i3jQ1NQUamxs/DKAHZVSOyG3frEjgB2ZeY/8/wfx+d0A4D5mvtO27bvyR9UHLANSEPKLZKcw8+lEdHAFTWUArCCiF5j5ZWZ+kYheSqfTL7e3t28Myt7u5L8o9xDRMR7VVhLR/sViCBz5AMKRrXAbAccBAAN3pj/Ctx88Cp5D6rwn4zPI5XNw497Gxsbp1ToZOXv27KEbNmzYKxQKjWPmvQHsBWAfALugsrWxx5n5eiL6e7mLsPXMgBGE/FHgrwOIApiO0oejWQCvENESAEuYeUlDQ8NztYoP0IWu6xcC+LVHlQwRTU0mkw8Wa2vaMlyB3PPoTvuiCfhxsXsNwziamRfBw5eFmX9tWVbJod8rIX8WY18imsTMByIXB2JPlO6El2bmO5VS7aNHj753oBz57veCEI1GR0QiEQ1AM4AdSrg1C+BJZl5ERA8w8zO1CmriRktLyxFKqfvh7VB2nmmavy/W1tRl+CkBzvUYP100EUVzMei6/nMAv/WokmXmIy3LeqRYW9VkxowZwwcNGrR/KBQ6yrbtY4noQJTmlPc2gDalVFu526r1Qr8VhNbW1i03b958LhEZ8M5Z2J33ANxFRIvC4fD9c+fO9UqpXlNaW1u37OzsfA7e5xRuN03zROTWMFw5djm+z4xr4f73Zyac8e/x8NxRAEC6rt8O4HiPOm9HIpF9+9KzbGlpGUVEU5RS05h5OnL+In5YQ0SJbDY7p62tbXU1bewt+p0gxGKxQbZt6wB+CWBLH7dsIKJ/MPN1jY2ND/TVoaGu67cA+C+PKq9HIpEDi33xpi7FFCLcBe+M0ADQyYzp/56I+70qtbS0jFJKLUFu7u7GzaZpnlKkv14hHo+rjz766OhsNvsDIvoOekZ7duIjABc3NjZa8Xi8s8om1pR+JQiGYezFzDch5zRUjBeY+TIAt/T2VKAYmqadTUTtHlU2AzgkfyDJlWnLsT0YS9AzAYobK0OMSXdPxHtelQzDmMTMj8JDZIhoRjKZvNpnv73CjBkzhg8ePLgJuZwQRZ3FmPlZIvquaZqvVt+62tBvBEHTtO8Q0XUovm34DjP/YsyYMTf21dFAd/IitwQev5cfZ6Djl2DI5gY8Bn9i+TmM5walcei/JsFz10TX9XMB/MmjynoAB9TDlye/k3NaPpWc1xQNyP1ep5umeXsNTKs6/UIQdF2fAaAdxReK5jY0NPyi1jsD5RKLxQZls9nHiWg/j2p3mKZ5AoqsG0xbhusBnFamKdcvmoDvF6lDmqbdSUTfcK1A9PTo0aMPqZdhdj5i9aUAYkWqZgDMME3zrzUwq6rUfTwEwzCOBnAFiogBEbWYptlaL2IAALZt/7aIGLyfTqd/iCJiMHUZdJQvBgBw+rSlRWMjsG3bZ3bLp9CzAvMB9RQ/ob29faNpmjOJaGaRqmEAV+m6fngt7KomdT1CiMfjDalUagW8Q31VdJqwt4jFYsfYtr0I7n8jm5mPsSzrAa92jl2Kw5hwPyp3A04TY8q9E/Efr0q6rk8BsAjuLxtfdvc1DMO4lpnPKFLtnXQ6vVsx79C+TF2PEFatWnUyiogBANi2PbcG5gRGNBodYdv2VfAW7N8W+1Id/Ry2ZcLNCOZMQIQJN099Bl/2qmSa5v1w82/IoYjo6lgsVo1DYlWj2CGyPDtEIhGvnaA+T10LAjNP9lPPtu26ckGNRCJz4e1E9VhjY6NnfoSmF9AQVlgIYOsATduawljY9IL3lmVjY+OvATzuUWXHbDbrO3JTH8HvZ+igqlpRZepaEOAzV2E4HK55BuNyMQzjBABnelT5hIhOL3aEd62Ni+Ed+qxcDlmTxUVeFeLxeEYp9X0A69zqENFZLS0txwVuXZXIZrOui6UF1NXIp5B6F4SX/VQiooubm5v9eqP1GtFodDQze/kbdG0xvulV5+vL8R0w/jtY67rZAPxs6nLvjFCJRGIFEXkdk0YoFLpS1/WtgrUueHRd35GIPEWwCyJ6qdr2VJO6FgRm/hvgfTIvz7ZKqQcMw9ir2jZVQiQSaYPHCUIiurWYc8+0pdjZZlwVuHGFtjAWTFuKnb3qJJPJq7wyQTHzNsxsBm9dcOi6vg+A/4W/qVcaQCDBaHqLuhYEy7LeBfAHP3WJaHdmXmwYxuxoNFqNwBsVkU+PdpJbORF90NnZ6XkKsekFNDDh7wBGBm2fAyOhcFOx9YRMJnM2gA/dyonoFF3Xyw27VjXi8XiDpmnnAXgS3m7Z3fldvaetr2tBAIB0Oh0HcI/P6sOY+Y+RSORVXdd/dOaZZ3qlOqsZmqZtTURJjyps2/aMYgFH1mZxPnmnYAsWxuS1WZzvVSVv81nw9pWwmpub/bpTV5XW1tbBmqb9OJVKvUZEv4P/gDl3NDY21vSodzWoe0Fob29Pb9iw4UQAd5Rw204Arhw6dOj7hmEkYrFYae68wTMXHgexiMi0LMuP6BnBmeQbrVgF0zTvIqI2jypbhUKhOQHaVDKxWGw/XdeTnZ2d7xPRfJR2VP52pdRJ/SFWY107JnUnHwDlFwDiKC8BzRIANxLRXclk0tdiZRDouj4dwJ1u5cz8WiaT2ddPBKbpL2LHbAa7gzHCJgwnwnC2MRyUX/lmjCCCAjCEgEGc808YVtDMegLSnDswtZEZNghrAYAIa4mxzgbWK8Y6ENaGwnjtrr3xdjHb8m7ASwHs5vG7fsOn8AWCpmnjAExXSp3KzAeU0USGmc+3LOv3KOItWi/0G0HooqWl5QClVDuA/Sto5g0AdxPRnZFI5MFquTvPnj176KZNm16Ae74Dm5mP6O0AI0GRd+19AO4j0zfT6fRXqhV+Lh9N6SgiOo6ZvwF4L4oWYTEzRy3Lei4o+/oC/U4QgFz49I6Ojh8S0a9Q2tDPiTSA54joSWZ+yrbtp9ra2l5FAG8EXdf/BOBct3JmnmdZ1qxK++lL6Lp+OTwOCzHzZZZlBbFlSi0tLXsQ0UFENBk5h6GJqNxr8y0Av+no6Li2P+Z76JeC0EU+WMoM5L50rkPVMlhNRItt216mlHrRtu3nAbxUSlwFXdf3R24F2y0g6Ip0Oj2hWm/L3iI/KloKYFeXKhml1GQ/CWm70DRtGIBxRDQeuYQvE5BbXA1st4WZXwPwpzFjxlxdL6c1y6FfC0IX8XhcrVq16kQAUWaeguokue0Kvf4CgBeI6DUAK0Kh0BujRo16r3vshXwGqCc95q22bdtHt7W1PVQFO3sdwzCOZOb74T51WNLR0XFw9zdwPB5Xq1ev3i6dTu+az9uwG3JRlvdBbpG4Gp/lDID7AVzR2Nh4ez3Ez6iUASEI3Wlubh4TCoWaAHwXudwCtXgGm5FPWApgBXLurT9wq0xEyWQyWewMfl2j63oSgO5R5RrkXJ93Rc4PYGcAg6pvGex89KebbNu+Zf78+R016LPPMOAEoTvnnHPODplM5hTkYhVOQnVGDuVwF4D3mXmNUmoNM3/2EwqF1jDzmmw2u0YptbavhX/TNG1YKBQaadv2CCIamc1mRxLRZz+2bY8kopHMvK1XMJUak2XmxQBuAXBz3uFtQDKgBaE7+ajGx+RTiU1D8dBZfYUMgDXIRQTuyn683rbtNBFlkD9gxMyfEFHXEJzz93gxEvnPBzOHiKjr0M5wZg4rpT7bsmTmEfn6I1E/CYTfAbCIme9l5vv7axTlUhFBcEHTtHFEdCwRHcPMBwEY3ds2CRWRAvAkEd0H4N5a+prUEyIIPolGo9uEw+FJSqkD8ouBB0NEoq+yCsATRPS0bdtPA1hsWZbreQrhc0QQyiQej4c/+uijfbLZ7GSl1DhmHgdgD+RWvOveJbxOyAJ4m5lfUUq9zMwv2rb91NixY1/sD27EvYEIQsDEYrFBzLyHbdt7KqX2ZOY9iWg/Zv5Kb9tWzxDR88z8LICXmflVpdQrRPSqpHIPFhGEGlBkiy0L4HkAjfmfPnc0u8qkkZvfpwCMh/voKmGaZrHox0KFiCBUmZkzZ+6ezWZfgMsXnZnnW5b1WYjzaDQ6etCgQWMymUxjKBTaGsAYZh7FzFsqpUYx8ygAowBsSURd/+4Tx7gBfEpEq5n5YwCrkfPo/Ozf+f9fCeBDZk4RUUf3lOuGYbQz89kubXcqpfZOJBIravB7DFjqZYuobslms5fA/a3/cSaT+UI8gXz8AM+4B05Eo9ERSqlBRDQsFAoNzWazDeFweJRt2w1E1P1Mv4L/5LdrAXzmncfMG5RSnZlMZjUzbyaijeFweN3mzZs729vb13q04wsi+iUzN8HZ5bjBtu2LAZxaaT+COzJCqCKGYRzKzK4nFZm52bKsK2ppU19H0zSNiFzDqjHzoZZlPVZLmwYSshpePci27cs8yhePGTPmyppZUyekUqkrACz1qHJprWwZiIggVAld108gooNdipmZZw2EwzKlsnDhwiwzG3A5Xk5Eh9VT+PZ6QwShCsTj8TC8sxf9TYa97liW9Qgz3+xWHgqFLm1qauor5076FSIIVSCVSp0KYE+X4k4i8gxMKgAAfgWXEPvM/JXGxsY+F6m5PyCCEDD50cEFbuXM3FYs0YoAWJb1OhG5Jq0hogvi8bh8fgNGHmjAdHR0NME9GtC6UCh0SS3tqWc6OzsvBuAWMWrPVatW1XVi1b6ICEKAxONxlY/j6EYikUikamZQndPe3v4BAK8tyF9Cts4DRQQhQFKp1HTkYvo58QmAest43Otks9nLALgFgZmoaVrdJPKtB0QQAiT/xnLjT93ddKtNU1NTaNasWYGndJs1a9bIWs7d8yHM5rqVE5HXMxdKRAQhIHRdn+Lhd7A2nU7Pq5UtsVhs4pgxY5al0+nVuq4va25u3qnSNg3D2FnX9efT6fTqVCq1rKWlZUIApvqioaFhDtxTy3+tpaXliFrZ0t8RQQgIZj7Po9gKwtffD5qmHWXb9iP4fOoyPhQKVTxVYea5yEU4BoB9lFKP1uqLOGfOnI+Z2dXFWynl9eyFEhBBCADDMPYioikuxZvC4XBNRge6rk8lojvQMz2bW2aoUtip4N/DlFJ36bru9nsHilJqDnLRq504VtO0IPNuDFhEEAKAmX8M99XuBfPmzVtZbRsMw/gGgP8BMMSh+KZK22fmGx0uDwHwL8Mwqr6wl0+zfq1LMRFRtNo2DAREECpk9uzZQwH80KW4UylV9cM4mqadxsz/gnNchPNM0/xjpX1YlnUpAKeh+WBm/pemad+ptI9iENGlcPFeBHB2NBp1EkOhBEQQKmTjxo1NcIkvwMz/TCQS71Wzf03TvkNEolFRPgAAE5pJREFUC+CcU+J80zS9zlSUhGmav2fmuENRhIhuNAzjhKD6ciKZTL5JRP/jUjwyHA6fWM3+BwIiCBVCRM0eZVdVs29N004lopsANBQUMYBzTNO8OOg+Lcu6kJnPRc/TiA3MfIthGE1B99kdZvZ6pj+uZt8DARGECjAMYxJyWYWdWGGa5n1V7DtORDegZzSmNIDvmaZ5ebX6tixrDhGdnu+rOxFm/rthGPFq9d3Y2HgPgLedyojosFgstl+1+h4IiCBUADOf5lZGRNcjgJTxhcTjcaXr+hxmvgA9FzI7ieg00zQrXkQsRjKZ/BuAM9BTFIiZL9A07Y8O9lVMPB63mfl6t/JsNvvdoPscSIgglA8BcBseZ4noL0F3GI1Gh6RSqdsAzHIoXkdExyaTyYVB9+uGaZo3MfOxcHAtJqLZuq7/s0oLfVcgF626B0R0MuR8Q9mIIJSJYRgHANjWpfjJoBcTW1paRkUikbsBHO9QvJaITkgmkw8G2acfLMt6AMC3kTurUci3IpHIHUG7UOeTsT7tUrxTLBarmRdlf0MEoUyY2XWbjYjuCrKvmTNn7q6UehTA4Q7F72az2a/1hhh0YZrm/UqpwwH8n0PxUel0+pFYLOZ2JLwsmNn1Gdu2XfUt0P6KCEL5eK2mu22NlYxhGN/OZrPPABjnUPwogP3mz5//fFD9lUsikVjKzJMALHEo3se27Wd1Xf9WUP2FQiGvZyzrCGUic60y0HV9H+SyLTnxnmmaOyCABUVN01qJ6I9w9jG4d9OmTU0LFixwO/TTK0Sj0RHhcPgWIjrGoTjDzK2WZSUD6Ip0XX8fwNZOhbZt79XW1vZKAP0MKGSEUAbMPM2tjIj+gwrFIBaLDdJ1/Woi+jOcxWBBOp0+vq+JAQC0t7evzWQy0+HsZhwmooRhGO3xeLzQd6JUGIBrzgsimlph+wMSEYTycJrLAwCYeXklDc+cOXP3/GnFM52b51+bpnlWe3t74XZfn6G9vT1tmuYPAVzkVM7MZ6dSqYdbWlp2qaQfZnadKhHRYZW0PVARQSgDIjrUo8zRacYPmqadmV8vmORQvImIzrAs6zfltl9j2DTNC5ATtk8dyg9SSj2r6/r3y+2AiN7yKHb9GwnuiCCUiKZpWyOXpdkRZl5TapvRaHSEpmk3EdHV6Hl0Gcz8mlLqq8lk0tUhp69imua1SqlDADglad0CwHW6rl8fi8W2KKN5r2e9bWtr65ZltDmgEUEokVAo9BWvcmYuKROzpmknRSKRZUR0ikuVf4RCoUmJRMIrvVmfJpFIPBuJRCYx820uVU6zbXtpGScmB3sVdnZ2ev6thJ5I9ucSYWbPYCNEtDeAW4u1E4vF9rNtey7c1yPSAH5qmuY8VMEFutbMnTt3DYDvGIbxE2b+HXp+9nYion/ouv6AUqrVjwAS0d7Mno8miMAwAwoZIZSO4zZXN07LJ2txZObMmbvrur7Atu0lcBeD95j5SNM056IfiEE3OJlMXqaUOhrA+y51jrJt+2ld16/0cmaKRqMRZv6eV2dENLYSYwci4odQIrquXwrgZ0Wq/SMcDutdkZI0TRtGREcy81lEdAI8hJiZbwuFQtH+nr+hubl5TCgUuhrAdI9qWQC3AVgwePDghy677LINQG4dh4gsAMXiH1yUX9gUfCKCUCK6rl8C4Bc+qtoAus4zbIfio7FVzDzTsiynUGX9FdI0LUpEfwIwtEhdG8C7yH1m/TxPMHPcsqwLKzdz4CBThhJh5o99VlUAdsj/FHvOt2Sz2X0GmBgAAFuWdQUz70tEDxWpq5BbE/DzPAEARLS6UgMHGrKoWCJE9EaAzXUQkVHLI8t9EcuyXgdwlGEYM5j5DwCC2i58PaB2BgwyQiidh+Ee6NMvaQBmOp3eZ6CLQTc4mUxelc1mxzHzfFT+jDvT6fSjQRg2kJA1hDLQdX0hgJPKuNVm5oXhcPj8yy+//LWg7epPtLS07KmUugS5hcOSX1xEdGMymfTchRB6IlOGMmDmn+cPzzhGW3agE8ANtm3/Xk7g+SP/nE6KxWJ7M/PPmPlU9Iwf6caaTCbjZ+FXKEBGCGWiadoh+YjH23tUexHANdls9tp80lKhTPIu42cS0ZkA9vSo+o5t26e0tbU9URvL+hciCBUQjUaHRCKRJmY+mIi2Qs6J6D0AL2Wz2fvmz5//Vu9a2D9paWnZhYimKKXGMfO2yH2OVzHzE4MGDVo4Z86cTb1toyAIgiAIgiAIgiAIgiAIgiAIgiAIgiAIgiAIgiAIgiAIgiAIgiAIgiAIgiAIgiAIghAMEiDFB9OW4S4ABxdcXgfGuyBcsmgC7i7WxpFLMLqhAYVh1jtB+ATAilAa8+7eH76Ss0x/AVtnMjgLhB0YGKkAsoH1xPjIJlx33wSUnZJ+6lJMUYRh907A7X5t6cxiiFPZhvX44PFD4C9YCYOOW46RHMYWmTRUhLDmzgmQMOo1RmIq+mMkgGGUyyIEAGDCl5BLxTYfPnIIhgdjCLI4BsAnAFZ9VsAYDODkbAQTAJxQrJ2mmxFam8X/gjAOwDoCMoy8shNGKkCb+iz2/fd+KCuIKxF+D8KOgD9BSGfxFwUc51Q2bDiOBbCoWBvHPIdj1XK0pYGdkQFAubDU05bhAzDOXTQRN/mxZdoy3AtgK8dCxuuLJuK7ftoZyIgg+EMBWH/vBJzc/eK0ZXgcwDhfDWQQAgEg3LBoPLSCdt4AYzc/7XyyB/YCMA6MyxdNxDndy6Yux4nEuJXC+DpQpiAAihnZUurn/3teYZnK4FU/bSiFdgCDGPi1AjoZIDC2AuEUEK4+8gXc8eA+WO+nqW7/PwE5XXkJAIhg+7FloCOC4I8QgJHTlqEwa9MoAI/5aYDCCHEWAOP4acuwe7ciBWA7EO7x044dxkiyAaZuo4wuIxkf2gCIMcpPW05w7nf1LQjI1WcbX+yTAL53P7xV7OYjX8AwZLEDAQsXTcBvupdNW471YJwxhLAVUFwQFk3A1M/uXYYOAO8smoBJfn8RQQTBFwSEGNhMwH1d15hhM+EtZHC5rzYyCHFuxWY4gF26FY1ELrz4037aUVkoJoC45xsvYyOkFOD3DT9tGU4DcHg2gl/cPw4fdXWBEgQh/2yIeibAzQL4edEGNuFLaABsxqeFRYvG40IA5eZmDIFKEjYBIgi+yL81VxdOGUoho6AUAwRcde8E/KTr+pFv4ksN67AJjIk+jfkYBJDCTsct++JbOcPYheE8enBuCgcTEI104s8APgKDsBxDQT2/nG7YQIiATyPAl/3e052hDcimARAhVFg2dSkuI8IMECYuGo93S2xaoYSpj5BDBMEfJb01HRuwc2sIXPBm3/wBqGGY/3bunYgXpy3HM8z4URr40RcKcyOQtUS4y1djjKUgwCa8PG0Z0G1v4jq/9uTXECJp4ObCsmOWY+Z943NzeDfuHI8105ajA8ARU5fhfJVLapMzj3ACGIM7h/nbfSkgBIdRlOCNCIIPFk3A+ErbWLcBK4YPw6QM48Pu17d7D52f7IWpRPCXyIXAoddwlL0R3wZhNxto+KyIsJYzuOXf+/l7mx46AQseWw7FjP2I8gtyjA8iwDz/vxleRy6Ve491i1DWRwo2AmM5zgKjjYCLuHsZYy0RWh/c2f+IpXv3MmUoHfFDEPoEcYZavBwjslmMyISgFINHvIJ3Fp5c3pf6mOewHwMb7t/X306HIAiCIAiCIAiCIAiCIAiCIAiCIAiCEAh17YfAzD9Bceeqa4nowyJ1vPr4JoDty72/gCeI6NlKG2HmrZA7Kn0EcraNyBetAfAGgIcA/A8Rrau0r259nglgrEeVNUR0RZE2JgD4RpGubiGiFSWa19X+bsDnB5y68QkR3VBOmy79jADwLQBHAtgBufMoQO75v4nc8789yOcvFIGZx7A/vlNhP//22Y8fih/28bYlxMznMfMaH32tZOZmZg5E9Jn56SL9bWZmT3Fm5rgPux1jK/i08bsubb5ZbpsF7RMz68yc8vn8zw6i31pS3LW07+IrDkEJ9fo0zDwcwN0AfofPRwRejAHQBuBGZo5U07Y8DQB2LVJnjxrYURXyYncDgCSA0T5uGQOgnZmvZea6+Z7VjaEODChBALAAzsPhYpwC4NKAbXFj7yLle9bEiupwAYBTy7jvDAC/DNiWqiGCUAcw81EATqqgiVnMXOzLGgSufXBu6lKXIwRm3gE94z2Uwi+ZeZug7KkmA0EQ9qqnIZsLsz3KNiMXJuxV5EKGOaEAnBu0UQ54/U22AVDCQe8+hY5cEBsnssgt5D4PuAaUHQR8MWxeX6Wejz/v5bPeEORW4t8us59WOBztRS5mX9LhehbAUS5tlby4xcxD4D5VeALAiV27KMy8C4A74PzFPIGZiYjYoSwovEYh9Txd+KbL9fcBHEdEzwGf7T5c71L/OADnV8e8AQ4zD2Nm22Fl91OXFd9pVbDhMJe+3N7S5fZzhEs/WWbusYjHzId4rHyXPWTn4rsMzMwb2GU0xsw/9nE/cx/bZWDmkfln7USPCFrMPIqZ1zvUTTPz0HLtqBX1OpTeC84+FPe71K/F/Lla7ORy/V2X/frHAdeAIjsHYpE7Q+Bub72OELaH+/fkgcILRLQagJOvSRjAtgHaVRXqVRCchsQZwDV0WD0vLLpFUH7P6WJ+SuAWMansaMwl4Pas63JBEe5bvJsB19iVvfn8K6I/CcLbAF4ooX69MNjluldYMbeyWgxZ3UZj9TpCcMxKBWCzx3qM2+KiTBmqhNMX/FUAr5RQv14I0r28Fq7qPRZ7OecYtVPh5RrYIpRIfxKEV4joA+RSpRUympn9eJcJpeH0rJ1GCLui546W45RH6F3qThCYeRCcXWRfK/hvIfU8SuirOE3R9uae5yecpgsvV8GevkIKOd+Ewh9/iW97kboTBDi/bYDPpwtuUXZFEILnNeQW17qzBXquphcKQhpl+GTUC0R0HhHt6vDzeG/bVox6FAS3L/arBf/1e59QPhk4j8gKpw2FOwxvwN2rUuhF+osgrMfnc1IRhNriOG0o+HfhCMFt8VfoZfqLILzebQtIBKG2vOhwrfBZiyDUCf1FELqLwCtw3tLanpnr9XBNX8ZTEJh5SwCNBeWSTamPUleCkPeTd1qx/uwDlg9b5RQyjVzuFSqj2JRhd4dyGSH0UepKEJCLX+fkOVb4xpFpQ+14Hd0yNufZipnH5P/fSYRFEPoo9SYIxXYY3P5d7H6hTIgoDe+dhsIdhtVE5C/TtVBz+osgFL5xRBBqi9M6QpcgFI4QZP2gD9MfBKGDiNYUXBNBqC1eC4uyw1BH9AdBcPryuwnCrszcEKA9Qg43F2YFYLeC6zJC6MPUWwg1v4LwBnJedIW/XwQ51+eXArZroOM2QtgePY9v9/sRAjO3wDmM3kVE9Hyt7SmFuhGE/Kr1lg5FPQSBiDrzYbOctrzGQQQhaF5Fbqeh++hrGwBfdajb7wUBwCQATQ7X59fakFKpG0GAe1DVfZnZKUS223TIb3BWwSdElGbm19HTZflbBf+2kdumFPoo9SQIbpF4vpv/qbQdoTJeQs9n+/WCf79HRH3+CPBApp4WFYPaIZCdhurgNDceWfDv/hwDoV8wEAWh3hK3BBlqzA6wrUKcFhYLqcf1g3p5/oFQT1+MoAShK3FLveAUpgzIBSJxo/DN3MXaCm3xor8KgltK9yHsnu3a7fm7/S37DHWxhpA/pegU0/5TeIel2gJAyOH6Xig/k1OtWe1yfSeni/kQc192uefjIAxy4VU4b/UW1qk3Cp3euggD2A7AWw5lbvkv3P6WfYZ6GSGMg3PE4POIaEu3H+RSnTlRTwuLbqHlG5n5GIfrJ8NZBNmjrYohok4U30GoxxHCm3APa98jGzTnkupOdKi7Hu75GvoM9SQIThR74/SHsOzL4f5m+Rszn8nMOzHzbsysATBd6r5Sg0NFXoKzEXXwhSiEiDYDWOxSHGfm85j5AGYex8ynIJdb0+l79SQRZapmaEDUxZQB/g81FVL3ZxqIyGbma5BLOltII4CrfTblt14lvAjgv1zKXqtyotlqch2AwxyuNwD4Xf6nGNcGalGVqOcRwqdwnr91p+4FIY8J70xNxVgN4KqAbPHCa2GxHqcLXdwIYGUF9/8fgIUB2VJV6lkQVhBRsW0ctw/hVsxcGNarz5JP6jqrzNsZwA+J6KMATXLDa8pQt4JARBsA/ADlbRvaAM4gokoEvWb0eUHIr5rv4lDk5wO2AkDWpayuRglEdAWAH6G08OUbAZxERLdXx6oevILcToMT9bjD8BlEdC9y06GNJdz2CYDpRPS/1bEqePq8ICB3fNZpraPoByy/IOS2vVhXggAARHQVgEOQW7jyeltlANwE4EAiurUWtgGf7TQ4pagH6niE0AUR3QZgMnLDf7cXDZB7/jcCOCgvJHVDLZJ/VkTe+WO4Q9EmP8MwZh4OZ0Hxdb9Hu1+C834/E1HVsxLlc1VORc4foStl+RrkhPJ+IgrUCYmZnXw6NhPRxoJ6bs97bfcpHjMPATDIod76fFi2cmwcCmCsQ1GaiALd4WDmUcg9/13wuSPSGuS2Xu9zCNojCIIgCIIgCIIgCIIgCIIgCIIgCIIgCIIgCIIgCIIgCIIgCIIgCIIgCIIgCIIgCIIgCIIgCIIgCIIgCIIgCIIgCIIgCIIgCIIgCIIgCIIgCIIg9E3+H9b4Yds5Uq7bAAAAAElFTkSuQmCC" alt="Project Atomic" />
2305
+    </span>
2306
+    <div class="container">
2307
+      <div class="row">
2308
+        <div class="col-sm-12">
2309
+          <div id="brand">
2310
+            <!-- insert brand image here -->
2311
+            <!--
2312
+            <img src="" alt="Atomic Registry">
2313
+            -->
2314
+          </div>
2315
+
2316
+        </div>
2317
+        <div class="col-sm-7 col-md-6 col-lg-5 login">
2318
+          {{ if .Error }}
2319
+            <div class="error">{{ .Error }}</div>
2320
+            <!-- Error code: {{ .ErrorCode }} -->
2321
+          {{ end }}
2322
+
2323
+          <!-- Identity provider name: {{ .ProviderName }} -->
2324
+          <form class="form-horizontal" role="form" action="{{ .Action }}" method="POST">
2325
+            <input type="hidden" name="{{ .Names.Then }}" value="{{ .Values.Then }}">
2326
+            <input type="hidden" name="{{ .Names.CSRF }}" value="{{ .Values.CSRF }}">
2327
+            <div class="form-group">
2328
+              <label for="inputUsername" class="col-sm-2 col-md-2 control-label">Username</label>
2329
+              <div class="col-sm-10 col-md-10">
2330
+                <input type="text" class="form-control" id="inputUsername" placeholder="" tabindex="1" autofocus="autofocus" type="text" name="{{ .Names.Username }}" value="{{ .Values.Username }}">
2331
+              </div>
2332
+            </div>
2333
+            <div class="form-group">
2334
+              <label for="inputPassword" class="col-sm-2 col-md-2 control-label">Password</label>
2335
+              <div class="col-sm-10 col-md-10">
2336
+                <input type="password" class="form-control" id="inputPassword" placeholder="" tabindex="2" type="password" name="{{ .Names.Password }}" value="">
2337
+              </div>
2338
+            </div>
2339
+            <div class="form-group">
2340
+              <div class="col-xs-8 col-sm-offset-2 col-sm-6 col-md-offset-2 col-md-6">
2341
+
2342
+              </div>
2343
+              <div class="col-xs-4 col-sm-4 col-md-4 submit">
2344
+                <button type="submit" class="btn btn-primary btn-lg" tabindex="4">Log In</button>
2345
+              </div>
2346
+            </div>
2347
+          </form>
2348
+        </div>
2349
+        <div class="col-sm-5 col-md-6 col-lg-7 details">
2350
+          <p><strong>Welcome to Atomic Registry.</strong>
2351
+          </p>
2352
+        </div>
2353
+      </div>
2354
+    </div>
2355
+  </body>
2356
+</html>
0 2357
new file mode 100644
... ...
@@ -0,0 +1,152 @@
0
+{
1
+    "kind": "Template",
2
+    "apiVersion": "v1",
3
+    "metadata": {
4
+        "name": "registry-newproject-template-shared",
5
+        "creationTimestamp": null
6
+    },
7
+    "objects": [
8
+        {
9
+            "kind": "Project",
10
+            "apiVersion": "v1",
11
+            "metadata": {
12
+                "name": "${PROJECT_NAME}",
13
+                "creationTimestamp": null,
14
+                "annotations": {
15
+                    "openshift.io/description": "${PROJECT_DESCRIPTION}",
16
+                    "openshift.io/display-name": "${PROJECT_DISPLAYNAME}",
17
+                    "openshift.io/requester": "${PROJECT_REQUESTING_USER}"
18
+                }
19
+            },
20
+            "spec": {},
21
+            "status": {}
22
+        },
23
+        {
24
+            "apiVersion": "v1",
25
+            "groupNames": [
26
+                "system:authenticated"
27
+            ],
28
+            "kind": "RoleBinding",
29
+            "metadata": {
30
+                "creationTimestamp": null,
31
+                "name": "registry-viewer",
32
+                "namespace": "${PROJECT_NAME}"
33
+            },
34
+            "roleRef": {
35
+                "name": "registry-viewer"
36
+            },
37
+            "subjects": [
38
+                {
39
+                    "kind": "Group",
40
+                    "name": "system:authenticated"
41
+                }
42
+            ],
43
+            "userNames": null
44
+        },
45
+        {
46
+            "kind": "RoleBinding",
47
+            "apiVersion": "v1",
48
+            "metadata": {
49
+                "name": "registry-admin",
50
+                "namespace": "${PROJECT_NAME}",
51
+                "creationTimestamp": null
52
+            },
53
+            "userNames": [
54
+                "${PROJECT_ADMIN_USER}"
55
+            ],
56
+            "groupNames": null,
57
+            "subjects": [
58
+                {
59
+                    "kind": "User",
60
+                    "name": "${PROJECT_ADMIN_USER}"
61
+                }
62
+            ],
63
+            "roleRef": {
64
+                "name": "registry-admin"
65
+            }
66
+        },
67
+        {
68
+            "kind": "RoleBinding",
69
+            "apiVersion": "v1",
70
+            "metadata": {
71
+                "name": "system:image-pullers",
72
+                "namespace": "${PROJECT_NAME}",
73
+                "creationTimestamp": null
74
+            },
75
+            "userNames": null,
76
+            "groupNames": [
77
+                "system:serviceaccounts:${PROJECT_NAME}"
78
+            ],
79
+            "subjects": [
80
+                {
81
+                    "kind": "SystemGroup",
82
+                    "name": "system:serviceaccounts:${PROJECT_NAME}"
83
+                }
84
+            ],
85
+            "roleRef": {
86
+                "name": "system:image-puller"
87
+            }
88
+        },
89
+        {
90
+            "kind": "RoleBinding",
91
+            "apiVersion": "v1",
92
+            "metadata": {
93
+                "name": "system:image-builders",
94
+                "namespace": "${PROJECT_NAME}",
95
+                "creationTimestamp": null
96
+            },
97
+            "userNames": [
98
+                "system:serviceaccount:${PROJECT_NAME}:builder"
99
+            ],
100
+            "groupNames": null,
101
+            "subjects": [
102
+                {
103
+                    "kind": "ServiceAccount",
104
+                    "name": "builder"
105
+                }
106
+            ],
107
+            "roleRef": {
108
+                "name": "system:image-builder"
109
+            }
110
+        },
111
+        {
112
+            "kind": "RoleBinding",
113
+            "apiVersion": "v1",
114
+            "metadata": {
115
+                "name": "system:deployers",
116
+                "namespace": "${PROJECT_NAME}",
117
+                "creationTimestamp": null
118
+            },
119
+            "userNames": [
120
+                "system:serviceaccount:${PROJECT_NAME}:deployer"
121
+            ],
122
+            "groupNames": null,
123
+            "subjects": [
124
+                {
125
+                    "kind": "ServiceAccount",
126
+                    "name": "deployer"
127
+                }
128
+            ],
129
+            "roleRef": {
130
+                "name": "system:deployer"
131
+            }
132
+        }
133
+    ],
134
+    "parameters": [
135
+        {
136
+            "name": "PROJECT_NAME"
137
+        },
138
+        {
139
+            "name": "PROJECT_DISPLAYNAME"
140
+        },
141
+        {
142
+            "name": "PROJECT_DESCRIPTION"
143
+        },
144
+        {
145
+            "name": "PROJECT_ADMIN_USER"
146
+        },
147
+        {
148
+            "name": "PROJECT_REQUESTING_USER"
149
+        }
150
+    ]
151
+}
0 152
new file mode 100644
... ...
@@ -0,0 +1,130 @@
0
+{
1
+    "kind": "Template",
2
+    "apiVersion": "v1",
3
+    "metadata": {
4
+        "name": "registry-newproject-template-unshared",
5
+        "creationTimestamp": null
6
+    },
7
+    "objects": [
8
+        {
9
+            "kind": "Project",
10
+            "apiVersion": "v1",
11
+            "metadata": {
12
+                "name": "${PROJECT_NAME}",
13
+                "creationTimestamp": null,
14
+                "annotations": {
15
+                    "openshift.io/description": "${PROJECT_DESCRIPTION}",
16
+                    "openshift.io/display-name": "${PROJECT_DISPLAYNAME}",
17
+                    "openshift.io/requester": "${PROJECT_REQUESTING_USER}"
18
+                }
19
+            },
20
+            "spec": {},
21
+            "status": {}
22
+        },
23
+        {
24
+            "kind": "RoleBinding",
25
+            "apiVersion": "v1",
26
+            "metadata": {
27
+                "name": "registry-admin",
28
+                "namespace": "${PROJECT_NAME}",
29
+                "creationTimestamp": null
30
+            },
31
+            "userNames": [
32
+                "${PROJECT_ADMIN_USER}"
33
+            ],
34
+            "groupNames": null,
35
+            "subjects": [
36
+                {
37
+                    "kind": "User",
38
+                    "name": "${PROJECT_ADMIN_USER}"
39
+                }
40
+            ],
41
+            "roleRef": {
42
+                "name": "registry-admin"
43
+            }
44
+        },
45
+        {
46
+            "kind": "RoleBinding",
47
+            "apiVersion": "v1",
48
+            "metadata": {
49
+                "name": "system:image-pullers",
50
+                "namespace": "${PROJECT_NAME}",
51
+                "creationTimestamp": null
52
+            },
53
+            "userNames": null,
54
+            "groupNames": [
55
+                "system:serviceaccounts:${PROJECT_NAME}"
56
+            ],
57
+            "subjects": [
58
+                {
59
+                    "kind": "SystemGroup",
60
+                    "name": "system:serviceaccounts:${PROJECT_NAME}"
61
+                }
62
+            ],
63
+            "roleRef": {
64
+                "name": "system:image-puller"
65
+            }
66
+        },
67
+        {
68
+            "kind": "RoleBinding",
69
+            "apiVersion": "v1",
70
+            "metadata": {
71
+                "name": "system:image-builders",
72
+                "namespace": "${PROJECT_NAME}",
73
+                "creationTimestamp": null
74
+            },
75
+            "userNames": [
76
+                "system:serviceaccount:${PROJECT_NAME}:builder"
77
+            ],
78
+            "groupNames": null,
79
+            "subjects": [
80
+                {
81
+                    "kind": "ServiceAccount",
82
+                    "name": "builder"
83
+                }
84
+            ],
85
+            "roleRef": {
86
+                "name": "system:image-builder"
87
+            }
88
+        },
89
+        {
90
+            "kind": "RoleBinding",
91
+            "apiVersion": "v1",
92
+            "metadata": {
93
+                "name": "system:deployers",
94
+                "namespace": "${PROJECT_NAME}",
95
+                "creationTimestamp": null
96
+            },
97
+            "userNames": [
98
+                "system:serviceaccount:${PROJECT_NAME}:deployer"
99
+            ],
100
+            "groupNames": null,
101
+            "subjects": [
102
+                {
103
+                    "kind": "ServiceAccount",
104
+                    "name": "deployer"
105
+                }
106
+            ],
107
+            "roleRef": {
108
+                "name": "system:deployer"
109
+            }
110
+        }
111
+    ],
112
+    "parameters": [
113
+        {
114
+            "name": "PROJECT_NAME"
115
+        },
116
+        {
117
+            "name": "PROJECT_DISPLAYNAME"
118
+        },
119
+        {
120
+            "name": "PROJECT_DESCRIPTION"
121
+        },
122
+        {
123
+            "name": "PROJECT_ADMIN_USER"
124
+        },
125
+        {
126
+            "name": "PROJECT_REQUESTING_USER"
127
+        }
128
+    ]
129
+}
0 130
new file mode 100755
... ...
@@ -0,0 +1,89 @@
0
+#!/bin/bash
1
+
2
+# wait_for_url attempts to access a url in order to
3
+# determine if it is available to service requests.
4
+#
5
+# $1 - The URL to check
6
+# $2 - Optional prefix to use when echoing a successful result
7
+# $3 - Optional time to sleep between attempts (Default: 0.2s)
8
+# $4 - Optional number of attemps to make (Default: 10)
9
+# attribution: openshift/origin hack/util.sh
10
+function wait_for_url {
11
+	url=$1
12
+	prefix=${2:-}
13
+	wait=${3:-0.5}
14
+	times=${4:-40}
15
+
16
+	set +e
17
+	cmd="chroot /host curl -kfLs ${url}"
18
+	for i in $(seq 1 $times); do
19
+		out=$(${cmd})
20
+		if [ $? -eq 0 ]; then
21
+			set -e
22
+			echo "${prefix}${out}"
23
+			return 0
24
+		fi
25
+		sleep $wait
26
+	done
27
+	echo "ERROR: gave up waiting ${wait} seconds ${times} times for ${url} with command ${cmd}"
28
+  set -e
29
+	return 1
30
+}
31
+
32
+INSTALL_HOST=${1:-`hostname`}
33
+
34
+echo "Running using hostname ${INSTALL_HOST}"
35
+
36
+chroot /host sudo docker run -d --name "origin" \
37
+        --privileged --pid=host --net=host \
38
+        -e KUBECONFIG=/etc/origin/master/admin.kubeconfig \
39
+        -v /:/rootfs:ro -v /var/run:/var/run:rw -v /sys:/sys -v /var/lib/docker:/var/lib/docker:rw \
40
+        -v /etc/origin/:/etc/origin/ -v /var/lib/origin:/var/lib/origin \
41
+        openshift/origin start \
42
+        --master-config /etc/origin/master/master-config.yaml \
43
+        --node-config=/etc/origin/node/node-config.yaml \
44
+        --latest-images=true
45
+
46
+echo "Waiting for services to come up..."
47
+wait_for_url "https://${INSTALL_HOST}:8443/api"
48
+
49
+CMD="chroot /host docker exec -it origin"
50
+echo "Starting registry services..."
51
+
52
+set -x
53
+
54
+$CMD oadm registry --credentials /etc/origin/master/openshift-registry.kubeconfig --latest-images=true
55
+# we're hacking the service to use a node port to reduce deployment complexity
56
+$CMD oc patch service docker-registry -p \
57
+     '{ "spec": { "type": "NodePort", "selector": {"docker-registry": "default"}, "ports": [ {"nodePort": 5000, "port": 5000, "targetPort": 5000}] }}'
58
+
59
+set +x
60
+echo "Starting web UI service..."
61
+
62
+# TODO: use master cert from /etc/origin/registry/master.server.cert
63
+# create secret volume
64
+# mounted at /etc/cockpit/ws-certs.d/master.server.cert
65
+# use secret volume in template
66
+
67
+set -x
68
+$CMD oc create -f /etc/origin/registry/registry-console-template.yaml
69
+$CMD oc new-app --template registry-console-template \
70
+     -p OPENSHIFT_OAUTH_PROVIDER_URL=https://${INSTALL_HOST}:8443,COCKPIT_KUBE_URL=https://${INSTALL_HOST},REGISTRY_HOST=${INSTALL_HOST}:5000
71
+# we're hacking the service to use a node port to reduce deployment complexity
72
+$CMD oc patch service registry-console -p \
73
+     '{ "spec": { "type": "NodePort", "selector": {"name": "registry-console"}, "ports": [ {"nodePort": 443, "port": 9000, "targetPort": 9090}] }}'
74
+
75
+set +x
76
+echo "Updating default project configuration"
77
+set -x
78
+$CMD oc create -f /etc/origin/registry/registry-newproject-template-shared.json
79
+$CMD oc create -f /etc/origin/registry/registry-newproject-template-unshared.json
80
+sed -i 's/  projectRequestTemplate:.*$/  projectRequestTemplate: "default\/registry-newproject-template-shared"/' /etc/origin/master/master-config.yaml
81
+
82
+set +x
83
+echo "Restarting API server"
84
+set -x
85
+chroot /host docker restart origin
86
+
87
+set +x
88
+echo "Web UI hosted at https://${INSTALL_HOST}"
0 89
new file mode 100755
... ...
@@ -0,0 +1,58 @@
0
+#!/bin/bash
1
+
2
+###
3
+# basic install and run test for atomic registry quickstart image
4
+# run with "uninstall" argument to test tear down after test
5
+###
6
+
7
+set -o errexit
8
+set -o pipefail
9
+set -x
10
+
11
+TEST_IMAGE=atomic-registry-quickstart
12
+
13
+# we're going to use this for testing
14
+# node ports aren't working with boxes default hostname localdomain.localhost
15
+LOCALHOST=127.0.0.1
16
+CMD="docker exec -it origin"
17
+
18
+USER=mary
19
+PROJ=mary-project
20
+
21
+function test_push() {
22
+  # login as $USER and do a basic docker workflow
23
+  $CMD oc login -u ${USER} -p test
24
+  $CMD oc new-project ${PROJ}
25
+  TOKEN=$($CMD oc whoami -t)
26
+  docker login -p ${TOKEN} -u unused -e test@example.com ${LOCALHOST}:5000
27
+  docker pull busybox
28
+  docker tag busybox ${LOCALHOST}:5000/${PROJ}/busybox
29
+  docker push ${LOCALHOST}:5000/${PROJ}/busybox
30
+  docker rmi busybox ${LOCALHOST}:5000/${PROJ}/busybox
31
+  docker logout
32
+}
33
+
34
+function test_cannot_push() {
35
+  # in shared mode...
36
+  # we pull $USERS's image, tag and try to push
37
+  # bob shouldn't be able to push
38
+  $CMD oc login -u bob -p test
39
+  TOKEN=$($CMD oc whoami -t)
40
+  docker login -p ${TOKEN} -u unused -e test@example.com ${LOCALHOST}:5000
41
+  docker pull ${LOCALHOST}:5000/${PROJ}/busybox
42
+  docker tag ${LOCALHOST}:5000/${PROJ}/busybox ${LOCALHOST}:5000/${PROJ}/busybox:evil
43
+  if docker push ${LOCALHOST}:5000/${PROJ}/busybox:evil; then
44
+    echo "registry-viewer user should not have been able to push to repo"
45
+    docker logout
46
+    exit 1
47
+  fi
48
+  docker rmi ${LOCALHOST}:5000/${PROJ}/busybox ${LOCALHOST}:5000/${PROJ}/busybox:evil
49
+  docker logout
50
+}
51
+
52
+# first we need to patch for the vagrant port mapping 443 -> 1443
53
+$CMD oc login -u system:admin
54
+$CMD oc patch oauthclient cockpit-oauth-client -p  '{ "redirectURIs": [ "https://'"${LOCALHOST}"':1443" ] }'
55
+
56
+test_push
57
+test_cannot_push
0 58
new file mode 100755
... ...
@@ -0,0 +1,46 @@
0
+#!/bin/bash
1
+
2
+CMD="chroot /host docker exec -it origin"
3
+
4
+# let's make sure we're logged in as admin/default
5
+$CMD oc login -u system:admin
6
+$CMD oc project default
7
+
8
+SERVICES=(docker-registry registry-console)
9
+for SERVICE in "${SERVICES[@]}"
10
+do
11
+  $CMD oc delete dc,service ${SERVICE} --grace-period=0
12
+done
13
+
14
+echo "Waiting for pods to terminate"
15
+
16
+# poll for pods named "k8s", wait for them to die
17
+until [[ $(chroot /host docker ps -f NAME=k8s --format '{{ .Names }}' | wc -l) == 0 ]]
18
+do
19
+  printf "."
20
+  sleep 1
21
+done
22
+
23
+set -x
24
+chroot /host docker stop origin
25
+# remove all the containers that have started since origin
26
+#docker rm $(docker ps --since=origin -qa)
27
+# now remove origin
28
+chroot /host docker rm origin
29
+
30
+chroot /host find /var/lib/origin/volumes -type d -exec umount {} \; 2>/dev/null
31
+
32
+echo "Removing configuration files..."
33
+DIRS=(/etc/origin /var/lib/origin)
34
+for DIR in "${DIRS[@]}"
35
+do
36
+  chroot /host rm -rf ${DIR}
37
+done
38
+
39
+set +x
40
+IMAGES=(openshift/origin openshift/origin-docker-registry cockpit/kubernetes)
41
+
42
+echo "Uninstallation complete."
43
+echo "Stopped container and images have not been removed. To remove them manually run:"
44
+echo "'sudo docker rm \$(sudo docker ps -qa)'"
45
+echo "'sudo docker rmi ${IMAGES[*]}'"
... ...
@@ -97,7 +97,7 @@
97 97
                   "initialDelaySeconds": 5,
98 98
                   "exec": {
99 99
                     "command": [ "/bin/sh", "-i", "-c",
100
-                      "MYSQL_PWD='$MYSQL_PASSWORD' mysql -h 127.0.0.1 -u $MYSQL_USER -D $MYSQL_DATABASE -e 'SELECT 1'"]
100
+                      "MYSQL_PWD=\"$MYSQL_PASSWORD\" mysql -h 127.0.0.1 -u $MYSQL_USER -D $MYSQL_DATABASE -e 'SELECT 1'"]
101 101
                   }
102 102
                 },
103 103
                 "livenessProbe": {
... ...
@@ -114,7 +114,7 @@
114 114
                   "initialDelaySeconds": 5,
115 115
                   "exec": {
116 116
                     "command": [ "/bin/sh", "-i", "-c",
117
-                      "MYSQL_PWD='$MYSQL_PASSWORD' mysql -h 127.0.0.1 -u $MYSQL_USER -D $MYSQL_DATABASE -e 'SELECT 1'"]
117
+                      "MYSQL_PWD=\"$MYSQL_PASSWORD\" mysql -h 127.0.0.1 -u $MYSQL_USER -D $MYSQL_DATABASE -e 'SELECT 1'"]
118 118
                   }
119 119
                 },
120 120
                 "livenessProbe": {
... ...
@@ -331,6 +331,12 @@
331 331
                   }
332 332
                 ],
333 333
                 "resources": {},
334
+                "volumeMounts": [
335
+                  {
336
+                    "name": "ruby-helloworld-data",
337
+                    "mountPath": "/var/lib/mysql/data"
338
+                  }
339
+                ],
334 340
                 "terminationMessagePath": "/dev/termination-log",
335 341
                 "imagePullPolicy": "IfNotPresent",
336 342
                 "capabilities": {},
... ...
@@ -340,6 +346,14 @@
340 340
                 }
341 341
               }
342 342
             ],
343
+            "volumes": [
344
+              {
345
+                "name": "ruby-helloworld-data",
346
+                "emptyDir": {
347
+                  "medium": ""
348
+                }
349
+              }
350
+            ],
343 351
             "restartPolicy": "Always",
344 352
             "dnsPolicy": "ClusterFirst",
345 353
             "serviceAccount": ""
... ...
@@ -333,6 +333,12 @@
333 333
                   }
334 334
                 ],
335 335
                 "resources": {},
336
+                "volumeMounts": [
337
+                  {
338
+                    "name": "ruby-helloworld-data",
339
+                    "mountPath": "/var/lib/mysql/data"
340
+                  }
341
+                ],
336 342
                 "terminationMessagePath": "/dev/termination-log",
337 343
                 "imagePullPolicy": "IfNotPresent",
338 344
                 "capabilities": {},
... ...
@@ -342,6 +348,14 @@
342 342
                 }
343 343
               }
344 344
             ],
345
+            "volumes": [
346
+              {
347
+                "name": "ruby-helloworld-data",
348
+                "emptyDir": {
349
+                  "medium": ""
350
+                }
351
+              }
352
+            ],
345 353
             "restartPolicy": "Always",
346 354
             "dnsPolicy": "ClusterFirst",
347 355
             "serviceAccount": ""
... ...
@@ -384,6 +384,12 @@
384 384
                   }
385 385
                 ],
386 386
                 "resources": {},
387
+                "volumeMounts": [
388
+                  {
389
+                    "name": "ruby-helloworld-data",
390
+                    "mountPath": "/var/lib/mysql/data"
391
+                  }
392
+                ],
387 393
                 "terminationMessagePath": "/dev/termination-log",
388 394
                 "imagePullPolicy": "Always",
389 395
                 "securityContext": {
... ...
@@ -392,6 +398,14 @@
392 392
                 }
393 393
               }
394 394
             ],
395
+            "volumes": [
396
+              {
397
+                "name": "ruby-helloworld-data",
398
+                "emptyDir": {
399
+                  "medium": ""
400
+                }
401
+              }
402
+            ],
395 403
             "restartPolicy": "Always",
396 404
             "dnsPolicy": "ClusterFirst"
397 405
           }
... ...
@@ -409,6 +409,12 @@
409 409
                   }
410 410
                 ],
411 411
                 "resources": {},
412
+                "volumeMounts": [
413
+                  {
414
+                    "name": "ruby-helloworld-data",
415
+                    "mountPath": "/var/lib/mysql/data"
416
+                  }
417
+                ],
412 418
                 "terminationMessagePath": "/dev/termination-log",
413 419
                 "imagePullPolicy": "Always",
414 420
                 "securityContext": {
... ...
@@ -417,6 +423,14 @@
417 417
                 }
418 418
               }
419 419
             ],
420
+            "volumes": [
421
+              {
422
+                "name": "ruby-helloworld-data",
423
+                "emptyDir": {
424
+                  "medium": ""
425
+                }
426
+              }
427
+            ],
420 428
             "restartPolicy": "Always",
421 429
             "dnsPolicy": "ClusterFirst"
422 430
           }
... ...
@@ -134,24 +134,24 @@ function os::cmd::try_until_text() {
134 134
 
135 135
 # Functions in the os::cmd::internal namespace are discouraged from being used outside of os::cmd
136 136
 
137
-# In order to harvest stderr and stdout at the same time into different buckets, we need to stick them into files 
137
+# In order to harvest stderr and stdout at the same time into different buckets, we need to stick them into files
138 138
 # in an intermediate step
139 139
 BASETMPDIR="${TMPDIR:-"/tmp"}/openshift"
140 140
 os_cmd_internal_tmpdir="${BASETMPDIR}/test-cmd"
141 141
 os_cmd_internal_tmpout="${os_cmd_internal_tmpdir}/tmp_stdout.log"
142 142
 os_cmd_internal_tmperr="${os_cmd_internal_tmpdir}/tmp_stderr.log"
143 143
 
144
-# os::cmd::internal::expect_exit_code_run_grep runs the provided test command and expects a specific 
145
-# exit code from that command as well as the success of a specified `grep` invocation. Output from the 
144
+# os::cmd::internal::expect_exit_code_run_grep runs the provided test command and expects a specific
145
+# exit code from that command as well as the success of a specified `grep` invocation. Output from the
146 146
 # command to be tested is suppressed unless either `VERBOSE=1` or the test fails. This function bypasses
147
-# any error exiting settings or traps set by upstream callers by masking the return code of the command 
147
+# any error exiting settings or traps set by upstream callers by masking the return code of the command
148 148
 # with the return code of setting the result variable on failure.
149 149
 function os::cmd::internal::expect_exit_code_run_grep() {
150 150
 	local cmd=$1
151 151
 	# default expected cmd code to 0 for success
152 152
 	local cmd_eval_func=${2:-os::cmd::internal::success_func}
153
-	# default to nothing 
154
-	local grep_args=${3:-} 
153
+	# default to nothing
154
+	local grep_args=${3:-}
155 155
 	# default expected test code to 0 for success
156 156
 	local test_eval_func=${4:-os::cmd::internal::success_func}
157 157
 
... ...
@@ -168,7 +168,7 @@ function os::cmd::internal::expect_exit_code_run_grep() {
168 168
 	local test_result=0
169 169
 	if [[ -n "${grep_args}" ]]; then
170 170
 		test_result=$( os::cmd::internal::run_collecting_output 'os::cmd::internal::get_results | grep -Eq "${grep_args}"'; echo $? )
171
-		
171
+
172 172
 	fi
173 173
 	local test_succeeded=$( ${test_eval_func} "${test_result}"; echo $? )
174 174
 
... ...
@@ -189,21 +189,21 @@ function os::cmd::internal::expect_exit_code_run_grep() {
189 189
 		return 0
190 190
 	else
191 191
 		local cause=$(os::cmd::internal::assemble_causes "${cmd_succeeded}" "${test_succeeded}")
192
-		
192
+
193 193
 		os::text::print_red_bold "FAILURE after ${time_elapsed}s: ${name}: ${cause}"
194 194
 		os::text::print_red "$(os::cmd::internal::print_results)"
195 195
 		return 1
196 196
 	fi
197 197
 }
198 198
 
199
-# os::cmd::internal::init_tempdir initializes the temporary directory 
199
+# os::cmd::internal::init_tempdir initializes the temporary directory
200 200
 function os::cmd::internal::init_tempdir() {
201 201
 	mkdir -p "${os_cmd_internal_tmpdir}"
202 202
 	rm -f "${os_cmd_internal_tmpdir}"/tmp_std{out,err}.log
203 203
 }
204 204
 
205 205
 # os::cmd::internal::describe_call determines the file:line of the latest function call made
206
-# from outside of this file in the call stack, and the name of the function being called from 
206
+# from outside of this file in the call stack, and the name of the function being called from
207 207
 # that line, returning a string describing the call
208 208
 function os::cmd::internal::describe_call() {
209 209
 	local cmd=$1
... ...
@@ -293,7 +293,7 @@ function os::cmd::internal::run_collecting_output() {
293 293
 	local result=${result:-0} # if we haven't set result yet, the command succeeded
294 294
 
295 295
 	return "${result}"
296
-} 
296
+}
297 297
 
298 298
 # os::cmd::internal::success_func determines if the input exit code denotes success
299 299
 # this function returns 0 for false and 1 for true to be compatible with arithmetic tests
... ...
@@ -335,18 +335,18 @@ function os::cmd::internal::get_results() {
335 335
 # using a timeline format, where consecutive output lines that are the same are condensed into one line
336 336
 # with a counter
337 337
 function os::cmd::internal::print_try_until_results() {
338
-	if grep -vq $'\x1e' "${os_cmd_internal_tmpout}"; then 
338
+	if grep -vq $'\x1e' "${os_cmd_internal_tmpout}"; then
339 339
 		echo "Standard output from the command:"
340 340
 		os::cmd::internal::compress_output "${os_cmd_internal_tmpout}"
341
-	else 
342
-		echo "There was no output from the command."                                      																																																																																																																
343
-	fi	
341
+	else
342
+		echo "There was no output from the command."
343
+	fi
344 344
 
345
-	if grep -vq $'\x1e' "${os_cmd_internal_tmperr}"; then 
345
+	if grep -vq $'\x1e' "${os_cmd_internal_tmperr}"; then
346 346
 		echo "Standard error from the command:"
347 347
 		os::cmd::internal::compress_output "${os_cmd_internal_tmperr}"
348
-	else 
349
-		echo "There was no error output from the command."                                      																																																																																																																
348
+	else
349
+		echo "There was no error output from the command."
350 350
 	fi
351 351
 }
352 352
 
... ...
@@ -365,19 +365,19 @@ function os::cmd::internal::compress_output() {
365 365
 
366 366
 # os::cmd::internal::print_results pretty-prints the stderr and stdout files
367 367
 function os::cmd::internal::print_results() {
368
-	if [[ -s "${os_cmd_internal_tmpout}" ]]; then 
368
+	if [[ -s "${os_cmd_internal_tmpout}" ]]; then
369 369
 		echo "Standard output from the command:"
370 370
 		cat "${os_cmd_internal_tmpout}"
371
-	else 
372
-		echo "There was no output from the command."                                      																																																																																																																
373
-	fi	
371
+	else
372
+		echo "There was no output from the command."
373
+	fi
374 374
 
375
-	if [[ -s "${os_cmd_internal_tmperr}" ]]; then 
375
+	if [[ -s "${os_cmd_internal_tmperr}" ]]; then
376 376
 		echo "Standard error from the command:"
377 377
 		cat "${os_cmd_internal_tmperr}"
378
-	else 
379
-		echo "There was no error output from the command."                                      																																																																																																																
380
-	fi	
378
+	else
379
+		echo "There was no error output from the command."
380
+	fi
381 381
 }
382 382
 
383 383
 # os::cmd::internal::assemble_causes determines from the two input booleans which part of the test
... ...
@@ -399,7 +399,7 @@ function os::cmd::internal::assemble_causes() {
399 399
 }
400 400
 
401 401
 
402
-# os::cmd::internal::run_until_exit_code runs the provided command until the exit code test given 
402
+# os::cmd::internal::run_until_exit_code runs the provided command until the exit code test given
403 403
 # succeeds or the timeout given runs out. Output from the command to be tested is suppressed unless
404 404
 # either `VERBOSE=1` or the test fails. This function bypasses any error exiting settings or traps
405 405
 # set by upstream callers by masking the return code of the command with the return code of setting
... ...
@@ -416,12 +416,12 @@ function os::cmd::internal::run_until_exit_code() {
416 416
 	local duration_seconds=$(echo "scale=3; $(( duration )) / 1000" | bc | xargs printf '%5.3f')
417 417
 	local description="${description}; re-trying every ${interval}s until completion or ${duration_seconds}s"
418 418
 	echo "Running ${description}..."
419
-	
419
+
420 420
 	local start_time=$(os::cmd::internal::seconds_since_epoch)
421 421
 
422 422
 	local deadline=$(( $(date +%s000) + $duration ))
423 423
 	local cmd_succeeded=0
424
-	while [ $(date +%s000) -lt $deadline ]; do	
424
+	while [ $(date +%s000) -lt $deadline ]; do
425 425
 		local cmd_result=$( os::cmd::internal::run_collecting_output "${cmd}"; echo $? )
426 426
 		cmd_succeeded=$( ${cmd_eval_func} "${cmd_result}"; echo $? )
427 427
 		if (( cmd_succeeded )); then
... ...
@@ -471,12 +471,12 @@ function os::cmd::internal::run_until_text() {
471 471
 	local duration_seconds=$(echo "scale=3; $(( duration )) / 1000" | bc | xargs printf '%5.3f')
472 472
 	local description="${description}; re-trying every ${interval}s until completion or ${duration_seconds}s"
473 473
 	echo "Running ${description}..."
474
-	
474
+
475 475
 	local start_time=$(os::cmd::internal::seconds_since_epoch)
476
-	
476
+
477 477
 	local deadline=$(( $(date +%s000) + $duration ))
478 478
 	local test_succeeded=0
479
-	while [ $(date +%s000) -lt $deadline ]; do	
479
+	while [ $(date +%s000) -lt $deadline ]; do
480 480
 		local cmd_result=$( os::cmd::internal::run_collecting_output "${cmd}"; echo $? )
481 481
 		local test_result=$( os::cmd::internal::run_collecting_output 'os::cmd::internal::get_results | grep -Eq "${text}"'; echo $? )
482 482
 		test_succeeded=$( os::cmd::internal::success_func "${test_result}"; echo $? )
... ...
@@ -32,7 +32,12 @@ if [[ ! -d "${relativedir}" ]]; then
32 32
 fi
33 33
 
34 34
 if [[ -z "${NO_REBASE-}" ]]; then
35
-  lastrev="$(go run ${OS_ROOT}/tools/godepversion/godepversion.go ${OS_ROOT}/Godeps/Godeps.json ${repo}/${package})"
35
+  if [[ "${package}" != "." ]]; then
36
+    out="${repo}/${package}"
37
+  else
38
+    out="${repo}"
39
+  fi
40
+  lastrev="$(go run ${OS_ROOT}/tools/godepversion/godepversion.go ${OS_ROOT}/Godeps/Godeps.json ${out})"
36 41
 fi
37 42
 
38 43
 branch="${TARGET_BRANCH:-$(git rev-parse --abbrev-ref HEAD)}"
39 44
new file mode 100755
... ...
@@ -0,0 +1,41 @@
0
+#!/bin/bash
1
+
2
+set -o errexit
3
+set -o pipefail
4
+
5
+IMAGE=atomic-registry-quickstart
6
+docker build -t $IMAGE ../images/atomic-registry-quickstart/.
7
+
8
+set -x
9
+
10
+function install() {
11
+  INSTALL=$(docker inspect -f '{{ .ContainerConfig.Labels.INSTALL }}' $IMAGE)
12
+  # We need $IMAGE string replaced with the image we built here
13
+  ${INSTALL//\$IMAGE/$IMAGE}
14
+}
15
+
16
+function run() {
17
+  RUN=$(docker inspect -f '{{ .ContainerConfig.Labels.RUN }}' $IMAGE)
18
+  ${RUN//\$IMAGE/$IMAGE}
19
+}
20
+
21
+function stop() {
22
+  STOP=$(docker inspect -f '{{ .ContainerConfig.Labels.STOP }}' $IMAGE)
23
+  ${STOP//\$IMAGE/$IMAGE}
24
+}
25
+
26
+function uninstall() {
27
+  UNINSTALL=$(docker inspect -f '{{ .ContainerConfig.Labels.UNINSTALL }}' $IMAGE)
28
+  ${UNINSTALL//\$IMAGE/$IMAGE}
29
+}
30
+
31
+if [ ! -z $1 ] ; then
32
+  $1
33
+  exit
34
+fi
35
+
36
+install
37
+run
38
+stop
39
+uninstall
40
+
... ...
@@ -24,6 +24,14 @@ function cleanup()
24 24
     set +e
25 25
     kill_all_processes
26 26
 
27
+    echo "[INFO] Dumping etcd contents to ${ARTIFACT_DIR}/etcd_dump.json"
28
+    set_curl_args 0 1
29
+    curl -s ${clientcert_args} -L "${API_SCHEME}://${API_HOST}:${ETCD_PORT}/v2/keys/?recursive=true" > "${ARTIFACT_DIR}/etcd_dump.json"
30
+    echo
31
+
32
+    # we keep a JSON dump of etcd data so we do not need to keep the binary store
33
+    rm -rf "${ETCD_DATA_DIR}"
34
+
27 35
     if [ $out -ne 0 ]; then
28 36
         echo "[FAIL] !!!!! Test Failed !!!!"
29 37
         echo
30 38
new file mode 100644
... ...
@@ -0,0 +1,46 @@
0
+FROM openshift/origin
1
+MAINTAINER Aaron Weitekamp <aweiteka@redhat.com>
2
+
3
+LABEL name="projectatomic/atomic-registry-quickstart" \
4
+      vendor="Project Atomic" \
5
+      url="https://projectatomic.io/registry" \
6
+      summary="Quickstart image for Atomic Registry" \
7
+      description="Atomic Registry is an open source enterprise registry solution based on the Origin and Cockpit projects featuring single sign-on (SSO) user experience, a robust web interface and advanced role-based access control (RBAC)."
8
+
9
+ADD install.sh run.sh uninstall.sh stop.sh /container/bin/
10
+ADD atomic-openshift-master.service /container/etc/systemd/system/
11
+ADD atomic-openshift-master /container/etc/sysconfig/
12
+ADD registry-ui-template.json /container/etc/origin/
13
+
14
+LABEL INSTALL='docker run -it --rm \
15
+                --privileged --net=host \
16
+                -v /var/run:/var/run:rw \
17
+                -v /sys:/sys \
18
+                -v /etc/localtime:/etc/localtime:ro \
19
+                -v /var/lib/docker:/var/lib/docker:rw \
20
+                -v /var/lib/origin/:/var/lib/origin/ \
21
+                -v /etc/origin/:/etc/origin/ \
22
+                -v /:/host \
23
+                -e KUBECONFIG=/etc/origin/master/admin.kubeconfig \
24
+                --entrypoint /container/bin/install.sh \
25
+                $IMAGE' \
26
+      RUN='docker run -it --rm --privileged \
27
+                --net=host \
28
+                -v /:/host \
29
+                -v /var/lib/docker:/var/lib/docker:rw \
30
+                -v /etc/origin:/etc/origin \
31
+                -v /var/lib/registry:/var/lib/registry \
32
+                -e KUBECONFIG=/etc/origin/master/admin.kubeconfig \
33
+                --entrypoint /container/bin/run.sh \
34
+                $IMAGE' \
35
+      STOP='docker run -it --rm --privileged \
36
+                --net=host \
37
+                -v /:/host \
38
+                -e KUBECONFIG=/etc/origin/master/admin.kubeconfig \
39
+                --entrypoint /container/bin/stop.sh \
40
+                $IMAGE' \
41
+      UNINSTALL='docker run -it --rm --privileged \
42
+                -v /:/host \
43
+                --entrypoint /container/bin/uninstall.sh \
44
+                $IMAGE'
45
+
0 46
new file mode 100644
... ...
@@ -0,0 +1,41 @@
0
+# Getting Started With Atomic Registry
1
+
2
+http://projectatomic.io/registry
3
+
4
+**Requirements**
5
+
6
+- Red Hat-based system (RHEL, Centos, Fedora, including Atomic)
7
+- Docker
8
+- atomic cli
9
+
10
+## Install and Run
11
+
12
+1. Install the system service files and pull images.
13
+
14
+        sudo atomic install atomic-registry-quickstart
15
+1. Optional: edit configuration file `/etc/origin/master/master-config.yaml`.
16
+1. Run the application. This will enable and start the docker containers as system services.
17
+
18
+        sudo atomic run atomic-registry-quickstart
19
+
20
+## Stopping the application
21
+
22
+`sudo atomic stop atomic-registry-quickstart`
23
+
24
+## Uninstall
25
+
26
+`sudo atomic uninstall atomic-registry-quickstart`
27
+
28
+# Additional Setup steps
29
+
30
+1. [Configure authentication](https://docs.openshift.org/latest/install_config/configuring_authentication.html)
31
+1. [Configure persistent registry storage](https://docs.openshift.org/latest/install_config/install/docker_registry.html#advanced-overriding-the-registry-configuration)
32
+1. [Assign a user cluster-admin privilege](https://docs.openshift.org/latest/admin_guide/manage_authorization_policy.html#managing-role-bindings)
33
+1. Explore the web UI
34
+1. Create a project and [push an image](https://docs.openshift.org/latest/install_config/install/docker_registry.html#access-logging-in-to-the-registry)
35
+
36
+## Reference Documentation
37
+
38
+https://docs.openshift.org/latest/welcome/index.html
39
+
40
+
0 41
new file mode 100644
... ...
@@ -0,0 +1,9 @@
0
+OPTIONS=--loglevel=2
1
+CONFIG_FILE=/etc/origin/master/master-config.yaml
2
+
3
+# Proxy configuration
4
+# Origin uses standard HTTP_PROXY environment variables. Be sure to set
5
+# NO_PROXY for your master
6
+#NO_PROXY=master.example.com
7
+#HTTP_PROXY=http://USER:PASSWORD@IPADDR:PORT
8
+#HTTPS_PROXY=https://USER:PASSWORD@IPADDR:PORT
0 9
new file mode 100644
... ...
@@ -0,0 +1,18 @@
0
+[Unit]
1
+Description=Atomic Registry origin master
2
+Documentation=https://github.com/openshift/origin
3
+After=docker.service
4
+Requires=docker.service
5
+PartOf=docker.service
6
+
7
+[Service]
8
+EnvironmentFile=/etc/sysconfig/atomic-openshift-master
9
+# this fails unless it exists. necessary for cleanup?
10
+ExecStartPre=-/usr/bin/docker rm -f origin-master
11
+ExecStart=/usr/bin/docker run --restart=no --rm --privileged --net=host --pid=host --name origin-master -v /:/rootfs:ro -v /dev:/dev -v /var/run:/var/run:rw -v /var/lib/kubelet/:/var/lib/kubelet:rw -e CONFIG_FILE=${CONFIG_FILE} -e OPTIONS=${OPTIONS} -e HOST=/rootfs -e HOST_ETC=/host-etc -v /etc/localtime:/etc/localtime:ro -v /etc/machine-id:/etc/machine-id:ro -v /run:/run -v /sys:/sys:ro -v /usr/bin/docker:/usr/bin/docker:ro -v /var/lib/docker:/var/lib/docker -v /lib/modules:/lib/modules -v /etc/origin/openvswitch:/etc/openvswitch -v /etc/origin/sdn:/etc/openshift-sdn -v /etc/systemd/system:/host-etc/systemd/system -v /etc/origin/:/etc/origin/ -v /var/lib/origin:/var/lib/origin -e KUBECONFIG=/etc/origin/master/admin.kubeconfig openshift/origin start --master-config /etc/origin/master/master-config.yaml --node-config=/etc/origin/node/node-config.yaml --latest-images=true
12
+ExecStartPost=/usr/bin/sleep 10
13
+ExecStop=/usr/bin/docker stop origin-master
14
+Restart=always
15
+
16
+[Install]
17
+WantedBy=multi-user.target
0 18
new file mode 100755
... ...
@@ -0,0 +1,39 @@
0
+#!/bin/bash
1
+
2
+SERVICES=(atomic-openshift-master)
3
+for SERVICE in "${SERVICES[@]}"
4
+do
5
+  echo "Installing system service ${SERVICE}..."
6
+  cp /container/etc/systemd/system/${SERVICE}.service /host/etc/systemd/system/${SERVICE}.service
7
+  cp /container/etc/sysconfig/${SERVICE} /host/etc/sysconfig/${SERVICE}
8
+done
9
+
10
+chroot /host systemctl daemon-reload
11
+
12
+IMAGES=(openshift/origin openshift/origin-docker-registry aweiteka/cockpit-registry:0.95)
13
+
14
+for IMAGE in "${IMAGES[@]}"
15
+do
16
+  chroot /host docker pull $IMAGE
17
+done
18
+
19
+# write out configuration
20
+openshift start --write-config /etc/origin/ --etcd-dir /var/lib/origin/etcd --volume-dir /var/lib/origin/volumes --public-master `hostname`
21
+
22
+echo "Moving node directory to /etc/origin/node"
23
+mv /host/etc/origin/node* /host/etc/origin/node
24
+
25
+# Copy install script to host
26
+mkdir -p /host/etc/origin/registry/bin
27
+cp /container/bin/* /host/etc/origin/registry/bin/.
28
+cp /container/etc/origin/registry-ui-template.json /host/etc/origin/registry/registry-ui-template.json
29
+
30
+echo "Creating registry UI service certificates..."
31
+cat /etc/origin/master/master.server.crt /etc/origin/master/master.server.key > /etc/origin/registry/master.server.cert
32
+
33
+echo "Updating servicesNodePortRange to 443-32767..."
34
+sed -i 's/  servicesNodePortRange:.*$/  servicesNodePortRange: 443-32767/' /etc/origin/master/master-config.yaml
35
+
36
+echo "Optionally edit configuration file /etc/origin/master/master-config.yaml,"
37
+echo "add certificates to /etc/origin/master,"
38
+echo "then run 'atomic run atomic-registry-quickstart'"
0 39
new file mode 100644
... ...
@@ -0,0 +1,137 @@
0
+{
1
+   "kind": "Template",
2
+   "apiVersion": "v1",
3
+   "metadata": {
4
+      "name": "cockpit-openshift-template"
5
+   },
6
+   "labels": {
7
+      "createdBy": "cockpit-openshift-template"
8
+   },
9
+   "parameters": [
10
+      {
11
+         "description": "The public url for the Openshift OAuth Provider",
12
+         "name": "OPENSHIFT_OAUTH_PROVIDER_URL",
13
+         "required": true
14
+      },
15
+      {
16
+         "description": "The public url for the Openshift OAuth Provider",
17
+         "name": "COCKPIT_KUBE_URL",
18
+         "required": true
19
+      },
20
+      {
21
+         "description": "The public url for the Openshift OAuth Provider",
22
+         "name": "COCKPIT_KUBE_INSECURE",
23
+         "required": false
24
+      },
25
+      {
26
+         "description": "Oauth client secret",
27
+         "name": "OPENSHIFT_OAUTH_CLIENT_SECRET",
28
+         "from": "user[a-zA-Z0-9]{64}",
29
+         "generate": "expression"
30
+      },
31
+      {
32
+         "description": "Oauth client id",
33
+         "name": "OPENSHIFT_OAUTH_CLIENT_ID",
34
+         "value": "cockpit-oauth-client"
35
+      },
36
+      {
37
+         "description": "Skip kubernetes CA verification",
38
+         "name": "KUBERNETES_INSECURE",
39
+         "value": ""
40
+      },
41
+      {
42
+         "description": "PEM Encoded certificate to use for CA verification",
43
+         "name": "KUBERNETES_CA_DATA",
44
+         "value": ""
45
+      }
46
+   ],
47
+   "objects": [
48
+      {
49
+         "kind":"DeploymentConfig",
50
+         "apiVersion":"v1",
51
+         "metadata":{
52
+            "name":"cockpit-kube",
53
+            "labels":{
54
+               "name":"cockpit-kube"
55
+            }
56
+         },
57
+         "spec":{
58
+            "replicas":1,
59
+            "selector":{
60
+               "name":"cockpit-kube"
61
+            },
62
+            "template":{
63
+               "metadata":{
64
+                  "labels":{
65
+                     "name":"cockpit-kube"
66
+                  }
67
+               },
68
+               "spec":{
69
+                  "containers":[{
70
+                    "name": "cockpit-kube",
71
+                    "image": "aweiteka/cockpit-registry:0.95",
72
+                    "ports":[{
73
+                        "containerPort":9090,
74
+                        "protocol":"TCP"
75
+                     }],
76
+                    "env":[
77
+                      {
78
+                        "name": "OPENSHIFT_OAUTH_PROVIDER_URL",
79
+                        "value": "${OPENSHIFT_OAUTH_PROVIDER_URL}"
80
+                      },
81
+                      {
82
+                        "name": "OPENSHIFT_OAUTH_CLIENT_ID",
83
+                        "value": "${OPENSHIFT_OAUTH_CLIENT_ID}"
84
+                      },
85
+                      {
86
+                        "name": "KUBERNETES_INSECURE",
87
+                        "value": "${KUBERNETES_INSECURE}"
88
+                      },
89
+                      {
90
+                        "name": "KUBERNETES_CA_DATA",
91
+                        "value": "${KUBERNETES_CA_DATA}"
92
+                      },
93
+                      {
94
+                        "name": "COCKPIT_KUBE_INSECURE",
95
+                        "value": "${COCKPIT_KUBE_INSECURE}"
96
+                      }]
97
+                  }]
98
+               }
99
+            }
100
+         }
101
+      },
102
+      {
103
+         "kind":"Service",
104
+         "apiVersion":"v1",
105
+         "metadata":{
106
+            "name":"cockpit-kube",
107
+            "labels":{
108
+               "name":"cockpit-kube"
109
+            }
110
+         },
111
+         "spec":{
112
+             "type": "ClusterIP",
113
+             "ports": [{
114
+                 "protocol": "TCP",
115
+                 "port": 9000,
116
+                 "targetPort": 9090
117
+             }],
118
+            "selector":{
119
+               "name":"cockpit-kube"
120
+            }
121
+         }
122
+      },
123
+      {
124
+        "kind": "OAuthClient",
125
+        "apiVersion": "v1",
126
+        "metadata": {
127
+          "name": "${OPENSHIFT_OAUTH_CLIENT_ID}"
128
+        },
129
+        "respondWithChallenges": false,
130
+        "secret": "${OPENSHIFT_OAUTH_CLIENT_SECRET}",
131
+        "redirectURIs": [
132
+            "${COCKPIT_KUBE_URL}"
133
+        ]
134
+      }
135
+   ]
136
+}
0 137
new file mode 100755
... ...
@@ -0,0 +1,51 @@
0
+#!/bin/bash
1
+
2
+SERVICES=(atomic-openshift-master)
3
+
4
+# enable and start services
5
+
6
+for SERVICE in "${SERVICES[@]}"
7
+do
8
+  echo "Starting service ${SERVICE}..."
9
+  chroot /host systemctl enable $SERVICE.service
10
+  chroot /host systemctl start $SERVICE.service
11
+done
12
+
13
+# TODO: loop until running...
14
+echo "Waiting for services to come up..."
15
+until curl -kLs https://`hostname`:8443/api
16
+do
17
+  printf "."
18
+  sleep 1
19
+done
20
+
21
+CMD="chroot /host docker exec -it origin-master"
22
+# TODO: this needs to be smarter. it will fail on openstack instance with floating IP
23
+IPADDR=`hostname -I | awk '{print $1}'`
24
+echo "Starting registry services..."
25
+
26
+set -x
27
+
28
+$CMD oadm registry --credentials /etc/origin/master/openshift-registry.kubeconfig
29
+$CMD oc patch service docker-registry -p \
30
+     '{ "spec": { "type": "NodePort", "ports": [ {"nodePort": 5000, "port": 5000, "targetPort": 5000}] }}'
31
+
32
+set +x
33
+echo "Starting web UI service..."
34
+HOSTNAME=`hostname`
35
+
36
+# TODO: use master cert from /etc/origin/registry/master.server.cert
37
+# create secret volume
38
+# mounted at /etc/cockpit/ws-certs.d/master.server.cert
39
+# use secret volume in template
40
+
41
+set -x
42
+$cmd oc create -f /host/etc/origin/registry/registry-ui-template.json
43
+$cmd oc new-app --template cockpit-openshift-template \
44
+     -p OPENSHIFT_OAUTH_PROVIDER_URL=https://${HOSTNAME}:8443,COCKPIT_KUBE_URL=https://${HOSTNAME}
45
+$CMD oc patch service cockpit-kube -p \
46
+     '{ "spec": { "type": "NodePort", "ports": [ {"nodePort": 443, "port": 9000, "targetPort": 9090}] }}'
47
+
48
+set +x
49
+echo "Web UI hosted at https://${HOSTNAME}"
50
+
0 51
new file mode 100755
... ...
@@ -0,0 +1,20 @@
0
+#!/bin/bash
1
+
2
+
3
+CMD="chroot /host docker exec -it origin-master"
4
+
5
+PODS=(docker-registry cockpit-kube)
6
+for POD in "${PODS[@]}"
7
+do
8
+  echo "Scaling down ${POD}"
9
+  $CMD oc scale dc ${POD} --replicas=0
10
+done
11
+
12
+SERVICES=(atomic-openshift-master)
13
+for SERVICE in "${SERVICES[@]}"
14
+do
15
+  echo "Stopping and disabling system service ${SERVICE}..."
16
+  chroot /host systemctl stop $SERVICE.service
17
+  chroot /host systemctl disable $SERVICE.service
18
+done
19
+
0 20
new file mode 100755
... ...
@@ -0,0 +1,24 @@
0
+#!/bin/bash
1
+
2
+SERVICES=(atomic-openshift-master)
3
+for SERVICE in "${SERVICES[@]}"
4
+do
5
+  echo "uninstalling system service ${SERVICE}..."
6
+  chroot /host rm /etc/systemd/system/${SERVICE}.service
7
+  chroot /host rm /etc/sysconfig/${SERVICE}
8
+done
9
+
10
+echo "Removing configuration files..."
11
+DIRS=(/etc/origin /var/lib/origin)
12
+for DIR in "${DIRS[@]}"
13
+do
14
+  chroot /host rm -rf ${DIR}
15
+done
16
+
17
+IMAGES=(openshift/origin openshift/origin-docker-registry cockpit/kubernetes)
18
+#aweiteka/cockpit-registry
19
+
20
+echo "Uninstallation complete."
21
+echo "Stopped container and images have not been removed. To remove them manually run:"
22
+echo "'sudo docker rm \$(sudo docker ps -qa)'"
23
+echo "'sudo docker rmi ${IMAGES[*]}'"
... ...
@@ -6,7 +6,8 @@
6 6
 #
7 7
 FROM rhel7
8 8
 
9
-RUN INSTALL_PKGS="which git tar wget socat hostname sysvinit-tools util-linux ethtool bsdtar" && \
9
+RUN INSTALL_PKGS="which git tar wget hostname sysvinit-tools util-linux bsdtar \
10
+    socat ethtool device-mapper iptables e2fsprogs xfsprogs" && \
10 11
     yum install -y $INSTALL_PKGS && \
11 12
     rpm -V $INSTALL_PKGS && \
12 13
     yum clean all
... ...
@@ -15,8 +15,9 @@ MAINTAINER Devan Goodwin <dgoodwin@redhat.com>
15 15
 
16 16
 ADD https://copr.fedoraproject.org/coprs/maxamillion/origin-next/repo/epel-7/maxamillion-origin-next-epel-7.repo /etc/yum.repos.d/
17 17
 RUN INSTALL_PKGS="libmnl libnetfilter_conntrack openvswitch \
18
-      libnfnetlink iptables iproute bridge-utils procps-ng ethtool socat openssl \
19
-      binutils xz kmod-libs kmod sysvinit-tools device-mapper-libs dbus" && \
18
+    libnfnetlink iptables iproute bridge-utils procps-ng ethtool socat openssl \
19
+    binutils xz kmod-libs kmod sysvinit-tools device-mapper-libs dbus \
20
+    ceph-common iscsi-initiator-utils" && \
20 21
     yum install -y $INSTALL_PKGS && \
21 22
     rpm -V $INSTALL_PKGS && \
22 23
     yum clean all
... ...
@@ -2,6 +2,10 @@
2 2
 # This is the official OpenShift Origin image. It has as its entrypoint the OpenShift
3 3
 # all-in-one binary.
4 4
 #
5
+# While this image can be used for a simple node it does not support OVS based
6
+# SDN or storage plugins required for EBS, GCE, Gluster, Ceph, or iSCSI volume
7
+# management. For those features please use 'openshift/node' 
8
+#
5 9
 # The standard name for this image is openshift/origin
6 10
 #
7 11
 FROM openshift/origin-base
... ...
@@ -6,24 +6,40 @@ config_file=/var/lib/haproxy/conf/haproxy.config
6 6
 pid_file=/var/lib/haproxy/run/haproxy.pid
7 7
 old_pid=""
8 8
 haproxy_conf_dir=/var/lib/haproxy/conf
9
-readonly max_retries=30
9
+readonly max_wait_time=30
10
+readonly timeout_opts="-m 1 --connect-timeout 1"
11
+readonly numeric_re='^[0-9]+$'
10 12
 
11
-function waitForHAProxyToStartListening() {
13
+function haproxyHealthCheck() {
14
+  local wait_time=${MAX_RELOAD_WAIT_TIME:-$max_wait_time}
12 15
   local port=${STATS_PORT:-"1936"}
13 16
   local retries=0
17
+  local start_ts=$(date +"%s")
14 18
 
15
-  echo " - Checking if HAProxy is listening on port $port ..."
16
-  while ! lsof -nPp $(< $pid_file) | grep ":${port} (LISTEN)" &> /dev/null ; do
17
-    sleep 0.2
18
-    retries=$((retries + 1))
19
-    if [ $retries -ge $max_retries ]; then
20
-      echo " - Exceeded $max_retries retries waiting for HAProxy to start listening on port $port"
21
-      return
19
+  if ! [[ $wait_time =~ $numeric_re ]]; then
20
+    echo " - Invalid max reload wait time, using default $max_wait_time ..."
21
+    wait_time=$max_wait_time
22
+  fi
23
+
24
+  local end_ts=$((start_ts + wait_time))
25
+
26
+  echo " - Checking HAProxy /healthz on port $port ..."
27
+  while true; do
28
+    local httpcode=$(curl $timeout_opts -s -o /dev/null -I -w "%{http_code}" http://localhost:${port}/healthz)
29
+
30
+    if [ "$httpcode" == "200" ]; then
31
+      echo " - HAProxy port $port health check ok : $retries retry attempt(s)."
32
+      return 0
33
+    fi
34
+
35
+    if [ $(date +"%s") -ge $end_ts ]; then
36
+      echo " - Exceeded max wait time ($wait_time) in HAProxy health check - $retries retry attempt(s)."
37
+      return 1
22 38
     fi
23
-  done
24 39
 
25
-  echo " - HAProxy listening on port $port : $retries retry attempt(s)."
26
-  return 0
40
+    sleep 0.5
41
+    retries=$((retries + 1))
42
+  done
27 43
 }
28 44
 
29 45
 
... ...
@@ -97,4 +113,4 @@ else
97 97
 fi
98 98
 
99 99
 [ $reload_status -ne 0 ] && exit $reload_status
100
-waitForHAProxyToStartListening
100
+haproxyHealthCheck
101 101
new file mode 100644
... ...
@@ -0,0 +1,39 @@
0
+package graphview
1
+
2
+import (
3
+	osgraph "github.com/openshift/origin/pkg/api/graph"
4
+	kubegraph "github.com/openshift/origin/pkg/api/kubegraph/nodes"
5
+)
6
+
7
+type Pod struct {
8
+	Pod *kubegraph.PodNode
9
+}
10
+
11
+// AllPods returns all Pods and the set of covered NodeIDs
12
+func AllPods(g osgraph.Graph, excludeNodeIDs IntSet) ([]Pod, IntSet) {
13
+	covered := IntSet{}
14
+	pods := []Pod{}
15
+
16
+	for _, uncastNode := range g.NodesByKind(kubegraph.PodNodeKind) {
17
+		if excludeNodeIDs.Has(uncastNode.ID()) {
18
+			continue
19
+		}
20
+
21
+		pod, covers := NewPod(g, uncastNode.(*kubegraph.PodNode))
22
+		covered.Insert(covers.List()...)
23
+		pods = append(pods, pod)
24
+	}
25
+
26
+	return pods, covered
27
+}
28
+
29
+// NewPod returns the Pod and a set of all the NodeIDs covered by the Pod
30
+func NewPod(g osgraph.Graph, podNode *kubegraph.PodNode) (Pod, IntSet) {
31
+	covered := IntSet{}
32
+	covered.Insert(podNode.ID())
33
+
34
+	podView := Pod{}
35
+	podView.Pod = podNode
36
+
37
+	return podView, covered
38
+}
... ...
@@ -97,6 +97,11 @@ func NewServiceGroup(g osgraph.Graph, serviceNode *kubegraph.ServiceNode) (Servi
97 97
 		service.ReplicationControllers = append(service.ReplicationControllers, rcView)
98 98
 	}
99 99
 
100
+	for _, fulfillingPod := range service.FulfillingPods {
101
+		_, podCovers := NewPod(g, fulfillingPod)
102
+		covered.Insert(podCovers.List()...)
103
+	}
104
+
100 105
 	return service, covered
101 106
 }
102 107
 
... ...
@@ -4,7 +4,7 @@ import (
4 4
 	"fmt"
5 5
 	"time"
6 6
 
7
-	. "github.com/MakeNowJust/heredoc/dot"
7
+	"github.com/MakeNowJust/heredoc"
8 8
 
9 9
 	kapi "k8s.io/kubernetes/pkg/api"
10 10
 	"k8s.io/kubernetes/pkg/api/unversioned"
... ...
@@ -44,7 +44,7 @@ func FindRestartingPods(g osgraph.Graph, f osgraph.Namer, logsCommandName, secur
44 44
 				var suggestion string
45 45
 				switch {
46 46
 				case containerIsNonRoot(pod, containerStatus.Name):
47
-					suggestion = Df(`
47
+					suggestion = heredoc.Docf(`
48 48
 						The container is starting and exiting repeatedly. This usually means the container is unable
49 49
 						to start, misconfigured, or limited by security restrictions. Check the container logs with
50 50
 
... ...
@@ -57,7 +57,7 @@ func FindRestartingPods(g osgraph.Graph, f osgraph.Namer, logsCommandName, secur
57 57
 						  %s
58 58
 						`, logsCommandName, pod.Name, containerStatus.Name, fmt.Sprintf(securityPolicyCommandPattern, pod.Namespace, pod.Spec.ServiceAccountName))
59 59
 				default:
60
-					suggestion = Df(`
60
+					suggestion = heredoc.Docf(`
61 61
 						The container is starting and exiting repeatedly. This usually means the container is unable
62 62
 						to start, misconfigured, or limited by security restrictions. Check the container logs with
63 63
 
... ...
@@ -539,7 +539,21 @@ var _indexHtml = []byte(`<!doctype html>
539 539
 <body class="console-os">
540 540
 
541 541
 <div ng-view></div>
542
-<noscript class="attention-message"><h1>To use OpenShift, please enable JavaScript.</h1></noscript>
542
+<noscript>
543
+<nav class="navbar navbar-pf-alt" role="navigation">
544
+<div row>
545
+<div class="navbar-header">
546
+<a class="navbar-brand" id="openshift-logo" href="./">
547
+<div id="header-logo"></div>
548
+</a>
549
+</div>
550
+</div>
551
+</nav>
552
+<div class="attention-message">
553
+<h1>JavaScript Required</h1>
554
+<p>The OpenShift web console requires JavaScript to provide a rich interactive experience. Please enable JavaScript to continue. If you do not wish to enable JavaScript or are unable to do so, you may use the command-line tools to manage your projects and applications instead.</p>
555
+</div>
556
+</noscript>
543 557
 <script src="config.js"></script>
544 558
 <!--[if lt IE 9]><script src="scripts/oldieshim.js"></script><![endif]-->
545 559
 <script src="scripts/vendor.js"></script>
... ...
@@ -2306,7 +2320,7 @@ return c.project = a, c.projectPromise.resolve(a), [ a, c ];
2306 2306
 }, function(b) {
2307 2307
 c.projectPromise.reject(b);
2308 2308
 var d = "The project could not be loaded.", e = "error";
2309
-403 === b.status ? (d = "The project " + c.projectName + " does not exist or you are not authorized to view it.", e = "access_denied") :404 === b.status && (d = 'The project " + context.projectName + " does not exist.', e = "not_found"), a.url(URI("error").query({
2309
+403 === b.status ? (d = "The project " + c.projectName + " does not exist or you are not authorized to view it.", e = "access_denied") :404 === b.status && (d = "The project " + c.projectName + " does not exist.", e = "not_found"), a.url(URI("error").query({
2310 2310
 error:e,
2311 2311
 error_description:d
2312 2312
 }).toString());
... ...
@@ -6021,7 +6035,8 @@ templateUrl:"views/directives/osc-secondary-nav.html"
6021 6021
 return {
6022 6022
 restrict:"E",
6023 6023
 scope:{
6024
-alerts:"="
6024
+alerts:"=",
6025
+hideCloseButton:"=?"
6025 6026
 },
6026 6027
 templateUrl:"views/_alerts.html"
6027 6028
 };
... ...
@@ -6567,7 +6582,9 @@ k && (a.cancel(k), k = null);
6567 6567
 } ]), angular.module("openshiftConsole").directive("logViewer", [ "$sce", "$timeout", "$window", "AuthService", "APIDiscovery", "DataService", "logLinks", "BREAKPOINTS", function(a, b, c, d, e, f, g, h) {
6568 6568
 var i = $(window), j = $('<tr class="log-line"><td class="log-line-number"></td><td class="log-line-text"></td></tr>').get(0), k = function(a, b) {
6569 6569
 var c = j.cloneNode(!0);
6570
-return c.firstChild.setAttribute("data-line-number", a), c.lastChild.appendChild(document.createTextNode(b)), c;
6570
+c.firstChild.setAttribute("data-line-number", a);
6571
+var d = ansi_up.escape_for_html(b), e = ansi_up.ansi_to_html(d), f = ansi_up.linkify(e);
6572
+return c.lastChild.innerHTML = f, c;
6571 6573
 };
6572 6574
 return {
6573 6575
 restrict:"AE",
... ...
@@ -6842,7 +6859,9 @@ e(h(c.total, c.type, !0));
6842 6842
 }
6843 6843
 var f = a("usageValue"), g = a("usageWithUnits"), h = a("amountAndUnit");
6844 6844
 "right" === c.legendPosition ? (c.height = 175, c.width = 250) :(c.height = 200, c.width = 175);
6845
-var i = d3.format(".2p");
6845
+var i = function(a) {
6846
+return a ? (100 * Number(a)).toFixed(1) + "%" :"0%";
6847
+};
6846 6848
 c.chartID = _.uniqueId("quota-usage-chart-");
6847 6849
 var j, k = {
6848 6850
 type:"donut",
... ...
@@ -7645,8 +7664,10 @@ return function(a) {
7645 7645
 if (!a) return a;
7646 7646
 var b = {
7647 7647
 configmaps:"Config Maps",
7648
+cpu:"CPU (Request)",
7648 7649
 "limits.cpu":"CPU (Limit)",
7649 7650
 "limits.memory":"Memory (Limit)",
7651
+memory:"Memory (Request)",
7650 7652
 "openshift.io/imagesize":"Image Size",
7651 7653
 "openshift.io/imagestreamsize":"Image Stream Size",
7652 7654
 "openshift.io/projectimagessize":"Project Image Size",
... ...
@@ -8086,7 +8107,7 @@ var _scriptsTemplatesJs = []byte(`angular.module('openshiftConsoleTemplates', []
8086 8086
     "    'alert-success': alert.type === 'success',\n" +
8087 8087
     "    'alert-info': !alert.type || alert.type === 'info'\n" +
8088 8088
     "  }\">\n" +
8089
-    "<button ng-click=\"alert.hidden = true\" type=\"button\" class=\"close\">\n" +
8089
+    "<button ng-if=\"!hideCloseButton\" ng-click=\"alert.hidden = true\" type=\"button\" class=\"close\">\n" +
8090 8090
     "<span class=\"pficon pficon-close\" aria-hidden=\"true\"></span>\n" +
8091 8091
     "<span class=\"sr-only\">Close</span>\n" +
8092 8092
     "</button>\n" +
... ...
@@ -8699,7 +8720,8 @@ var _scriptsTemplatesJs = []byte(`angular.module('openshiftConsoleTemplates', []
8699 8699
     "</a>\n" +
8700 8700
     "</div>\n" +
8701 8701
     "<div ng-show=\"!task.hasErrors || expanded\">\n" +
8702
-    "<alerts alerts=\"task.alerts\"></alerts>\n" +
8702
+    "\n" +
8703
+    "<alerts alerts=\"task.alerts\" hide-close-button=\"true\"></alerts>\n" +
8703 8704
     "</div>\n" +
8704 8705
     "</div>\n" +
8705 8706
     "</div>\n" +
... ...
@@ -14069,6 +14091,9 @@ var _scriptsTemplatesJs = []byte(`angular.module('openshiftConsoleTemplates', []
14069 14069
   $templateCache.put('views/util/error.html',
14070 14070
     "<default-header class=\"top-header\"></default-header>\n" +
14071 14071
     "<div class=\"wrap no-sidebar\">\n" +
14072
+    "<div class=\"sidebar-left collapse navbar-collapse navbar-collapse-2\">\n" +
14073
+    "<navbar-utility-mobile></navbar-utility-mobile>\n" +
14074
+    "</div>\n" +
14072 14075
     "<div class=\"middle surface-shaded\">\n" +
14073 14076
     "<div class=\"container surface-shaded\">\n" +
14074 14077
     "<div>\n" +
... ...
@@ -60329,7 +60354,152 @@ e.appendSegment(g);
60329 60329
 }
60330 60330
 return e.pathSegList;
60331 60331
 });
60332
-}();`)
60332
+}(), function(a, b) {
60333
+function c() {
60334
+this.fg = this.bg = this.fg_truecolor = this.bg_truecolor = null, this.bright = 0;
60335
+}
60336
+var d, e, f = "undefined" != typeof module, g = [ [ {
60337
+color:"0, 0, 0",
60338
+"class":"ansi-black"
60339
+}, {
60340
+color:"187, 0, 0",
60341
+"class":"ansi-red"
60342
+}, {
60343
+color:"0, 187, 0",
60344
+"class":"ansi-green"
60345
+}, {
60346
+color:"187, 187, 0",
60347
+"class":"ansi-yellow"
60348
+}, {
60349
+color:"0, 0, 187",
60350
+"class":"ansi-blue"
60351
+}, {
60352
+color:"187, 0, 187",
60353
+"class":"ansi-magenta"
60354
+}, {
60355
+color:"0, 187, 187",
60356
+"class":"ansi-cyan"
60357
+}, {
60358
+color:"255,255,255",
60359
+"class":"ansi-white"
60360
+} ], [ {
60361
+color:"85, 85, 85",
60362
+"class":"ansi-bright-black"
60363
+}, {
60364
+color:"255, 85, 85",
60365
+"class":"ansi-bright-red"
60366
+}, {
60367
+color:"0, 255, 0",
60368
+"class":"ansi-bright-green"
60369
+}, {
60370
+color:"255, 255, 85",
60371
+"class":"ansi-bright-yellow"
60372
+}, {
60373
+color:"85, 85, 255",
60374
+"class":"ansi-bright-blue"
60375
+}, {
60376
+color:"255, 85, 255",
60377
+"class":"ansi-bright-magenta"
60378
+}, {
60379
+color:"85, 255, 255",
60380
+"class":"ansi-bright-cyan"
60381
+}, {
60382
+color:"255, 255, 255",
60383
+"class":"ansi-bright-white"
60384
+} ] ];
60385
+c.prototype.setup_palette = function() {
60386
+e = [], function() {
60387
+var a, b;
60388
+for (a = 0; 2 > a; ++a) for (b = 0; 8 > b; ++b) e.push(g[a][b].color);
60389
+}(), function() {
60390
+var a, b, c, d = [ 0, 95, 135, 175, 215, 255 ], f = function(a, b, c) {
60391
+return d[a] + ", " + d[b] + ", " + d[c];
60392
+};
60393
+for (a = 0; 6 > a; ++a) for (b = 0; 6 > b; ++b) for (c = 0; 6 > c; ++c) e.push(f.call(this, a, b, c));
60394
+}(), function() {
60395
+var a, b = 8, c = function(a) {
60396
+return a + ", " + a + ", " + a;
60397
+};
60398
+for (a = 0; 24 > a; ++a, b += 10) e.push(c.call(this, b));
60399
+}();
60400
+}, c.prototype.escape_for_html = function(a) {
60401
+return a.replace(/[&<>]/gm, function(a) {
60402
+return "&" == a ? "&amp;" :"<" == a ? "&lt;" :">" == a ? "&gt;" :void 0;
60403
+});
60404
+}, c.prototype.linkify = function(a) {
60405
+return a.replace(/(https?:\/\/[^\s]+)/gm, function(a) {
60406
+return '<a href="' + a + '">' + a + "</a>";
60407
+});
60408
+}, c.prototype.ansi_to_html = function(a, b) {
60409
+return this.process(a, b, !0);
60410
+}, c.prototype.ansi_to_text = function(a) {
60411
+var b = {};
60412
+return this.process(a, b, !1);
60413
+}, c.prototype.process = function(a, b, c) {
60414
+var d = this, e = a.split(/\033\[/), f = e.shift(), g = e.map(function(a) {
60415
+return d.process_chunk(a, b, c);
60416
+});
60417
+return g.unshift(f), g.join("");
60418
+}, c.prototype.process_chunk = function(a, b, c) {
60419
+b = "undefined" == typeof b ? {} :b;
60420
+var d = "undefined" != typeof b.use_classes && b.use_classes, f = d ? "class" :"color", h = a.match(/^([!\x3c-\x3f]*)([\d;]*)([\x20-\x2c]*[\x40-\x7e])([\s\S]*)/m);
60421
+if (!h) return a;
60422
+var i = h[4], j = h[2].split(";");
60423
+if ("" !== h[1] || "m" !== h[3]) return i;
60424
+if (!c) return i;
60425
+for (var k = this; j.length > 0; ) {
60426
+var l = j.shift(), m = parseInt(l);
60427
+isNaN(m) || 0 === m ? (k.fg = k.bg = null, k.bright = 0) :1 === m ? k.bright = 1 :39 == m ? k.fg = null :49 == m ? k.bg = null :m >= 30 && 38 > m ? k.fg = g[k.bright][m % 10][f] :m >= 90 && 98 > m ? k.fg = g[1][m % 10][f] :m >= 40 && 48 > m ? k.bg = g[0][m % 10][f] :m >= 100 && 108 > m ? k.bg = g[1][m % 10][f] :38 !== m && 48 !== m || !function() {
60428
+var a = 38 === m;
60429
+if (j.length >= 1) {
60430
+var b = j.shift();
60431
+if ("5" === b && j.length >= 1) {
60432
+var c = parseInt(j.shift());
60433
+if (c >= 0 && 255 >= c) if (d) {
60434
+var f = c >= 16 ? "ansi-palette-" + c :g[c > 7 ? 1 :0][c % 8]["class"];
60435
+a ? k.fg = f :k.bg = f;
60436
+} else e || k.setup_palette.call(k), a ? k.fg = e[c] :k.bg = e[c];
60437
+} else if ("2" === b && j.length >= 3) {
60438
+var h = parseInt(j.shift()), i = parseInt(j.shift()), l = parseInt(j.shift());
60439
+if (h >= 0 && 255 >= h && i >= 0 && 255 >= i && l >= 0 && 255 >= l) {
60440
+var n = h + ", " + i + ", " + l;
60441
+d ? a ? (k.fg = "ansi-truecolor", k.fg_truecolor = n) :(k.bg = "ansi-truecolor", k.bg_truecolor = n) :a ? k.fg = n :k.bg = n;
60442
+}
60443
+}
60444
+}
60445
+}();
60446
+}
60447
+if (null === k.fg && null === k.bg) return i;
60448
+var n = [], o = [], p = {}, q = function(a) {
60449
+var b, c = [];
60450
+for (b in a) a.hasOwnProperty(b) && c.push("data-" + b + '="' + this.escape_for_html(a[b]) + '"');
60451
+return c.length > 0 ? " " + c.join(" ") :"";
60452
+};
60453
+return k.fg && (d ? (o.push(k.fg + "-fg"), null !== k.fg_truecolor && (p["ansi-truecolor-fg"] = k.fg_truecolor, k.fg_truecolor = null)) :n.push("color:rgb(" + k.fg + ")")), k.bg && (d ? (o.push(k.bg + "-bg"), null !== k.bg_truecolor && (p["ansi-truecolor-bg"] = k.bg_truecolor, k.bg_truecolor = null)) :n.push("background-color:rgb(" + k.bg + ")")), d ? '<span class="' + o.join(" ") + '"' + q.call(k, p) + ">" + i + "</span>" :'<span style="' + n.join(";") + '"' + q.call(k, p) + ">" + i + "</span>";
60454
+}, d = {
60455
+escape_for_html:function(a) {
60456
+var b = new c();
60457
+return b.escape_for_html(a);
60458
+},
60459
+linkify:function(a) {
60460
+var b = new c();
60461
+return b.linkify(a);
60462
+},
60463
+ansi_to_html:function(a, b) {
60464
+var d = new c();
60465
+return d.ansi_to_html(a, b);
60466
+},
60467
+ansi_to_text:function(a) {
60468
+var b = new c();
60469
+return b.ansi_to_text(a);
60470
+},
60471
+ansi_to_html_obj:function() {
60472
+return new c();
60473
+}
60474
+}, f && (module.exports = d), "undefined" != typeof window && "undefined" == typeof ender && (window.ansi_up = d), "function" == typeof define && define.amd && define("ansi_up", [], function() {
60475
+return d;
60476
+});
60477
+}(Date);`)
60333 60478
 
60334 60479
 func scriptsVendorJsBytes() ([]byte, error) {
60335 60480
 	return _scriptsVendorJs, nil
... ...
@@ -86674,8 +86844,9 @@ select:invalid{box-shadow:none}
86674 86674
 .well h1:first-child,.well h2:first-child,.well h3:first-child,.well h4:first-child,.well h5:first-child{margin:5px 0 15px}
86675 86675
 .spinner.spinner-xs{height:12px;width:12px}
86676 86676
 .spinner.spinner-inverse{border-color:rgba(0,153,211,.25);border-top-color:rgba(0,153,211,.75)}
86677
-.attention-message{background-color:#48d1ff;border:1px solid #0082ae;position:absolute;top:20%;left:50%;transform:translate(-50%,-50%);padding:1em 1em 2em;min-width:85%}
86677
+.attention-message{background-color:#e1f7ff;border:1px solid #0082ae;position:absolute;left:50%;margin-top:100px;transform:translateX(-50%);padding:1em 1em 2em;min-width:85%}
86678 86678
 .attention-message h1,.attention-message p{text-align:center}
86679
+.attention-message p{max-width:70em;margin:auto}
86679 86680
 .learn-more-block{display:block;font-size:11px;font-weight:400}
86680 86681
 .short-id{background-color:#f1f1f1;color:#666}
86681 86682
 .input-number{width:60px}
... ...
@@ -212,12 +212,14 @@ func convert_v1_BuildStrategy_To_api_BuildStrategy(in *BuildStrategy, out *newer
212 212
 
213 213
 func addConversionFuncs(scheme *runtime.Scheme) {
214 214
 	err := scheme.AddDefaultingFuncs(
215
+		func(source *BuildSource) {
216
+			if (source != nil) && (source.Type == BuildSourceBinary) && (source.Binary == nil) {
217
+				source.Binary = &BinaryBuildSource{}
218
+			}
219
+		},
215 220
 		func(strategy *BuildStrategy) {
216
-			if (strategy != nil) && (strategy.Type == DockerBuildStrategyType) {
217
-				//  initialize DockerStrategy to a default state if it's not set.
218
-				if strategy.DockerStrategy == nil {
219
-					strategy.DockerStrategy = &DockerBuildStrategy{}
220
-				}
221
+			if (strategy != nil) && (strategy.Type == DockerBuildStrategyType) && (strategy.DockerStrategy == nil) {
222
+				strategy.DockerStrategy = &DockerBuildStrategy{}
221 223
 			}
222 224
 		},
223 225
 		func(obj *SourceBuildStrategy) {
... ...
@@ -11,6 +11,7 @@ import (
11 11
 	_ "github.com/openshift/origin/pkg/build/api/install"
12 12
 	older "github.com/openshift/origin/pkg/build/api/v1"
13 13
 	testutil "github.com/openshift/origin/test/util/api"
14
+	"reflect"
14 15
 )
15 16
 
16 17
 var Convert = knewer.Scheme.Convert
... ...
@@ -53,6 +54,35 @@ func TestBinaryBuildRequestOptions(t *testing.T) {
53 53
 	}
54 54
 }
55 55
 
56
+func TestBuildConfigDefaulting(t *testing.T) {
57
+	buildConfig := &older.BuildConfig{
58
+		Spec: older.BuildConfigSpec{
59
+			BuildSpec: older.BuildSpec{
60
+				Source: older.BuildSource{
61
+					Type: older.BuildSourceBinary,
62
+				},
63
+				Strategy: older.BuildStrategy{
64
+					Type: older.DockerBuildStrategyType,
65
+				},
66
+			},
67
+		},
68
+	}
69
+
70
+	var internalBuild newer.BuildConfig
71
+	Convert(buildConfig, &internalBuild)
72
+
73
+	binary := internalBuild.Spec.Source.Binary
74
+	if binary == (*newer.BinaryBuildSource)(nil) || *binary != (newer.BinaryBuildSource{}) {
75
+		t.Errorf("Expected non-nil but empty Source.Binary as default for Spec")
76
+	}
77
+
78
+	dockerStrategy := internalBuild.Spec.Strategy.DockerStrategy
79
+	// DeepEqual needed because DockerBuildStrategy contains slices
80
+	if dockerStrategy == (*newer.DockerBuildStrategy)(nil) || !reflect.DeepEqual(*dockerStrategy, newer.DockerBuildStrategy{}) {
81
+		t.Errorf("Expected non-nil but empty Strategy.DockerStrategy as default for Spec")
82
+	}
83
+}
84
+
56 85
 func TestBuildConfigConversion(t *testing.T) {
57 86
 	buildConfigs := []*older.BuildConfig{
58 87
 		{
... ...
@@ -73,6 +73,12 @@ func GetCGroupLimits() (*s2iapi.CGroupLimits, error) {
73 73
 	if err != nil {
74 74
 		return nil, fmt.Errorf("cannot determine cgroup limits: %v", err)
75 75
 	}
76
+	// math.MaxInt64 seems to give cgroups trouble, this value is
77
+	// still 92 terabytes, so it ought to be sufficiently large for
78
+	// our purposes.
79
+	if byteLimit > 92233720368547 {
80
+		byteLimit = 92233720368547
81
+	}
76 82
 
77 83
 	// different docker versions seem to use different cgroup directories,
78 84
 	// check for all of them.
... ...
@@ -112,15 +112,6 @@ func (bc *BuildController) HandleBuild(build *buildapi.Build) error {
112 112
 // the change cannot occur. When returning nil, be sure to set build.Status and optionally
113 113
 // build.Message.
114 114
 func (bc *BuildController) nextBuildPhase(build *buildapi.Build) error {
115
-	// If a cancelling event was triggered for the build, update build status.
116
-	if build.Status.Cancelled {
117
-		glog.V(4).Infof("Cancelling build %s/%s.", build.Namespace, build.Name)
118
-		build.Status.Phase = buildapi.BuildPhaseCancelled
119
-		build.Status.Reason = ""
120
-		build.Status.Message = ""
121
-		return nil
122
-	}
123
-
124 115
 	// Set the output Docker image reference.
125 116
 	ref, err := bc.resolveOutputDockerImageReference(build)
126 117
 	if err != nil {
... ...
@@ -9,6 +9,8 @@ import (
9 9
 	"github.com/spf13/cobra"
10 10
 
11 11
 	kapi "k8s.io/kubernetes/pkg/api"
12
+	"k8s.io/kubernetes/pkg/api/meta"
13
+	"k8s.io/kubernetes/pkg/api/unversioned"
12 14
 	kcmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
13 15
 
14 16
 	authorizationapi "github.com/openshift/origin/pkg/authorization/api"
... ...
@@ -24,7 +26,7 @@ type whoCanOptions struct {
24 24
 	client           *client.Client
25 25
 
26 26
 	verb     string
27
-	resource string
27
+	resource unversioned.GroupVersionResource
28 28
 }
29 29
 
30 30
 // NewCmdWhoCan implements the OpenShift cli who-can command
... ...
@@ -36,7 +38,7 @@ func NewCmdWhoCan(name, fullName string, f *clientcmd.Factory, out io.Writer) *c
36 36
 		Short: "List who can perform the specified action on a resource",
37 37
 		Long:  "List who can perform the specified action on a resource",
38 38
 		Run: func(cmd *cobra.Command, args []string) {
39
-			if err := options.complete(args); err != nil {
39
+			if err := options.complete(f, args); err != nil {
40 40
 				kcmdutil.CheckErr(kcmdutil.UsageError(cmd, err.Error()))
41 41
 			}
42 42
 
... ...
@@ -57,20 +59,41 @@ func NewCmdWhoCan(name, fullName string, f *clientcmd.Factory, out io.Writer) *c
57 57
 	return cmd
58 58
 }
59 59
 
60
-func (o *whoCanOptions) complete(args []string) error {
60
+func (o *whoCanOptions) complete(f *clientcmd.Factory, args []string) error {
61 61
 	if len(args) != 2 {
62 62
 		return errors.New("you must specify two arguments: verb and resource")
63 63
 	}
64 64
 
65
+	restMapper, _ := f.Object()
66
+
65 67
 	o.verb = args[0]
66
-	o.resource = args[1]
68
+	o.resource = resourceFor(restMapper, args[1])
69
+
67 70
 	return nil
68 71
 }
69 72
 
73
+func resourceFor(mapper meta.RESTMapper, resourceArg string) unversioned.GroupVersionResource {
74
+	fullySpecifiedGVR, groupResource := unversioned.ParseResourceArg(strings.ToLower(resourceArg))
75
+	gvr := unversioned.GroupVersionResource{}
76
+	if fullySpecifiedGVR != nil {
77
+		gvr, _ = mapper.ResourceFor(*fullySpecifiedGVR)
78
+	}
79
+	if gvr.IsEmpty() {
80
+		var err error
81
+		gvr, err = mapper.ResourceFor(groupResource.WithVersion(""))
82
+		if err != nil {
83
+			return unversioned.GroupVersionResource{Resource: resourceArg}
84
+		}
85
+	}
86
+
87
+	return gvr
88
+}
89
+
70 90
 func (o *whoCanOptions) run() error {
71 91
 	authorizationAttributes := authorizationapi.AuthorizationAttributes{
72
-		Resource: o.resource,
73 92
 		Verb:     o.verb,
93
+		Group:    o.resource.Group,
94
+		Resource: o.resource.Resource,
74 95
 	}
75 96
 
76 97
 	resourceAccessReviewResponse := &authorizationapi.ResourceAccessReviewResponse{}
... ...
@@ -90,8 +113,14 @@ func (o *whoCanOptions) run() error {
90 90
 	} else {
91 91
 		fmt.Printf("Namespace: %s\n", resourceAccessReviewResponse.Namespace)
92 92
 	}
93
+
94
+	resourceDisplay := o.resource.Resource
95
+	if len(o.resource.Group) > 0 {
96
+		resourceDisplay = resourceDisplay + "." + o.resource.Group
97
+	}
98
+
93 99
 	fmt.Printf("Verb:      %s\n", o.verb)
94
-	fmt.Printf("Resource:  %s\n\n", o.resource)
100
+	fmt.Printf("Resource:  %s\n\n", resourceDisplay)
95 101
 	if len(resourceAccessReviewResponse.Users) == 0 {
96 102
 		fmt.Printf("Users:  none\n\n")
97 103
 	} else {
... ...
@@ -93,7 +93,6 @@ the shell.`
93 93
 func NewCmdDebug(fullName string, f *clientcmd.Factory, in io.Reader, out, errout io.Writer) *cobra.Command {
94 94
 	options := &DebugOptions{
95 95
 		Timeout: 30 * time.Second,
96
-		Command: []string{"/bin/sh"},
97 96
 		Attach: kcmd.AttachOptions{
98 97
 			In:    in,
99 98
 			Out:   out,
... ...
@@ -173,6 +172,7 @@ func (o *DebugOptions) Complete(cmd *cobra.Command, f *clientcmd.Factory, args [
173 173
 		o.Attach.Stdin = false
174 174
 	default:
175 175
 		o.Attach.TTY = term.IsTerminal(in)
176
+		glog.V(4).Infof("Defaulting TTY to %t", o.Attach.TTY)
176 177
 	}
177 178
 	if o.NoStdin {
178 179
 		o.Attach.TTY = false
... ...
@@ -183,6 +183,10 @@ func (o *DebugOptions) Complete(cmd *cobra.Command, f *clientcmd.Factory, args [
183 183
 		o.Annotations = make(map[string]string)
184 184
 	}
185 185
 
186
+	if len(o.Command) == 0 {
187
+		o.Command = []string{"/bin/sh"}
188
+	}
189
+
186 190
 	cmdNamespace, explicit, err := f.DefaultNamespace()
187 191
 	if err != nil {
188 192
 		return err
... ...
@@ -521,7 +525,7 @@ func (o *DebugOptions) transformPodForDebug(annotations map[string]string) (*kap
521 521
 	pod.ResourceVersion = ""
522 522
 	pod.Spec.RestartPolicy = kapi.RestartPolicyNever
523 523
 	// TODO: shorten segments, make incrementing?
524
-	pod.Name = fmt.Sprintf("debug-%s", pod.Name)
524
+	pod.Name = fmt.Sprintf("%s-debug", pod.Name)
525 525
 	pod.Status = kapi.PodStatus{}
526 526
 	pod.UID = ""
527 527
 	pod.CreationTimestamp = unversioned.Time{}
... ...
@@ -10,7 +10,7 @@ import (
10 10
 	"strings"
11 11
 	"time"
12 12
 
13
-	. "github.com/MakeNowJust/heredoc/dot"
13
+	"github.com/MakeNowJust/heredoc"
14 14
 	docker "github.com/fsouza/go-dockerclient"
15 15
 	"github.com/golang/glog"
16 16
 	"github.com/spf13/cobra"
... ...
@@ -613,7 +613,7 @@ func transformError(err error, c *cobra.Command, fullName string, groups errorGr
613 613
 		if t.Input.Token != nil && t.Input.Token.ServiceAccount {
614 614
 			groups.Add(
615 615
 				"explicit-access-installer",
616
-				D(`
616
+				heredoc.Doc(`
617 617
 					WARNING: This will allow the pod to create and manage resources within your namespace -
618 618
 					ensure you trust the image with those permissions before you continue.
619 619
 
... ...
@@ -625,7 +625,7 @@ func transformError(err error, c *cobra.Command, fullName string, groups errorGr
625 625
 		} else {
626 626
 			groups.Add(
627 627
 				"explicit-access-you",
628
-				D(`
628
+				heredoc.Doc(`
629 629
 					WARNING: This will allow the pod to act as you across the entire cluster - ensure you
630 630
 					trust the image with those permissions before you continue.
631 631
 
... ...
@@ -639,7 +639,7 @@ func transformError(err error, c *cobra.Command, fullName string, groups errorGr
639 639
 	case newapp.ErrNoMatch:
640 640
 		groups.Add(
641 641
 			"no-matches",
642
-			Df(`
642
+			heredoc.Docf(`
643 643
 				The '%[1]s' command will match arguments to the following types:
644 644
 
645 645
 				  1. Images tagged into image streams in the current project or the 'openshift' project
... ...
@@ -664,8 +664,8 @@ func transformError(err error, c *cobra.Command, fullName string, groups errorGr
664 664
 		}
665 665
 		groups.Add(
666 666
 			"multiple-matches",
667
-			Df(`
668
-					The argument %[1]q could apply to the following Docker images or OpenShift image streams:
667
+			heredoc.Docf(`
668
+					The argument %[1]q could apply to the following Docker images, OpenShift image streams, or templates:
669 669
 
670 670
 					%[2]s`, t.Value, buf.String(),
671 671
 			),
... ...
@@ -680,8 +680,8 @@ func transformError(err error, c *cobra.Command, fullName string, groups errorGr
680 680
 
681 681
 		groups.Add(
682 682
 			"partial-match",
683
-			Df(`
684
-					The argument %[1]q only partially matched the following Docker image or OpenShift image stream:
683
+			heredoc.Docf(`
684
+					The argument %[1]q only partially matched the following Docker image, OpenShift image stream, or template:
685 685
 
686 686
 					%[2]s`, t.Value, buf.String(),
687 687
 			),
... ...
@@ -694,7 +694,7 @@ func transformError(err error, c *cobra.Command, fullName string, groups errorGr
694 694
 		fmt.Fprintf(buf, "  Use --allow-missing-imagestream-tags to use this image stream\n\n")
695 695
 		groups.Add(
696 696
 			"no-tags",
697
-			Df(`
697
+			heredoc.Docf(`
698 698
 					The image stream %[1]q exists, but it has no tags.
699 699
 
700 700
 					%[2]s`, t.Match.Name, buf.String(),
... ...
@@ -7,7 +7,7 @@ import (
7 7
 	"io/ioutil"
8 8
 	"os"
9 9
 
10
-	. "github.com/MakeNowJust/heredoc/dot"
10
+	"github.com/MakeNowJust/heredoc"
11 11
 	"github.com/spf13/cobra"
12 12
 
13 13
 	kcmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util"
... ...
@@ -242,7 +242,7 @@ func transformBuildError(err error, c *cobra.Command, fullName string, groups er
242 242
 	case newapp.ErrNoMatch:
243 243
 		groups.Add(
244 244
 			"no-matches",
245
-			Df(`
245
+			heredoc.Docf(`
246 246
 				The '%[1]s' command will match arguments to the following types:
247 247
 
248 248
 				  1. Images tagged into image streams in the current project or the 'openshift' project
... ...
@@ -214,9 +214,9 @@ func RunProcess(f *clientcmd.Factory, out io.Writer, cmd *cobra.Command, args []
214 214
 		// when user specify the --value
215 215
 		if cmd.Flag("value").Changed {
216 216
 			values := kcmdutil.GetFlagStringSlice(cmd, "value")
217
-			injectUserVars(values, out, obj)
217
+			injectUserVars(values, cmd, obj)
218 218
 		}
219
-		injectUserVars(valueArgs, out, obj)
219
+		injectUserVars(valueArgs, cmd, obj)
220 220
 
221 221
 		resultObj, err := client.TemplateConfigs(namespace).Create(obj)
222 222
 		if err != nil {
... ...
@@ -271,11 +271,11 @@ func RunProcess(f *clientcmd.Factory, out io.Writer, cmd *cobra.Command, args []
271 271
 }
272 272
 
273 273
 // injectUserVars injects user specified variables into the Template
274
-func injectUserVars(values []string, out io.Writer, t *templateapi.Template) {
274
+func injectUserVars(values []string, cmd *cobra.Command, t *templateapi.Template) {
275 275
 	for _, keypair := range values {
276 276
 		p := strings.SplitN(keypair, "=", 2)
277 277
 		if len(p) != 2 {
278
-			fmt.Fprintf(out, "invalid parameter assignment in %q: %q\n", t.Name, keypair)
278
+			fmt.Fprintf(cmd.Out(), "invalid parameter assignment in %q: %q\n", t.Name, keypair)
279 279
 			continue
280 280
 		}
281 281
 		if v := template.GetParameterByName(t, p[0]); v != nil {
... ...
@@ -283,7 +283,7 @@ func injectUserVars(values []string, out io.Writer, t *templateapi.Template) {
283 283
 			v.Generate = ""
284 284
 			template.AddParameter(t, *v)
285 285
 		} else {
286
-			fmt.Fprintf(out, "unknown parameter name %q\n", p[0])
286
+			fmt.Fprintf(cmd.Out(), "unknown parameter name %q\n", p[0])
287 287
 		}
288 288
 	}
289 289
 }
... ...
@@ -554,6 +554,7 @@ func RunStartBuildWebHook(f *clientcmd.Factory, out io.Writer, webhook string, p
554 554
 	if err != nil {
555 555
 		return err
556 556
 	}
557
+	defer resp.Body.Close()
557 558
 	switch {
558 559
 	case resp.StatusCode == 301 || resp.StatusCode == 302:
559 560
 		// TODO: follow redirect and display output
... ...
@@ -6,7 +6,7 @@ import (
6 6
 	"io"
7 7
 	"strings"
8 8
 
9
-	. "github.com/MakeNowJust/heredoc/dot"
9
+	"github.com/MakeNowJust/heredoc"
10 10
 	"github.com/spf13/cobra"
11 11
 
12 12
 	ocutil "github.com/openshift/origin/pkg/cmd/util"
... ...
@@ -23,7 +23,7 @@ var concepts = []concept{
23 23
 	{
24 24
 		"Containers",
25 25
 		"",
26
-		D(`
26
+		heredoc.Doc(`
27 27
       A definition of how to run one or more processes inside of a portable Linux
28 28
       environment. Containers are started from an Image and are usually isolated
29 29
       from other containers on the same machine.
... ...
@@ -32,7 +32,7 @@ var concepts = []concept{
32 32
 	{
33 33
 		"Image",
34 34
 		"",
35
-		D(`
35
+		heredoc.Doc(`
36 36
       A layered Linux filesystem that contains application code, dependencies,
37 37
       and any supporting operating system libraries. An image is identified by
38 38
       a name that can be local to the current cluster or point to a remote Docker
... ...
@@ -41,7 +41,7 @@ var concepts = []concept{
41 41
 	}, {
42 42
 		"Pods",
43 43
 		"pod",
44
-		D(`
44
+		heredoc.Doc(`
45 45
       A set of one or more containers that are deployed onto a Node together and
46 46
       share a unique IP and Volumes (persistent storage). Pods also define the
47 47
       security and runtime policy for each container.
... ...
@@ -49,7 +49,7 @@ var concepts = []concept{
49 49
 	}, {
50 50
 		"Labels",
51 51
 		"",
52
-		D(`
52
+		heredoc.Doc(`
53 53
       Labels are key value pairs that can be assigned to any resource in the
54 54
       system for grouping and selection. Many resources use labels to identify
55 55
       sets of other resources.
... ...
@@ -57,7 +57,7 @@ var concepts = []concept{
57 57
 	}, {
58 58
 		"Volumes",
59 59
 		"",
60
-		D(`
60
+		heredoc.Doc(`
61 61
       Containers are not persistent by default - on restart their contents are
62 62
       cleared. Volumes are mounted filesystems available to Pods and their
63 63
       containers which may be backed by a number of host-local or network
... ...
@@ -69,14 +69,14 @@ var concepts = []concept{
69 69
 	}, {
70 70
 		"Nodes",
71 71
 		"node",
72
-		D(`
72
+		heredoc.Doc(`
73 73
       Machines set up in the cluster to run containers. Usually managed
74 74
       by administrators and not by end users.
75 75
     `),
76 76
 	}, {
77 77
 		"Services",
78 78
 		"svc",
79
-		D(`
79
+		heredoc.Doc(`
80 80
       A name representing a set of pods (or external servers) that are
81 81
       accessed by other pods. The service gets an IP and a DNS name, and can be
82 82
       exposed externally to the cluster via a port or a Route. It's also easy
... ...
@@ -86,7 +86,7 @@ var concepts = []concept{
86 86
 	}, {
87 87
 		"Routes",
88 88
 		"route",
89
-		D(`
89
+		heredoc.Doc(`
90 90
       A route is an external DNS entry (either a top level domain or a
91 91
       dynamically allocated name) that is created to point to a service so that
92 92
       it can be accessed outside the cluster. The administrator may configure
... ...
@@ -97,7 +97,7 @@ var concepts = []concept{
97 97
 	{
98 98
 		"Replication Controllers",
99 99
 		"rc",
100
-		D(`
100
+		heredoc.Doc(`
101 101
       A replication controller maintains a specific number of pods based on a
102 102
       template that match a set of labels. If pods are deleted (because the
103 103
       node they run on is taken out of service) the controller creates a new
... ...
@@ -109,7 +109,7 @@ var concepts = []concept{
109 109
 	{
110 110
 		"Deployment Configuration",
111 111
 		"dc",
112
-		D(`
112
+		heredoc.Doc(`
113 113
       Defines the template for a pod and manages deploying new images or
114 114
       configuration changes whenever those change. A single deployment
115 115
       configuration is usually analogous to a single micro-service. Can support
... ...
@@ -121,7 +121,7 @@ var concepts = []concept{
121 121
 	{
122 122
 		"Build Configuration",
123 123
 		"bc",
124
-		D(`
124
+		heredoc.Doc(`
125 125
       Contains a description of how to build source code and a base image into a
126 126
       new image - the primary method for delivering changes to your application.
127 127
       Builds can be source based and use builder images for common languages like
... ...
@@ -133,7 +133,7 @@ var concepts = []concept{
133 133
 	{
134 134
 		"Builds",
135 135
 		"build",
136
-		D(`
136
+		heredoc.Doc(`
137 137
       Builds create a new image from source code, other images, Dockerfiles, or
138 138
       binary input. A build is run inside of a container and has the same
139 139
       restrictions normal pods have. A build usually results in an image pushed
... ...
@@ -144,7 +144,7 @@ var concepts = []concept{
144 144
 	{
145 145
 		"Image Streams and Image Stream Tags",
146 146
 		"is,istag",
147
-		D(`
147
+		heredoc.Doc(`
148 148
       An image stream groups sets of related images under tags - analogous to a
149 149
       branch in a source code repository. Each image stream may have one or
150 150
       more tags (the default tag is called "latest") and those tags may point
... ...
@@ -157,7 +157,7 @@ var concepts = []concept{
157 157
 	{
158 158
 		"Secrets",
159 159
 		"secret",
160
-		D(`
160
+		heredoc.Doc(`
161 161
       The secret resource can hold text or binary secrets for delivery into
162 162
       your pods. By default, every container is given a single secret which
163 163
       contains a token for accessing the API (with limited privileges) at
... ...
@@ -170,7 +170,7 @@ var concepts = []concept{
170 170
 	{
171 171
 		"Projects",
172 172
 		"project",
173
-		D(`
173
+		heredoc.Doc(`
174 174
       All of the above resources (except Nodes) exist inside of a project.
175 175
       Projects have a list of members and their roles, like viewer, editor,
176 176
       or admin, as well as a set of security controls on the running pods, and
... ...
@@ -194,7 +194,7 @@ func writeConcept(w io.Writer, c concept) {
194 194
 }
195 195
 
196 196
 var (
197
-	typesLong = D(`
197
+	typesLong = heredoc.Doc(`
198 198
     Concepts and Types
199 199
 
200 200
     Kubernetes and OpenShift help developers and operators build, test, and deploy
... ...
@@ -14,7 +14,6 @@ import (
14 14
 	kerrors "k8s.io/kubernetes/pkg/api/errors"
15 15
 	kclient "k8s.io/kubernetes/pkg/client/unversioned"
16 16
 	kctl "k8s.io/kubernetes/pkg/kubectl"
17
-	qosutil "k8s.io/kubernetes/pkg/kubelet/qos/util"
18 17
 	"k8s.io/kubernetes/pkg/labels"
19 18
 
20 19
 	kubegraph "github.com/openshift/origin/pkg/api/kubegraph/nodes"
... ...
@@ -134,11 +133,9 @@ func (d *DeploymentConfigDescriber) Describe(namespace, name string) (string, er
134 134
 			formatString(out, "Latest Version", strconv.Itoa(deploymentConfig.Status.LatestVersion))
135 135
 		}
136 136
 
137
-		printTriggers(deploymentConfig.Spec.Triggers, out)
138
-
139
-		formatString(out, "Strategy", deploymentConfig.Spec.Strategy.Type)
140
-		printStrategy(deploymentConfig.Spec.Strategy, out)
141 137
 		printDeploymentConfigSpec(deploymentConfig.Spec, out)
138
+		fmt.Fprintln(out)
139
+
142 140
 		if deploymentConfig.Status.Details != nil && len(deploymentConfig.Status.Details.Message) > 0 {
143 141
 			fmt.Fprintf(out, "Warning:\t%s\n", deploymentConfig.Status.Details.Message)
144 142
 		}
... ...
@@ -168,6 +165,7 @@ func (d *DeploymentConfigDescriber) Describe(namespace, name string) (string, er
168 168
 		}
169 169
 
170 170
 		if events != nil {
171
+			fmt.Fprintln(out)
171 172
 			kctl.DescribeEvents(events, out)
172 173
 		}
173 174
 		return nil
... ...
@@ -248,80 +246,29 @@ func printTriggers(triggers []deployapi.DeploymentTriggerPolicy, w *tabwriter.Wr
248 248
 	formatString(w, "Triggers", desc)
249 249
 }
250 250
 
251
-func printDeploymentConfigSpec(spec deployapi.DeploymentConfigSpec, w io.Writer) error {
252
-	fmt.Fprint(w, "Template:\n")
251
+func printDeploymentConfigSpec(spec deployapi.DeploymentConfigSpec, w *tabwriter.Writer) error {
252
+	// Selector
253
+	formatString(w, "Selector", formatLabels(spec.Selector))
253 254
 
255
+	// Replicas
256
+	test := ""
254 257
 	if spec.Test {
255
-		fmt.Fprintf(w, "  Selector:\t%s\n  Replicas:\t%d (test, will be scaled down between deployments)\n",
256
-			formatLabels(spec.Selector),
257
-			spec.Replicas)
258
-	} else {
259
-		fmt.Fprintf(w, "  Selector:\t%s\n  Replicas:\t%d\n",
260
-			formatLabels(spec.Selector),
261
-			spec.Replicas)
258
+		test = " (test, will be scaled down between deployments)"
262 259
 	}
260
+	formatString(w, "Replicas", fmt.Sprintf("%d%s", spec.Replicas, test))
263 261
 
264
-	fmt.Fprintf(w, "  Containers:\n")
265
-	describeContainers(spec.Template.Spec, w)
262
+	// Triggers
263
+	printTriggers(spec.Triggers, w)
266 264
 
267
-	return nil
268
-}
269
-
270
-// TODO: Reuse this from upstream once kubernetes/issues/21551 is fixed.
271
-func describeContainers(spec kapi.PodSpec, w io.Writer) {
272
-	for _, container := range spec.Containers {
273
-		fmt.Fprintf(w, "  %v:\n", container.Name)
274
-		fmt.Fprintf(w, "    Image:\t%s\n", container.Image)
265
+	// Strategy
266
+	formatString(w, "Strategy", spec.Strategy.Type)
267
+	printStrategy(spec.Strategy, w)
275 268
 
276
-		if len(container.Command) > 0 {
277
-			fmt.Fprintf(w, "    Command:\n")
278
-			for _, c := range container.Command {
279
-				fmt.Fprintf(w, "      %s\n", c)
280
-			}
281
-		}
282
-		if len(container.Args) > 0 {
283
-			fmt.Fprintf(w, "    Args:\n")
284
-			for _, arg := range container.Args {
285
-				fmt.Fprintf(w, "      %s\n", arg)
286
-			}
287
-		}
288
-
289
-		resourceToQoS := qosutil.GetQoS(&container)
290
-		if len(resourceToQoS) > 0 {
291
-			fmt.Fprintf(w, "    QoS Tier:\n")
292
-		}
293
-		for resource, qos := range resourceToQoS {
294
-			fmt.Fprintf(w, "      %s:\t%s\n", resource, qos)
295
-		}
269
+	// Pod template
270
+	fmt.Fprintf(w, "Template:\n")
271
+	kctl.DescribePodTemplate(spec.Template, w)
296 272
 
297
-		if len(container.Resources.Limits) > 0 {
298
-			fmt.Fprintf(w, "    Limits:\n")
299
-		}
300
-		for name, quantity := range container.Resources.Limits {
301
-			fmt.Fprintf(w, "      %s:\t%s\n", name, quantity.String())
302
-		}
303
-
304
-		if len(container.Resources.Requests) > 0 {
305
-			fmt.Fprintf(w, "    Requests:\n")
306
-		}
307
-		for name, quantity := range container.Resources.Requests {
308
-			fmt.Fprintf(w, "      %s:\t%s\n", name, quantity.String())
309
-		}
310
-
311
-		if container.LivenessProbe != nil {
312
-			probe := kctl.DescribeProbe(container.LivenessProbe)
313
-			fmt.Fprintf(w, "    Liveness:\t%s\n", probe)
314
-		}
315
-		if container.ReadinessProbe != nil {
316
-			probe := kctl.DescribeProbe(container.ReadinessProbe)
317
-			fmt.Fprintf(w, "    Readiness:\t%s\n", probe)
318
-		}
319
-
320
-		fmt.Fprintf(w, "    Environment Variables:\n")
321
-		for _, e := range container.Env {
322
-			fmt.Fprintf(w, "      %s:\t%s\n", e.Name, e.Value)
323
-		}
324
-	}
273
+	return nil
325 274
 }
326 275
 
327 276
 func printDeploymentRc(deployment *kapi.ReplicationController, client deploymentDescriberClient, w io.Writer, header string, verbose bool) error {
... ...
@@ -152,6 +152,9 @@ func (d *ProjectStatusDescriber) Describe(namespace, name string) (string, error
152 152
 	standaloneImages, coveredByImages := graphview.AllImagePipelinesFromBuildConfig(g, coveredNodes)
153 153
 	coveredNodes.Insert(coveredByImages.List()...)
154 154
 
155
+	standalonePods, coveredByPods := graphview.AllPods(g, coveredNodes)
156
+	coveredNodes.Insert(coveredByPods.List()...)
157
+
155 158
 	return tabbedString(func(out *tabwriter.Writer) error {
156 159
 		indent := "  "
157 160
 		if allNamespaces {
... ...
@@ -218,6 +221,15 @@ func (d *ProjectStatusDescriber) Describe(namespace, name string) (string, error
218 218
 			printLines(out, indent, 0, describeRCInServiceGroup(f, standaloneRC.RC)...)
219 219
 		}
220 220
 
221
+		monopods, err := filterBoringPods(standalonePods)
222
+		if err != nil {
223
+			return err
224
+		}
225
+		for _, monopod := range monopods {
226
+			fmt.Fprintln(out)
227
+			printLines(out, indent, 0, describeMonopod(f, monopod.Pod)...)
228
+		}
229
+
221 230
 		allMarkers := osgraph.Markers{}
222 231
 		allMarkers = append(allMarkers, createForbiddenMarkers(forbiddenResources)...)
223 232
 		for _, scanner := range getMarkerScanners(d.LogsCommandName, d.SecurityPolicyCommandFormat, d.SetProbeCommandName) {
... ...
@@ -502,6 +514,16 @@ func describePodInServiceGroup(f formatter, podNode *kubegraph.PodNode) []string
502 502
 	return lines
503 503
 }
504 504
 
505
+func describeMonopod(f formatter, podNode *kubegraph.PodNode) []string {
506
+	images := []string{}
507
+	for _, container := range podNode.Pod.Spec.Containers {
508
+		images = append(images, container.Image)
509
+	}
510
+
511
+	lines := []string{fmt.Sprintf("%s runs %s", f.ResourceName(podNode), strings.Join(images, ", "))}
512
+	return lines
513
+}
514
+
505 515
 // exposedRoutes orders strings by their leading prefix (https:// -> http:// other prefixes), then by
506 516
 // the shortest distance up to the first space (indicating a break), then alphabetically:
507 517
 //
... ...
@@ -1044,6 +1066,30 @@ func describeServicePorts(spec kapi.ServiceSpec) string {
1044 1044
 	}
1045 1045
 }
1046 1046
 
1047
+func filterBoringPods(pods []graphview.Pod) ([]graphview.Pod, error) {
1048
+	monopods := []graphview.Pod{}
1049
+
1050
+	for _, pod := range pods {
1051
+		actualPod, ok := pod.Pod.Object().(*kapi.Pod)
1052
+		if !ok {
1053
+			continue
1054
+		}
1055
+		meta, err := kapi.ObjectMetaFor(actualPod)
1056
+		if err != nil {
1057
+			return nil, err
1058
+		}
1059
+		_, isDeployerPod := meta.Labels[deployapi.DeployerPodForDeploymentLabel]
1060
+		_, isBuilderPod := meta.Annotations[buildapi.BuildAnnotation]
1061
+		isFinished := actualPod.Status.Phase == kapi.PodSucceeded || actualPod.Status.Phase == kapi.PodFailed
1062
+		if isDeployerPod || isBuilderPod || isFinished {
1063
+			continue
1064
+		}
1065
+		monopods = append(monopods, pod)
1066
+	}
1067
+
1068
+	return monopods, nil
1069
+}
1070
+
1047 1071
 // GraphLoader is a stateful interface that provides methods for building the nodes of a graph
1048 1072
 type GraphLoader interface {
1049 1073
 	// Load is responsible for gathering and saving the objects this GraphLoader should AddToGraph
... ...
@@ -293,6 +293,20 @@ func TestProjectStatus(t *testing.T) {
293 293
 				`View details with 'oc describe <resource>/<name>' or list everything with 'oc get all'.`,
294 294
 			},
295 295
 		},
296
+		"monopod": {
297
+			Path: "../../../../test/fixtures/app-scenarios/k8s-lonely-pod.json",
298
+			Extra: []runtime.Object{
299
+				&projectapi.Project{
300
+					ObjectMeta: kapi.ObjectMeta{Name: "example", Namespace: ""},
301
+				},
302
+			},
303
+			ErrFn: func(err error) bool { return err == nil },
304
+			Contains: []string{
305
+				"In project example on server https://example.com:8443\n",
306
+				"pod/lonely-pod runs openshift/hello-openshift",
307
+				"You have no services, deployment configs, or build configs.",
308
+			},
309
+		},
296 310
 	}
297 311
 	oldTimeFn := timeNowFn
298 312
 	defer func() { timeNowFn = oldTimeFn }()
... ...
@@ -42,6 +42,9 @@ const (
42 42
 
43 43
 	InfraPersistentVolumeProvisionerControllerServiceAccountName = "pv-provisioner-controller"
44 44
 	PersistentVolumeProvisionerControllerRoleName                = "system:pv-provisioner-controller"
45
+
46
+	InfraGCControllerServiceAccountName = "gc-controller"
47
+	GCControllerRoleName                = "system:gc-controller"
45 48
 )
46 49
 
47 50
 type InfraServiceAccounts struct {
... ...
@@ -536,4 +539,30 @@ func init() {
536 536
 	if err != nil {
537 537
 		panic(err)
538 538
 	}
539
+
540
+	err = InfraSAs.addServiceAccount(
541
+		InfraGCControllerServiceAccountName,
542
+		authorizationapi.ClusterRole{
543
+			ObjectMeta: kapi.ObjectMeta{
544
+				Name: GCControllerRoleName,
545
+			},
546
+			Rules: []authorizationapi.PolicyRule{
547
+				// GCController.podStore.ListWatch
548
+				{
549
+					APIGroups: []string{kapi.GroupName},
550
+					Verbs:     sets.NewString("list", "watch"),
551
+					Resources: sets.NewString("pods"),
552
+				},
553
+				// GCController.deletePod
554
+				{
555
+					APIGroups: []string{kapi.GroupName},
556
+					Verbs:     sets.NewString("delete"),
557
+					Resources: sets.NewString("pods"),
558
+				},
559
+			},
560
+		},
561
+	)
562
+	if err != nil {
563
+		panic(err)
564
+	}
539 565
 }
... ...
@@ -151,7 +151,12 @@ func GetBootstrapClusterRoles() []authorizationapi.ClusterRole {
151 151
 				{
152 152
 					APIGroups: []string{extensions.GroupName},
153 153
 					Verbs:     sets.NewString("get", "list", "watch", "create", "update", "patch", "delete", "deletecollection"),
154
-					Resources: sets.NewString("daemonsets", "jobs", "horizontalpodautoscalers", "replicationcontrollers/scale"),
154
+					Resources: sets.NewString("jobs", "horizontalpodautoscalers", "replicationcontrollers/scale"),
155
+				},
156
+				{
157
+					APIGroups: []string{extensions.GroupName},
158
+					Verbs:     sets.NewString("get", "list", "watch"),
159
+					Resources: sets.NewString("daemonsets"),
155 160
 				},
156 161
 				{
157 162
 					Verbs:     sets.NewString("get", "list", "watch"),
... ...
@@ -210,7 +215,12 @@ func GetBootstrapClusterRoles() []authorizationapi.ClusterRole {
210 210
 				{
211 211
 					APIGroups: []string{extensions.GroupName},
212 212
 					Verbs:     sets.NewString("get", "list", "watch", "create", "update", "patch", "delete", "deletecollection"),
213
-					Resources: sets.NewString("daemonsets", "jobs", "horizontalpodautoscalers", "replicationcontrollers/scale"),
213
+					Resources: sets.NewString("jobs", "horizontalpodautoscalers", "replicationcontrollers/scale"),
214
+				},
215
+				{
216
+					APIGroups: []string{extensions.GroupName},
217
+					Verbs:     sets.NewString("get", "list", "watch"),
218
+					Resources: sets.NewString("daemonsets"),
214 219
 				},
215 220
 				{
216 221
 					Verbs:     sets.NewString("get", "list", "watch"),
... ...
@@ -24,6 +24,7 @@ import (
24 24
 	"k8s.io/kubernetes/pkg/controller"
25 25
 	"k8s.io/kubernetes/pkg/controller/daemon"
26 26
 	endpointcontroller "k8s.io/kubernetes/pkg/controller/endpoint"
27
+	gccontroller "k8s.io/kubernetes/pkg/controller/gc"
27 28
 	jobcontroller "k8s.io/kubernetes/pkg/controller/job"
28 29
 	namespacecontroller "k8s.io/kubernetes/pkg/controller/namespace"
29 30
 	nodecontroller "k8s.io/kubernetes/pkg/controller/node"
... ...
@@ -289,6 +290,13 @@ func (c *MasterConfig) RunResourceQuotaManager() {
289 289
 	go kresourcequota.NewResourceQuotaController(resourceQuotaControllerOptions).Run(c.ControllerManager.ConcurrentResourceQuotaSyncs, utilwait.NeverStop)
290 290
 }
291 291
 
292
+func (c *MasterConfig) RunGCController(client *client.Client) {
293
+	if c.ControllerManager.TerminatedPodGCThreshold > 0 {
294
+		gcController := gccontroller.New(internalclientset.FromUnversionedClient(client), kctrlmgr.ResyncPeriod(c.ControllerManager), c.ControllerManager.TerminatedPodGCThreshold)
295
+		go gcController.Run(utilwait.NeverStop)
296
+	}
297
+}
298
+
292 299
 // RunNodeController starts the node controller
293 300
 func (c *MasterConfig) RunNodeController() {
294 301
 	s := c.ControllerManager
... ...
@@ -18,7 +18,6 @@ import (
18 18
 	"k8s.io/kubernetes/pkg/admission"
19 19
 	kapi "k8s.io/kubernetes/pkg/api"
20 20
 	"k8s.io/kubernetes/pkg/api/unversioned"
21
-	"k8s.io/kubernetes/pkg/apimachinery/registered"
22 21
 	"k8s.io/kubernetes/pkg/apis/extensions"
23 22
 	"k8s.io/kubernetes/pkg/apiserver"
24 23
 	"k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset"
... ...
@@ -205,20 +204,20 @@ func BuildKubernetesMasterConfig(options configapi.MasterConfig, requestContextM
205 205
 		storageVersions[configapi.APIGroupKube] = options.EtcdStorageConfig.KubernetesStorageVersion
206 206
 	}
207 207
 
208
-	// TODO: also need to enable this if batch or autoscaling is enabled and doesn't have a storage version set
209
-	enabledExtensionsVersions := configapi.GetEnabledAPIVersionsForGroup(*options.KubernetesMasterConfig, configapi.APIGroupExtensions)
210
-	if len(enabledExtensionsVersions) > 0 {
211
-		groupMeta, err := registered.Group(configapi.APIGroupExtensions)
212
-		if err != nil {
213
-			return nil, fmt.Errorf("Error setting up Kubernetes extensions server storage: %v", err)
214
-		}
215
-		// TODO expose storage version options for api groups
216
-		databaseStorage, err := NewEtcdStorage(etcdClient, groupMeta.GroupVersion, options.EtcdStorageConfig.KubernetesStoragePrefix)
208
+	// enable this if extensions API is enabled (or batch or autoscaling, since they persist to extensions/v1beta1 for now)
209
+	// TODO: replace this with a loop over configured storage versions
210
+	extensionsEnabled := len(configapi.GetEnabledAPIVersionsForGroup(*options.KubernetesMasterConfig, configapi.APIGroupExtensions)) > 0
211
+	batchEnabled := len(configapi.GetEnabledAPIVersionsForGroup(*options.KubernetesMasterConfig, configapi.APIGroupBatch)) > 0
212
+	autoscalingEnabled := len(configapi.GetEnabledAPIVersionsForGroup(*options.KubernetesMasterConfig, configapi.APIGroupAutoscaling)) > 0
213
+	if extensionsEnabled || autoscalingEnabled || batchEnabled {
214
+		// TODO: replace this with a configured storage version for extensions once configuration exposes this
215
+		extensionsStorageVersion := unversioned.GroupVersion{Group: extensions.GroupName, Version: "v1beta1"}
216
+		databaseStorage, err := NewEtcdStorage(etcdClient, extensionsStorageVersion, options.EtcdStorageConfig.KubernetesStoragePrefix)
217 217
 		if err != nil {
218 218
 			return nil, fmt.Errorf("Error setting up Kubernetes extensions server storage: %v", err)
219 219
 		}
220 220
 		storageDestinations.AddAPIGroup(configapi.APIGroupExtensions, databaseStorage)
221
-		storageVersions[configapi.APIGroupExtensions] = unversioned.GroupVersion{Group: extensions.GroupName, Version: enabledExtensionsVersions[0]}.String()
221
+		storageVersions[configapi.APIGroupExtensions] = extensionsStorageVersion.String()
222 222
 	}
223 223
 
224 224
 	// Preserve previous behavior of using the first non-loopback address
... ...
@@ -1,6 +1,7 @@
1 1
 package kubernetes
2 2
 
3 3
 import (
4
+	"errors"
4 5
 	"fmt"
5 6
 	"net"
6 7
 	"net/url"
... ...
@@ -28,9 +29,12 @@ import (
28 28
 	utiliptables "k8s.io/kubernetes/pkg/util/iptables"
29 29
 	utilnet "k8s.io/kubernetes/pkg/util/net"
30 30
 	"k8s.io/kubernetes/pkg/util/sysctl"
31
+	"k8s.io/kubernetes/pkg/volume"
31 32
 
33
+	configapi "github.com/openshift/origin/pkg/cmd/server/api"
32 34
 	cmdutil "github.com/openshift/origin/pkg/cmd/util"
33 35
 	dockerutil "github.com/openshift/origin/pkg/cmd/util/docker"
36
+	"github.com/openshift/origin/pkg/volume/emptydir"
34 37
 )
35 38
 
36 39
 type commandExecutor interface {
... ...
@@ -195,6 +199,52 @@ func (c *NodeConfig) initializeVolumeDir(ce commandExecutor, path string) (strin
195 195
 	return rootDirectory, nil
196 196
 }
197 197
 
198
+// EnsureLocalQuota checks if the node config specifies a local storage
199
+// perFSGroup quota, and if so will test that the volumeDirectory is on a
200
+// filesystem suitable for quota enforcement. If checks pass the k8s emptyDir
201
+// volume plugin will be replaced with a wrapper version which adds quota
202
+// functionality.
203
+func (c *NodeConfig) EnsureLocalQuota(nodeConfig configapi.NodeConfig) {
204
+	if nodeConfig.VolumeConfig.LocalQuota.PerFSGroup == nil {
205
+		return
206
+	}
207
+	glog.V(4).Info("Replacing empty-dir volume plugin with quota wrapper")
208
+	wrappedEmptyDirPlugin := false
209
+
210
+	quotaApplicator, err := emptydir.NewQuotaApplicator(nodeConfig.VolumeDirectory)
211
+	if err != nil {
212
+		glog.Fatalf("Could not set up local quota, %s", err)
213
+	}
214
+
215
+	// Create a volume spec with emptyDir we can use to search for the
216
+	// emptyDir plugin with CanSupport:
217
+	emptyDirSpec := &volume.Spec{
218
+		Volume: &kapi.Volume{
219
+			VolumeSource: kapi.VolumeSource{
220
+				EmptyDir: &kapi.EmptyDirVolumeSource{},
221
+			},
222
+		},
223
+	}
224
+
225
+	for idx, plugin := range c.KubeletConfig.VolumePlugins {
226
+		// Can't really do type checking or use a constant here as they are not exported:
227
+		if plugin.CanSupport(emptyDirSpec) {
228
+			wrapper := emptydir.EmptyDirQuotaPlugin{
229
+				Wrapped:         plugin,
230
+				Quota:           *nodeConfig.VolumeConfig.LocalQuota.PerFSGroup,
231
+				QuotaApplicator: quotaApplicator,
232
+			}
233
+			c.KubeletConfig.VolumePlugins[idx] = &wrapper
234
+			wrappedEmptyDirPlugin = true
235
+		}
236
+	}
237
+	// Because we can't look for the k8s emptyDir plugin by any means that would
238
+	// survive a refactor, error out if we couldn't find it:
239
+	if !wrappedEmptyDirPlugin {
240
+		glog.Fatal(errors.New("No plugin handling EmptyDir was found, unable to apply local quotas"))
241
+	}
242
+}
243
+
198 244
 // RunKubelet starts the Kubelet.
199 245
 func (c *NodeConfig) RunKubelet() {
200 246
 	if c.KubeletConfig.ClusterDNS == nil {
... ...
@@ -2,7 +2,6 @@ package kubernetes
2 2
 
3 3
 import (
4 4
 	"crypto/tls"
5
-	"errors"
6 5
 	"fmt"
7 6
 	"net"
8 7
 	"strconv"
... ...
@@ -24,7 +23,6 @@ import (
24 24
 	"k8s.io/kubernetes/pkg/util"
25 25
 	kerrors "k8s.io/kubernetes/pkg/util/errors"
26 26
 	"k8s.io/kubernetes/pkg/util/oom"
27
-	"k8s.io/kubernetes/pkg/volume"
28 27
 
29 28
 	osdnapi "github.com/openshift/openshift-sdn/plugins/osdn/api"
30 29
 	"github.com/openshift/openshift-sdn/plugins/osdn/factory"
... ...
@@ -35,7 +33,6 @@ import (
35 35
 	"github.com/openshift/origin/pkg/cmd/util/clientcmd"
36 36
 	cmdflags "github.com/openshift/origin/pkg/cmd/util/flags"
37 37
 	"github.com/openshift/origin/pkg/cmd/util/variable"
38
-	"github.com/openshift/origin/pkg/volume/empty_dir"
39 38
 	"k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset"
40 39
 )
41 40
 
... ...
@@ -183,49 +180,6 @@ func BuildKubernetesNodeConfig(options configapi.NodeConfig) (*NodeConfig, error
183 183
 		return nil, err
184 184
 	}
185 185
 
186
-	// Replace the standard k8s emptyDir volume plugin with a wrapper version
187
-	// which offers XFS quota functionality, but only if the node config
188
-	// specifies an empty dir quota to apply to projects:
189
-	if options.VolumeConfig.LocalQuota.PerFSGroup != nil {
190
-		glog.V(2).Info("Replacing empty-dir volume plugin with quota wrapper")
191
-		wrappedEmptyDirPlugin := false
192
-
193
-		quotaApplicator, err := empty_dir.NewQuotaApplicator(options.VolumeDirectory)
194
-		if err != nil {
195
-			return nil, err
196
-		}
197
-
198
-		// Create a volume spec with emptyDir we can use to search for the
199
-		// emptyDir plugin with CanSupport:
200
-		emptyDirSpec := &volume.Spec{
201
-			Volume: &kapi.Volume{
202
-				VolumeSource: kapi.VolumeSource{
203
-					EmptyDir: &kapi.EmptyDirVolumeSource{},
204
-				},
205
-			},
206
-		}
207
-
208
-		for idx, plugin := range cfg.VolumePlugins {
209
-			// Can't really do type checking or use a constant here as they are not exported:
210
-			if plugin.CanSupport(emptyDirSpec) {
211
-				wrapper := empty_dir.EmptyDirQuotaPlugin{
212
-					Wrapped:         plugin,
213
-					Quota:           *options.VolumeConfig.LocalQuota.PerFSGroup,
214
-					QuotaApplicator: quotaApplicator,
215
-				}
216
-				cfg.VolumePlugins[idx] = &wrapper
217
-				wrappedEmptyDirPlugin = true
218
-			}
219
-		}
220
-		// Because we can't look for the k8s emptyDir plugin by any means that would
221
-		// survive a refactor, error out if we couldn't find it:
222
-		if !wrappedEmptyDirPlugin {
223
-			return nil, errors.New("unable to wrap emptyDir volume plugin for quota support")
224
-		}
225
-	} else {
226
-		glog.V(2).Info("Skipping replacement of empty-dir volume plugin with quota wrapper, no local fsGroup quota specified")
227
-	}
228
-
229 186
 	// provide any config overrides
230 187
 	cfg.NodeName = options.NodeName
231 188
 	cfg.KubeClient = internalclientset.FromUnversionedClient(kubeClient)
... ...
@@ -305,8 +305,9 @@ func (c *MasterConfig) RunDeploymentController() {
305 305
 
306 306
 // RunDeployerPodController starts the deployer pod controller process.
307 307
 func (c *MasterConfig) RunDeployerPodController() {
308
-	_, kclient := c.DeployerPodControllerClients()
308
+	osclient, kclient := c.DeployerPodControllerClients()
309 309
 	factory := deployerpodcontroller.DeployerPodControllerFactory{
310
+		Client:     osclient,
310 311
 		KubeClient: kclient,
311 312
 		Codec:      c.EtcdHelper.Codec(),
312 313
 	}
... ...
@@ -567,6 +567,11 @@ func startControllers(oc *origin.MasterConfig, kc *kubernetes.MasterConfig) erro
567 567
 			glog.Fatalf("Could not get client for daemonset controller: %v", err)
568 568
 		}
569 569
 
570
+		_, _, gcClient, err := oc.GetServiceAccountClients(bootstrappolicy.InfraGCControllerServiceAccountName)
571
+		if err != nil {
572
+			glog.Fatalf("Could not get client for pod gc controller: %v", err)
573
+		}
574
+
570 575
 		namespaceControllerClientConfig, _, namespaceControllerKubeClient, err := oc.GetServiceAccountClients(bootstrappolicy.InfraNamespaceControllerServiceAccountName)
571 576
 		if err != nil {
572 577
 			glog.Fatalf("Could not get client for namespace controller: %v", err)
... ...
@@ -604,6 +609,7 @@ func startControllers(oc *origin.MasterConfig, kc *kubernetes.MasterConfig) erro
604 604
 		kc.RunPersistentVolumeClaimBinder(binderClient)
605 605
 		kc.RunPersistentVolumeProvisioner(provisionerClient)
606 606
 		kc.RunPersistentVolumeClaimRecycler(oc.ImageFor("recycler"), recyclerClient, oc.Options.PolicyConfig.OpenShiftInfrastructureNamespace)
607
+		kc.RunGCController(gcClient)
607 608
 
608 609
 		glog.Infof("Started Kubernetes Controllers")
609 610
 	} else {
... ...
@@ -301,6 +301,7 @@ func StartNode(nodeConfig configapi.NodeConfig, components *utilflags.ComponentF
301 301
 		config.EnsureKubeletAccess()
302 302
 		config.EnsureVolumeDir()
303 303
 		config.EnsureDocker(docker.NewHelper())
304
+		config.EnsureLocalQuota(nodeConfig) // must be performed after EnsureVolumeDir
304 305
 	}
305 306
 
306 307
 	// TODO: SDN plugin depends on the Kubelet registering as a Node and doesn't retry cleanly,
... ...
@@ -2,12 +2,16 @@ package deployerpod
2 2
 
3 3
 import (
4 4
 	"fmt"
5
+	"strconv"
5 6
 
6 7
 	"github.com/golang/glog"
7 8
 
8 9
 	kapi "k8s.io/kubernetes/pkg/api"
9 10
 	kerrors "k8s.io/kubernetes/pkg/api/errors"
11
+	"k8s.io/kubernetes/pkg/client/cache"
12
+	kclient "k8s.io/kubernetes/pkg/client/unversioned"
10 13
 
14
+	osclient "github.com/openshift/origin/pkg/client"
11 15
 	deployapi "github.com/openshift/origin/pkg/deploy/api"
12 16
 	deployutil "github.com/openshift/origin/pkg/deploy/util"
13 17
 )
... ...
@@ -17,14 +21,12 @@ import (
17 17
 //
18 18
 // Use the DeployerPodControllerFactory to create this controller.
19 19
 type DeployerPodController struct {
20
-	// deploymentClient provides access to deployments.
21
-	deploymentClient deploymentClient
22
-	// deployerPodsFor returns all deployer pods for the named deployment.
23
-	deployerPodsFor func(namespace, name string) (*kapi.PodList, error)
20
+	store   cache.Store
21
+	client  osclient.Interface
22
+	kClient kclient.Interface
23
+
24 24
 	// decodeConfig knows how to decode the deploymentConfig from a deployment's annotations.
25 25
 	decodeConfig func(deployment *kapi.ReplicationController) (*deployapi.DeploymentConfig, error)
26
-	// deletePod deletes a pod.
27
-	deletePod func(namespace, name string) error
28 26
 }
29 27
 
30 28
 // transientError is an error which will be retried indefinitely.
... ...
@@ -45,7 +47,17 @@ func (c *DeployerPodController) Handle(pod *kapi.Pod) error {
45 45
 		return nil
46 46
 	}
47 47
 
48
-	deployment, err := c.deploymentClient.getDeployment(pod.Namespace, deploymentName)
48
+	deployment := &kapi.ReplicationController{ObjectMeta: kapi.ObjectMeta{Namespace: pod.Namespace, Name: deploymentName}}
49
+	cached, exists, err := c.store.Get(deployment)
50
+	if err == nil && exists {
51
+		// Try to use the cache first. Trust hits and return them.
52
+		deployment = cached.(*kapi.ReplicationController)
53
+	} else {
54
+		// Double-check with the master for cache misses/errors, since those
55
+		// are rare and API calls are expensive but more reliable.
56
+		deployment, err = c.kClient.ReplicationControllers(pod.Namespace).Get(deploymentName)
57
+	}
58
+
49 59
 	// If the deployment for this pod has disappeared, we should clean up this
50 60
 	// and any other deployer pods, then bail out.
51 61
 	if err != nil {
... ...
@@ -54,14 +66,15 @@ func (c *DeployerPodController) Handle(pod *kapi.Pod) error {
54 54
 			return fmt.Errorf("couldn't get deployment %s/%s which owns deployer pod %s/%s", pod.Namespace, deploymentName, pod.Name, pod.Namespace)
55 55
 		}
56 56
 		// Find all the deployer pods for the deployment (including this one).
57
-		deployers, err := c.deployerPodsFor(pod.Namespace, deploymentName)
57
+		opts := kapi.ListOptions{LabelSelector: deployutil.DeployerPodSelector(deploymentName)}
58
+		deployers, err := c.kClient.Pods(pod.Namespace).List(opts)
58 59
 		if err != nil {
59 60
 			// Retry.
60 61
 			return fmt.Errorf("couldn't get deployer pods for %s: %v", deployutil.LabelForDeployment(deployment), err)
61 62
 		}
62 63
 		// Delete all deployers.
63 64
 		for _, deployer := range deployers.Items {
64
-			err := c.deletePod(deployer.Namespace, deployer.Name)
65
+			err := c.kClient.Pods(deployer.Namespace).Delete(deployer.Name, kapi.NewDeleteOptions(0))
65 66
 			if err != nil {
66 67
 				if !kerrors.IsNotFound(err) {
67 68
 					// TODO: Should this fire an event?
... ...
@@ -84,14 +97,8 @@ func (c *DeployerPodController) Handle(pod *kapi.Pod) error {
84 84
 			nextStatus = deployapi.DeploymentStatusRunning
85 85
 		}
86 86
 	case kapi.PodSucceeded:
87
-		// Detect failure based on the container state
88 87
 		nextStatus = deployapi.DeploymentStatusComplete
89
-		for _, info := range pod.Status.ContainerStatuses {
90
-			if info.State.Terminated != nil && info.State.Terminated.ExitCode != 0 {
91
-				nextStatus = deployapi.DeploymentStatusFailed
92
-				break
93
-			}
94
-		}
88
+
95 89
 		// Sync the internal replica annotation with the target so that we can
96 90
 		// distinguish deployer updates from other scaling events.
97 91
 		deployment.Annotations[deployapi.DeploymentReplicasAnnotation] = deployment.Annotations[deployapi.DesiredReplicasAnnotation]
... ...
@@ -114,42 +121,35 @@ func (c *DeployerPodController) Handle(pod *kapi.Pod) error {
114 114
 
115 115
 	if currentStatus != nextStatus {
116 116
 		deployment.Annotations[deployapi.DeploymentStatusAnnotation] = string(nextStatus)
117
-		if _, err := c.deploymentClient.updateDeployment(deployment.Namespace, deployment); err != nil {
117
+		if _, err := c.kClient.ReplicationControllers(deployment.Namespace).Update(deployment); err != nil {
118 118
 			if kerrors.IsNotFound(err) {
119 119
 				return nil
120 120
 			}
121 121
 			return fmt.Errorf("couldn't update Deployment %s to status %s: %v", deployutil.LabelForDeployment(deployment), nextStatus, err)
122 122
 		}
123 123
 		glog.V(4).Infof("Updated deployment %s status from %s to %s (scale: %d)", deployutil.LabelForDeployment(deployment), currentStatus, nextStatus, deployment.Spec.Replicas)
124
+
125
+		// If the deployment was canceled, trigger a reconcilation of its deployment config
126
+		// so that the latest complete deployment can immediately rollback in place of the
127
+		// canceled deployment.
128
+		if nextStatus == deployapi.DeploymentStatusFailed && deployutil.IsDeploymentCancelled(deployment) {
129
+			// If we are unable to get the deployment config, then the deploymentconfig controller will
130
+			// perform its duties once the resync interval forces the deploymentconfig to be reconciled.
131
+			name := deployutil.DeploymentConfigNameFor(deployment)
132
+			kclient.RetryOnConflict(kclient.DefaultRetry, func() error {
133
+				config, err := c.client.DeploymentConfigs(deployment.Namespace).Get(name)
134
+				if err != nil {
135
+					return err
136
+				}
137
+				if config.Annotations == nil {
138
+					config.Annotations = make(map[string]string)
139
+				}
140
+				config.Annotations[deployapi.DeploymentCancelledAnnotation] = strconv.Itoa(config.Status.LatestVersion)
141
+				_, err = c.client.DeploymentConfigs(config.Namespace).Update(config)
142
+				return err
143
+			})
144
+		}
124 145
 	}
125 146
 
126 147
 	return nil
127 148
 }
128
-
129
-// deploymentClient abstracts access to deployments.
130
-type deploymentClient interface {
131
-	getDeployment(namespace, name string) (*kapi.ReplicationController, error)
132
-	updateDeployment(namespace string, deployment *kapi.ReplicationController) (*kapi.ReplicationController, error)
133
-	// listDeploymentsForConfig should return deployments associated with the
134
-	// provided config.
135
-	listDeploymentsForConfig(namespace, configName string) (*kapi.ReplicationControllerList, error)
136
-}
137
-
138
-// deploymentClientImpl is a pluggable deploymentControllerDeploymentClient.
139
-type deploymentClientImpl struct {
140
-	getDeploymentFunc            func(namespace, name string) (*kapi.ReplicationController, error)
141
-	updateDeploymentFunc         func(namespace string, deployment *kapi.ReplicationController) (*kapi.ReplicationController, error)
142
-	listDeploymentsForConfigFunc func(namespace, configName string) (*kapi.ReplicationControllerList, error)
143
-}
144
-
145
-func (i *deploymentClientImpl) getDeployment(namespace, name string) (*kapi.ReplicationController, error) {
146
-	return i.getDeploymentFunc(namespace, name)
147
-}
148
-
149
-func (i *deploymentClientImpl) updateDeployment(namespace string, deployment *kapi.ReplicationController) (*kapi.ReplicationController, error) {
150
-	return i.updateDeploymentFunc(namespace, deployment)
151
-}
152
-
153
-func (i *deploymentClientImpl) listDeploymentsForConfig(namespace, configName string) (*kapi.ReplicationControllerList, error) {
154
-	return i.listDeploymentsForConfigFunc(namespace, configName)
155
-}
... ...
@@ -5,23 +5,78 @@ import (
5 5
 
6 6
 	kapi "k8s.io/kubernetes/pkg/api"
7 7
 	kerrors "k8s.io/kubernetes/pkg/api/errors"
8
+	"k8s.io/kubernetes/pkg/client/cache"
9
+	ktestclient "k8s.io/kubernetes/pkg/client/unversioned/testclient"
10
+	"k8s.io/kubernetes/pkg/runtime"
8 11
 	"k8s.io/kubernetes/pkg/util/sets"
9 12
 
13
+	"github.com/openshift/origin/pkg/client/testclient"
10 14
 	deployapi "github.com/openshift/origin/pkg/deploy/api"
11 15
 	_ "github.com/openshift/origin/pkg/deploy/api/install"
12 16
 	deploytest "github.com/openshift/origin/pkg/deploy/api/test"
13 17
 	deployutil "github.com/openshift/origin/pkg/deploy/util"
14 18
 )
15 19
 
20
+func okPod(deployment *kapi.ReplicationController) *kapi.Pod {
21
+	return &kapi.Pod{
22
+		ObjectMeta: kapi.ObjectMeta{
23
+			Name: deployutil.DeployerPodNameForDeployment(deployment.Name),
24
+			Labels: map[string]string{
25
+				deployapi.DeployerPodForDeploymentLabel: deployment.Name,
26
+			},
27
+			Annotations: map[string]string{
28
+				deployapi.DeploymentAnnotation: deployment.Name,
29
+			},
30
+		},
31
+		Status: kapi.PodStatus{
32
+			ContainerStatuses: []kapi.ContainerStatus{
33
+				{},
34
+			},
35
+		},
36
+	}
37
+}
38
+
39
+func succeededPod(deployment *kapi.ReplicationController) *kapi.Pod {
40
+	p := okPod(deployment)
41
+	p.Status.Phase = kapi.PodSucceeded
42
+	return p
43
+}
44
+
45
+func failedPod(deployment *kapi.ReplicationController) *kapi.Pod {
46
+	p := okPod(deployment)
47
+	p.Status.Phase = kapi.PodFailed
48
+	p.Status.ContainerStatuses = []kapi.ContainerStatus{
49
+		{
50
+			State: kapi.ContainerState{
51
+				Terminated: &kapi.ContainerStateTerminated{
52
+					ExitCode: 1,
53
+				},
54
+			},
55
+		},
56
+	}
57
+	return p
58
+}
59
+
60
+func terminatedPod(deployment *kapi.ReplicationController) *kapi.Pod {
61
+	p := okPod(deployment)
62
+	p.Status.Phase = kapi.PodFailed
63
+	return p
64
+}
65
+
66
+func runningPod(deployment *kapi.ReplicationController) *kapi.Pod {
67
+	p := okPod(deployment)
68
+	p.Status.Phase = kapi.PodRunning
69
+	return p
70
+}
71
+
16 72
 // TestHandle_uncorrelatedPod ensures that pods uncorrelated with a deployment
17 73
 // are ignored.
18 74
 func TestHandle_uncorrelatedPod(t *testing.T) {
75
+	kFake := &ktestclient.Fake{}
19 76
 	controller := &DeployerPodController{
20
-		deploymentClient: &deploymentClientImpl{
21
-			updateDeploymentFunc: func(namespace string, deployment *kapi.ReplicationController) (*kapi.ReplicationController, error) {
22
-				t.Fatalf("unexpected deployment update")
23
-				return nil, nil
24
-			},
77
+		kClient: kFake,
78
+		decodeConfig: func(deployment *kapi.ReplicationController) (*deployapi.DeploymentConfig, error) {
79
+			return deployutil.DecodeDeploymentConfig(deployment, kapi.Codecs.LegacyCodec(deployapi.SchemeGroupVersion))
25 80
 		},
26 81
 	}
27 82
 
... ...
@@ -29,51 +84,52 @@ func TestHandle_uncorrelatedPod(t *testing.T) {
29 29
 	deployment, _ := deployutil.MakeDeployment(deploytest.OkDeploymentConfig(1), kapi.Codecs.LegacyCodec(deployapi.SchemeGroupVersion))
30 30
 	pod := runningPod(deployment)
31 31
 	pod.Annotations = make(map[string]string)
32
-	err := controller.Handle(pod)
33 32
 
33
+	err := controller.Handle(pod)
34 34
 	if err != nil {
35 35
 		t.Fatalf("unexpected err: %v", err)
36 36
 	}
37
+	if len(kFake.Actions()) > 0 {
38
+		t.Fatalf("unexpected actions: %v", kFake.Actions())
39
+	}
37 40
 }
38 41
 
39 42
 // TestHandle_orphanedPod ensures that deployer pods associated with a non-
40 43
 // existent deployment results in all deployer pods being deleted.
41 44
 func TestHandle_orphanedPod(t *testing.T) {
42 45
 	deleted := sets.NewString()
46
+
47
+	kFake := &ktestclient.Fake{}
48
+	kFake.PrependReactor("get", "replicationcontrollers", func(action ktestclient.Action) (handled bool, ret runtime.Object, err error) {
49
+		name := action.(ktestclient.GetAction).GetName()
50
+		return true, nil, kerrors.NewNotFound(kapi.Resource("ReplicationController"), name)
51
+	})
52
+	kFake.PrependReactor("update", "replicationcontrollers", func(action ktestclient.Action) (handled bool, ret runtime.Object, err error) {
53
+		t.Fatalf("Unexpected deployment update")
54
+		return true, nil, nil
55
+	})
56
+	kFake.PrependReactor("list", "pods", func(action ktestclient.Action) (handled bool, ret runtime.Object, err error) {
57
+		mkpod := func(suffix string) kapi.Pod {
58
+			deployment, _ := deployutil.MakeDeployment(deploytest.OkDeploymentConfig(1), kapi.Codecs.LegacyCodec(deployapi.SchemeGroupVersion))
59
+			p := okPod(deployment)
60
+			p.Name = p.Name + suffix
61
+			return *p
62
+		}
63
+		return true, &kapi.PodList{Items: []kapi.Pod{mkpod(""), mkpod("-prehook"), mkpod("-posthook")}}, nil
64
+	})
65
+	kFake.PrependReactor("delete", "pods", func(action ktestclient.Action) (handled bool, ret runtime.Object, err error) {
66
+		name := action.(ktestclient.DeleteAction).GetName()
67
+		deleted.Insert(name)
68
+		return true, nil, nil
69
+	})
70
+
43 71
 	controller := &DeployerPodController{
44
-		deploymentClient: &deploymentClientImpl{
45
-			updateDeploymentFunc: func(namespace string, deployment *kapi.ReplicationController) (*kapi.ReplicationController, error) {
46
-				t.Fatalf("Unexpected deployment update")
47
-				return nil, nil
48
-			},
49
-			getDeploymentFunc: func(namespace, name string) (*kapi.ReplicationController, error) {
50
-				return nil, kerrors.NewNotFound(kapi.Resource("ReplicationController"), name)
51
-			},
52
-		},
53
-		deployerPodsFor: func(namespace, name string) (*kapi.PodList, error) {
54
-			mkpod := func(suffix string) kapi.Pod {
55
-				deployment, _ := deployutil.MakeDeployment(deploytest.OkDeploymentConfig(1), kapi.Codecs.LegacyCodec(deployapi.SchemeGroupVersion))
56
-				p := okPod(deployment)
57
-				p.Name = p.Name + suffix
58
-				return *p
59
-			}
60
-			return &kapi.PodList{
61
-				Items: []kapi.Pod{
62
-					mkpod(""),
63
-					mkpod("-prehook"),
64
-					mkpod("-posthook"),
65
-				},
66
-			}, nil
67
-		},
68
-		deletePod: func(namespace, name string) error {
69
-			deleted.Insert(name)
70
-			return nil
71
-		},
72
+		store:   cache.NewStore(cache.MetaNamespaceKeyFunc),
73
+		kClient: kFake,
72 74
 	}
73 75
 
74 76
 	deployment, _ := deployutil.MakeDeployment(deploytest.OkDeploymentConfig(1), kapi.Codecs.LegacyCodec(deployapi.SchemeGroupVersion))
75 77
 	err := controller.Handle(runningPod(deployment))
76
-
77 78
 	if err != nil {
78 79
 		t.Fatalf("unexpected error: %v", err)
79 80
 	}
... ...
@@ -91,20 +147,21 @@ func TestHandle_runningPod(t *testing.T) {
91 91
 	deployment.Annotations[deployapi.DeploymentStatusAnnotation] = string(deployapi.DeploymentStatusPending)
92 92
 	var updatedDeployment *kapi.ReplicationController
93 93
 
94
+	kFake := &ktestclient.Fake{}
95
+	kFake.PrependReactor("get", "replicationcontrollers", func(action ktestclient.Action) (handled bool, ret runtime.Object, err error) {
96
+		return true, deployment, nil
97
+	})
98
+	kFake.PrependReactor("update", "replicationcontrollers", func(action ktestclient.Action) (handled bool, ret runtime.Object, err error) {
99
+		updatedDeployment = deployment
100
+		return true, deployment, nil
101
+	})
102
+
94 103
 	controller := &DeployerPodController{
95
-		deploymentClient: &deploymentClientImpl{
96
-			getDeploymentFunc: func(namespace, name string) (*kapi.ReplicationController, error) {
97
-				return deployment, nil
98
-			},
99
-			updateDeploymentFunc: func(namespace string, deployment *kapi.ReplicationController) (*kapi.ReplicationController, error) {
100
-				updatedDeployment = deployment
101
-				return deployment, nil
102
-			},
103
-		},
104
+		store:   cache.NewStore(cache.MetaNamespaceKeyFunc),
105
+		kClient: kFake,
104 106
 	}
105 107
 
106 108
 	err := controller.Handle(runningPod(deployment))
107
-
108 109
 	if err != nil {
109 110
 		t.Fatalf("unexpected error: %v", err)
110 111
 	}
... ...
@@ -126,23 +183,24 @@ func TestHandle_podTerminatedOk(t *testing.T) {
126 126
 	deployment.Annotations[deployapi.DeploymentStatusAnnotation] = string(deployapi.DeploymentStatusRunning)
127 127
 	var updatedDeployment *kapi.ReplicationController
128 128
 
129
+	kFake := &ktestclient.Fake{}
130
+	kFake.PrependReactor("get", "replicationcontrollers", func(action ktestclient.Action) (handled bool, ret runtime.Object, err error) {
131
+		return true, deployment, nil
132
+	})
133
+	kFake.PrependReactor("update", "replicationcontrollers", func(action ktestclient.Action) (handled bool, ret runtime.Object, err error) {
134
+		updatedDeployment = deployment
135
+		return true, deployment, nil
136
+	})
137
+
129 138
 	controller := &DeployerPodController{
130 139
 		decodeConfig: func(deployment *kapi.ReplicationController) (*deployapi.DeploymentConfig, error) {
131 140
 			return deployutil.DecodeDeploymentConfig(deployment, kapi.Codecs.UniversalDecoder())
132 141
 		},
133
-		deploymentClient: &deploymentClientImpl{
134
-			getDeploymentFunc: func(namespace, name string) (*kapi.ReplicationController, error) {
135
-				return deployment, nil
136
-			},
137
-			updateDeploymentFunc: func(namespace string, deployment *kapi.ReplicationController) (*kapi.ReplicationController, error) {
138
-				updatedDeployment = deployment
139
-				return deployment, nil
140
-			},
141
-		},
142
+		store:   cache.NewStore(cache.MetaNamespaceKeyFunc),
143
+		kClient: kFake,
142 144
 	}
143 145
 
144 146
 	err := controller.Handle(succeededPod(deployment))
145
-
146 147
 	if err != nil {
147 148
 		t.Fatalf("unexpected error: %v", err)
148 149
 	}
... ...
@@ -167,23 +225,24 @@ func TestHandle_podTerminatedOkTest(t *testing.T) {
167 167
 	deployment.Annotations[deployapi.DeploymentStatusAnnotation] = string(deployapi.DeploymentStatusRunning)
168 168
 	var updatedDeployment *kapi.ReplicationController
169 169
 
170
+	kFake := &ktestclient.Fake{}
171
+	kFake.PrependReactor("get", "replicationcontrollers", func(action ktestclient.Action) (handled bool, ret runtime.Object, err error) {
172
+		return true, deployment, nil
173
+	})
174
+	kFake.PrependReactor("update", "replicationcontrollers", func(action ktestclient.Action) (handled bool, ret runtime.Object, err error) {
175
+		updatedDeployment = deployment
176
+		return true, deployment, nil
177
+	})
178
+
170 179
 	controller := &DeployerPodController{
171 180
 		decodeConfig: func(deployment *kapi.ReplicationController) (*deployapi.DeploymentConfig, error) {
172 181
 			return deployutil.DecodeDeploymentConfig(deployment, kapi.Codecs.UniversalDecoder())
173 182
 		},
174
-		deploymentClient: &deploymentClientImpl{
175
-			getDeploymentFunc: func(namespace, name string) (*kapi.ReplicationController, error) {
176
-				return deployment, nil
177
-			},
178
-			updateDeploymentFunc: func(namespace string, deployment *kapi.ReplicationController) (*kapi.ReplicationController, error) {
179
-				updatedDeployment = deployment
180
-				return deployment, nil
181
-			},
182
-		},
183
+		store:   cache.NewStore(cache.MetaNamespaceKeyFunc),
184
+		kClient: kFake,
183 185
 	}
184 186
 
185 187
 	err := controller.Handle(succeededPod(deployment))
186
-
187 188
 	if err != nil {
188 189
 		t.Fatalf("unexpected error: %v", err)
189 190
 	}
... ...
@@ -211,26 +270,24 @@ func TestHandle_podTerminatedFailNoContainerStatus(t *testing.T) {
211 211
 	// this also tests that the error is just logged and not result in a failure
212 212
 	deployment.Annotations[deployapi.DeploymentStatusAnnotation] = string(deployapi.DeploymentStatusRunning)
213 213
 
214
+	kFake := &ktestclient.Fake{}
215
+	kFake.PrependReactor("get", "replicationcontrollers", func(action ktestclient.Action) (handled bool, ret runtime.Object, err error) {
216
+		return true, deployment, nil
217
+	})
218
+	kFake.PrependReactor("update", "replicationcontrollers", func(action ktestclient.Action) (handled bool, ret runtime.Object, err error) {
219
+		updatedDeployment = deployment
220
+		return true, deployment, nil
221
+	})
222
+
214 223
 	controller := &DeployerPodController{
215 224
 		decodeConfig: func(deployment *kapi.ReplicationController) (*deployapi.DeploymentConfig, error) {
216 225
 			return deployutil.DecodeDeploymentConfig(deployment, kapi.Codecs.UniversalDecoder())
217 226
 		},
218
-		deploymentClient: &deploymentClientImpl{
219
-			getDeploymentFunc: func(namespace, name string) (*kapi.ReplicationController, error) {
220
-				return deployment, nil
221
-			},
222
-			updateDeploymentFunc: func(namespace string, deployment *kapi.ReplicationController) (*kapi.ReplicationController, error) {
223
-				updatedDeployment = deployment
224
-				return deployment, nil
225
-			},
226
-			listDeploymentsForConfigFunc: func(namespace, configName string) (*kapi.ReplicationControllerList, error) {
227
-				return &kapi.ReplicationControllerList{Items: []kapi.ReplicationController{*deployment}}, nil
228
-			},
229
-		},
227
+		store:   cache.NewStore(cache.MetaNamespaceKeyFunc),
228
+		kClient: kFake,
230 229
 	}
231 230
 
232 231
 	err := controller.Handle(terminatedPod(deployment))
233
-
234 232
 	if err != nil {
235 233
 		t.Fatalf("unexpected error: %v", err)
236 234
 	}
... ...
@@ -258,26 +315,24 @@ func TestHandle_podTerminatedFailNoContainerStatusTest(t *testing.T) {
258 258
 	// this also tests that the error is just logged and not result in a failure
259 259
 	deployment.Annotations[deployapi.DeploymentStatusAnnotation] = string(deployapi.DeploymentStatusRunning)
260 260
 
261
+	kFake := &ktestclient.Fake{}
262
+	kFake.PrependReactor("get", "replicationcontrollers", func(action ktestclient.Action) (handled bool, ret runtime.Object, err error) {
263
+		return true, deployment, nil
264
+	})
265
+	kFake.PrependReactor("update", "replicationcontrollers", func(action ktestclient.Action) (handled bool, ret runtime.Object, err error) {
266
+		updatedDeployment = deployment
267
+		return true, deployment, nil
268
+	})
269
+
261 270
 	controller := &DeployerPodController{
262 271
 		decodeConfig: func(deployment *kapi.ReplicationController) (*deployapi.DeploymentConfig, error) {
263 272
 			return deployutil.DecodeDeploymentConfig(deployment, kapi.Codecs.UniversalDecoder())
264 273
 		},
265
-		deploymentClient: &deploymentClientImpl{
266
-			getDeploymentFunc: func(namespace, name string) (*kapi.ReplicationController, error) {
267
-				return deployment, nil
268
-			},
269
-			updateDeploymentFunc: func(namespace string, deployment *kapi.ReplicationController) (*kapi.ReplicationController, error) {
270
-				updatedDeployment = deployment
271
-				return deployment, nil
272
-			},
273
-			listDeploymentsForConfigFunc: func(namespace, configName string) (*kapi.ReplicationControllerList, error) {
274
-				return &kapi.ReplicationControllerList{Items: []kapi.ReplicationController{*deployment}}, nil
275
-			},
276
-		},
274
+		store:   cache.NewStore(cache.MetaNamespaceKeyFunc),
275
+		kClient: kFake,
277 276
 	}
278 277
 
279 278
 	err := controller.Handle(terminatedPod(deployment))
280
-
281 279
 	if err != nil {
282 280
 		t.Fatalf("unexpected error: %v", err)
283 281
 	}
... ...
@@ -320,22 +375,21 @@ func TestHandle_cleanupDesiredReplicasAnnotation(t *testing.T) {
320 320
 		var updatedDeployment *kapi.ReplicationController
321 321
 		deployment.Annotations[deployapi.DesiredReplicasAnnotation] = "1"
322 322
 
323
+		kFake := &ktestclient.Fake{}
324
+		kFake.PrependReactor("get", "replicationcontrollers", func(action ktestclient.Action) (handled bool, ret runtime.Object, err error) {
325
+			return true, deployment, nil
326
+		})
327
+		kFake.PrependReactor("update", "replicationcontrollers", func(action ktestclient.Action) (handled bool, ret runtime.Object, err error) {
328
+			updatedDeployment = deployment
329
+			return true, deployment, nil
330
+		})
331
+
323 332
 		controller := &DeployerPodController{
324 333
 			decodeConfig: func(deployment *kapi.ReplicationController) (*deployapi.DeploymentConfig, error) {
325 334
 				return deployutil.DecodeDeploymentConfig(deployment, kapi.Codecs.UniversalDecoder())
326 335
 			},
327
-			deploymentClient: &deploymentClientImpl{
328
-				getDeploymentFunc: func(namespace, name string) (*kapi.ReplicationController, error) {
329
-					return deployment, nil
330
-				},
331
-				updateDeploymentFunc: func(namespace string, deployment *kapi.ReplicationController) (*kapi.ReplicationController, error) {
332
-					updatedDeployment = deployment
333
-					return deployment, nil
334
-				},
335
-				listDeploymentsForConfigFunc: func(namespace, configName string) (*kapi.ReplicationControllerList, error) {
336
-					return &kapi.ReplicationControllerList{Items: []kapi.ReplicationController{*deployment}}, nil
337
-				},
338
-			},
336
+			store:   cache.NewStore(cache.MetaNamespaceKeyFunc),
337
+			kClient: kFake,
339 338
 		}
340 339
 
341 340
 		if err := controller.Handle(test.pod); err != nil {
... ...
@@ -354,51 +408,63 @@ func TestHandle_cleanupDesiredReplicasAnnotation(t *testing.T) {
354 354
 	}
355 355
 }
356 356
 
357
-func okPod(deployment *kapi.ReplicationController) *kapi.Pod {
358
-	return &kapi.Pod{
359
-		ObjectMeta: kapi.ObjectMeta{
360
-			Name: deployutil.DeployerPodNameForDeployment(deployment.Name),
361
-			Annotations: map[string]string{
362
-				deployapi.DeploymentAnnotation: deployment.Name,
363
-			},
364
-		},
365
-		Status: kapi.PodStatus{
366
-			ContainerStatuses: []kapi.ContainerStatus{
367
-				{},
368
-			},
357
+// TestHandle_canceledDeploymentTrigger ensures that a canceled deployment
358
+// will trigger a reconcilation of its deploymentconfig (via an annotation
359
+// update) so that rolling back can happen on the spot and not rely on the
360
+// deploymentconfig cache resync interval.
361
+func TestHandle_canceledDeploymentTriggerTest(t *testing.T) {
362
+	var (
363
+		updatedDeployment *kapi.ReplicationController
364
+		updatedConfig     *deployapi.DeploymentConfig
365
+	)
366
+
367
+	initial := deploytest.OkDeploymentConfig(1)
368
+	// Canceled deployment
369
+	deployment, _ := deployutil.MakeDeployment(deploytest.TestDeploymentConfig(initial), kapi.Codecs.LegacyCodec(deployapi.SchemeGroupVersion))
370
+	deployment.Annotations[deployapi.DeploymentCancelledAnnotation] = deployapi.DeploymentCancelledAnnotationValue
371
+
372
+	kFake := &ktestclient.Fake{}
373
+	kFake.PrependReactor("get", "replicationcontrollers", func(action ktestclient.Action) (handled bool, ret runtime.Object, err error) {
374
+		return true, deployment, nil
375
+	})
376
+	kFake.PrependReactor("update", "replicationcontrollers", func(action ktestclient.Action) (handled bool, ret runtime.Object, err error) {
377
+		updatedDeployment = deployment
378
+		return true, deployment, nil
379
+	})
380
+	fake := &testclient.Fake{}
381
+	fake.PrependReactor("get", "deploymentconfigs", func(action ktestclient.Action) (handled bool, ret runtime.Object, err error) {
382
+		config := initial
383
+		return true, config, nil
384
+	})
385
+	fake.PrependReactor("update", "deploymentconfigs", func(action ktestclient.Action) (handled bool, ret runtime.Object, err error) {
386
+		updated := action.(ktestclient.UpdateAction).GetObject().(*deployapi.DeploymentConfig)
387
+		updatedConfig = updated
388
+		return true, updated, nil
389
+	})
390
+
391
+	controller := &DeployerPodController{
392
+		decodeConfig: func(deployment *kapi.ReplicationController) (*deployapi.DeploymentConfig, error) {
393
+			return deployutil.DecodeDeploymentConfig(deployment, kapi.Codecs.UniversalDecoder())
369 394
 		},
395
+		store:   cache.NewStore(cache.MetaNamespaceKeyFunc),
396
+		client:  fake,
397
+		kClient: kFake,
370 398
 	}
371
-}
372 399
 
373
-func succeededPod(deployment *kapi.ReplicationController) *kapi.Pod {
374
-	p := okPod(deployment)
375
-	p.Status.Phase = kapi.PodSucceeded
376
-	return p
377
-}
400
+	err := controller.Handle(terminatedPod(deployment))
401
+	if err != nil {
402
+		t.Fatalf("unexpected error: %v", err)
403
+	}
378 404
 
379
-func failedPod(deployment *kapi.ReplicationController) *kapi.Pod {
380
-	p := okPod(deployment)
381
-	p.Status.Phase = kapi.PodFailed
382
-	p.Status.ContainerStatuses = []kapi.ContainerStatus{
383
-		{
384
-			State: kapi.ContainerState{
385
-				Terminated: &kapi.ContainerStateTerminated{
386
-					ExitCode: 1,
387
-				},
388
-			},
389
-		},
405
+	if updatedDeployment == nil {
406
+		t.Fatalf("expected deployment update")
390 407
 	}
391
-	return p
392
-}
393 408
 
394
-func terminatedPod(deployment *kapi.ReplicationController) *kapi.Pod {
395
-	p := okPod(deployment)
396
-	p.Status.Phase = kapi.PodFailed
397
-	return p
398
-}
409
+	if e, a := deployapi.DeploymentStatusFailed, deployutil.DeploymentStatusFor(updatedDeployment); e != a {
410
+		t.Fatalf("expected updated deployment status %s, got %s", e, a)
411
+	}
399 412
 
400
-func runningPod(deployment *kapi.ReplicationController) *kapi.Pod {
401
-	p := okPod(deployment)
402
-	p.Status.Phase = kapi.PodRunning
403
-	return p
413
+	if updatedConfig == nil {
414
+		t.Fatalf("expected config update")
415
+	}
404 416
 }
... ...
@@ -11,6 +11,7 @@ import (
11 11
 	utilruntime "k8s.io/kubernetes/pkg/util/runtime"
12 12
 	"k8s.io/kubernetes/pkg/watch"
13 13
 
14
+	osclient "github.com/openshift/origin/pkg/client"
14 15
 	controller "github.com/openshift/origin/pkg/controller"
15 16
 	deployapi "github.com/openshift/origin/pkg/deploy/api"
16 17
 	deployutil "github.com/openshift/origin/pkg/deploy/util"
... ...
@@ -19,6 +20,8 @@ import (
19 19
 // DeployerPodControllerFactory can create a DeployerPodController which
20 20
 // handles processing deployer pods.
21 21
 type DeployerPodControllerFactory struct {
22
+	// Client is an OpenShift client.
23
+	Client osclient.Interface
22 24
 	// KubeClient is a Kubernetes client.
23 25
 	KubeClient kclient.Interface
24 26
 	// Codec is used for encoding/decoding.
... ...
@@ -57,36 +60,12 @@ func (factory *DeployerPodControllerFactory) Create() controller.RunnableControl
57 57
 	cache.NewReflector(podLW, &kapi.Pod{}, podQueue, 2*time.Minute).Run()
58 58
 
59 59
 	podController := &DeployerPodController{
60
-		deploymentClient: &deploymentClientImpl{
61
-			getDeploymentFunc: func(namespace, name string) (*kapi.ReplicationController, error) {
62
-				// Try to use the cache first. Trust hits and return them.
63
-				example := &kapi.ReplicationController{ObjectMeta: kapi.ObjectMeta{Namespace: namespace, Name: name}}
64
-				cached, exists, err := deploymentStore.Get(example)
65
-				if err == nil && exists {
66
-					return cached.(*kapi.ReplicationController), nil
67
-				}
68
-				// Double-check with the master for cache misses/errors, since those
69
-				// are rare and API calls are expensive but more reliable.
70
-				return factory.KubeClient.ReplicationControllers(namespace).Get(name)
71
-			},
72
-			updateDeploymentFunc: func(namespace string, deployment *kapi.ReplicationController) (*kapi.ReplicationController, error) {
73
-				return factory.KubeClient.ReplicationControllers(namespace).Update(deployment)
74
-			},
75
-			listDeploymentsForConfigFunc: func(namespace, configName string) (*kapi.ReplicationControllerList, error) {
76
-				opts := kapi.ListOptions{LabelSelector: deployutil.ConfigSelector(configName)}
77
-				return factory.KubeClient.ReplicationControllers(namespace).List(opts)
78
-			},
79
-		},
80
-		deployerPodsFor: func(namespace, name string) (*kapi.PodList, error) {
81
-			opts := kapi.ListOptions{LabelSelector: deployutil.DeployerPodSelector(name)}
82
-			return factory.KubeClient.Pods(namespace).List(opts)
83
-		},
60
+		store:   deploymentStore,
61
+		client:  factory.Client,
62
+		kClient: factory.KubeClient,
84 63
 		decodeConfig: func(deployment *kapi.ReplicationController) (*deployapi.DeploymentConfig, error) {
85 64
 			return deployutil.DecodeDeploymentConfig(deployment, factory.Codec)
86 65
 		},
87
-		deletePod: func(namespace, name string) error {
88
-			return factory.KubeClient.Pods(namespace).Delete(name, kapi.NewDeleteOptions(0))
89
-		},
90 66
 	}
91 67
 
92 68
 	return &controller.RetryController{
... ...
@@ -96,7 +96,7 @@ func (factory *DeploymentControllerFactory) Create() controller.RunnableControll
96 96
 		decodeConfig: func(deployment *kapi.ReplicationController) (*deployapi.DeploymentConfig, error) {
97 97
 			return deployutil.DecodeDeploymentConfig(deployment, factory.Codec)
98 98
 		},
99
-		recorder: eventBroadcaster.NewRecorder(kapi.EventSource{Component: "deployer"}),
99
+		recorder: eventBroadcaster.NewRecorder(kapi.EventSource{Component: "deployment-controller"}),
100 100
 	}
101 101
 
102 102
 	return &controller.RetryController{
... ...
@@ -9,7 +9,6 @@ import (
9 9
 
10 10
 	kapi "k8s.io/kubernetes/pkg/api"
11 11
 	kclient "k8s.io/kubernetes/pkg/client/unversioned"
12
-	"k8s.io/kubernetes/pkg/labels"
13 12
 
14 13
 	authorizationapi "github.com/openshift/origin/pkg/authorization/api"
15 14
 	osclient "github.com/openshift/origin/pkg/client"
... ...
@@ -94,7 +93,7 @@ func (d *MasterNode) CanRun() (bool, error) {
94 94
 func (d *MasterNode) Check() types.DiagnosticResult {
95 95
 	r := types.NewDiagnosticResult(MasterNodeName)
96 96
 
97
-	nodes, err := d.KubeClient.Nodes().List(kapi.ListOptions{LabelSelector: labels.Nothing()})
97
+	nodes, err := d.KubeClient.Nodes().List(kapi.ListOptions{})
98 98
 	if err != nil {
99 99
 		r.Error("DClu3002", err, fmt.Sprintf(clientErrorGettingNodes, err))
100 100
 		return r
... ...
@@ -320,7 +320,10 @@ func (c *AppConfig) addReferenceBuilderComponents(b *app.ReferenceBuilder) {
320 320
 		input.Argument = fmt.Sprintf("--image-stream=%q", input.From)
321 321
 		input.Searcher = c.ImageStreamSearcher
322 322
 		if c.ImageStreamSearcher != nil {
323
-			input.Resolver = app.FirstMatchResolver{Searcher: c.ImageStreamSearcher}
323
+			resolver := app.PerfectMatchWeightedResolver{
324
+				app.WeightedResolver{Searcher: c.ImageStreamSearcher},
325
+			}
326
+			input.Resolver = resolver
324 327
 		}
325 328
 		return input
326 329
 	})
... ...
@@ -328,7 +331,7 @@ func (c *AppConfig) addReferenceBuilderComponents(b *app.ReferenceBuilder) {
328 328
 		input.Argument = fmt.Sprintf("--template=%q", input.From)
329 329
 		input.Searcher = c.TemplateSearcher
330 330
 		if c.TemplateSearcher != nil {
331
-			input.Resolver = app.HighestScoreResolver{Searcher: c.TemplateSearcher}
331
+			input.Resolver = app.HighestUniqueScoreResolver{Searcher: c.TemplateSearcher}
332 332
 		}
333 333
 		return input
334 334
 	})
... ...
@@ -18,6 +18,9 @@ type ComponentMatch struct {
18 18
 	Insecure    bool
19 19
 	LocalOnly   bool
20 20
 	NoTagsFound bool
21
+	// this match represents a scratch image, there is no
22
+	// actual image/pullspec.
23
+	Virtual bool
21 24
 
22 25
 	// The source of the match. Generally only a single source is
23 26
 	// available.
... ...
@@ -56,6 +56,7 @@ func (r DockerClientSearcher) Search(precise bool, terms ...string) (ComponentMa
56 56
 				// we don't want to create an imagestream for "scratch", so treat
57 57
 				// it as a local only image.
58 58
 				LocalOnly: true,
59
+				Virtual:   true,
59 60
 			})
60 61
 			return componentMatches, errs
61 62
 		}
... ...
@@ -312,12 +312,17 @@ func (r *ImageStreamByAnnotationSearcher) annotationMatches(stream *imageapi.Ima
312 312
 
313 313
 		imageData := imageStream.Image
314 314
 		matchName := fmt.Sprintf("%s/%s", stream.Namespace, stream.Name)
315
+		description := fmt.Sprintf("Image stream %q in project %q", stream.Name, stream.Namespace)
316
+		if len(tag) > 0 {
317
+			matchName = fmt.Sprintf("%s:%s", matchName, tag)
318
+			description = fmt.Sprintf("Image stream %q (tag %q) in project %q", stream.Name, tag, stream.Namespace)
319
+		}
315 320
 		glog.V(5).Infof("ImageStreamAnnotationSearcher match found: %s for %s with score %f", matchName, value, score)
316 321
 		match := &ComponentMatch{
317 322
 			Value:       value,
318 323
 			Name:        fmt.Sprintf("%s", matchName),
319 324
 			Argument:    fmt.Sprintf("--image-stream=%q", matchName),
320
-			Description: fmt.Sprintf("Image stream %s in project %s", stream.Name, stream.Namespace),
325
+			Description: description,
321 326
 			Score:       score,
322 327
 
323 328
 			ImageStream: stream,
... ...
@@ -64,7 +64,7 @@ func (pb *pipelineBuilder) NewBuildPipeline(from string, resolvedMatch *Componen
64 64
 			return nil, fmt.Errorf("can't build %q: %v", from, err)
65 65
 		}
66 66
 		input = inputImage
67
-		if !input.AsImageStream && resolvedMatch.Value != "scratch" {
67
+		if !input.AsImageStream && !resolvedMatch.Virtual {
68 68
 			msg := "Could not find an image stream match for %q. Make sure that a Docker image with that tag is available on the node for the build to succeed."
69 69
 			glog.Warningf(msg, resolvedMatch.Value)
70 70
 		}
... ...
@@ -150,7 +150,7 @@ func (r *REST) Create(ctx kapi.Context, obj runtime.Object) (runtime.Object, err
150 150
 		}
151 151
 	} else {
152 152
 		if len(inputMeta.ResourceVersion) > 0 && inputMeta.ResourceVersion != stream.ResourceVersion {
153
-			glog.V(4).Infof("DEBUG: mismatch between requested UID %s and located UID %s", inputMeta.UID, stream.UID)
153
+			glog.V(4).Infof("DEBUG: mismatch between requested ResourceVersion %s and located ResourceVersion %s", inputMeta.ResourceVersion, stream.ResourceVersion)
154 154
 			return nil, kapierrors.NewConflict(api.Resource("imagestream"), inputMeta.Name, fmt.Errorf("the image stream was updated from %q to %q", inputMeta.ResourceVersion, stream.ResourceVersion))
155 155
 		}
156 156
 		if len(inputMeta.UID) > 0 && inputMeta.UID != stream.UID {
... ...
@@ -45,8 +45,14 @@ func NewStatusAdmitter(plugin router.Plugin, client client.RoutesNamespacer, nam
45 45
 	}
46 46
 }
47 47
 
48
+// Return a time truncated to the second to ensure that in-memory and
49
+// serialized timestamps can be safely compared.
50
+func getRfc3339Timestamp() unversioned.Time {
51
+	return unversioned.Now().Rfc3339Copy()
52
+}
53
+
48 54
 // nowFn allows the package to be tested
49
-var nowFn = unversioned.Now
55
+var nowFn = getRfc3339Timestamp
50 56
 
51 57
 // findOrCreateIngress loops through the router status ingress array looking for an entry
52 58
 // that matches name. If there is no entry in the array, it creates one and appends it
... ...
@@ -36,7 +36,7 @@ func (p *fakePlugin) HandleNamespaces(namespaces sets.String) error {
36 36
 }
37 37
 
38 38
 func TestStatusNoOp(t *testing.T) {
39
-	now := unversioned.Now()
39
+	now := nowFn()
40 40
 	touched := unversioned.Time{Time: now.Add(-time.Minute)}
41 41
 	p := &fakePlugin{}
42 42
 	c := testclient.NewSimpleFake()
... ...
@@ -118,7 +118,7 @@ func TestStatusResetsHost(t *testing.T) {
118 118
 }
119 119
 
120 120
 func TestStatusAdmitsRouteOnForbidden(t *testing.T) {
121
-	now := unversioned.Now()
121
+	now := nowFn()
122 122
 	nowFn = func() unversioned.Time { return now }
123 123
 	touched := unversioned.Time{Time: now.Add(-time.Minute)}
124 124
 	p := &fakePlugin{}
... ...
@@ -167,7 +167,7 @@ func TestStatusAdmitsRouteOnForbidden(t *testing.T) {
167 167
 }
168 168
 
169 169
 func TestStatusBackoffOnConflict(t *testing.T) {
170
-	now := unversioned.Now()
170
+	now := nowFn()
171 171
 	nowFn = func() unversioned.Time { return now }
172 172
 	touched := unversioned.Time{Time: now.Add(-time.Minute)}
173 173
 	p := &fakePlugin{}
... ...
@@ -217,7 +217,7 @@ func TestStatusBackoffOnConflict(t *testing.T) {
217 217
 }
218 218
 
219 219
 func TestStatusRecordRejection(t *testing.T) {
220
-	now := unversioned.Now()
220
+	now := nowFn()
221 221
 	nowFn = func() unversioned.Time { return now }
222 222
 	p := &fakePlugin{}
223 223
 	c := testclient.NewSimpleFake(&routeapi.Route{})
... ...
@@ -248,7 +248,7 @@ func TestStatusRecordRejection(t *testing.T) {
248 248
 }
249 249
 
250 250
 func TestStatusRecordRejectionNoChange(t *testing.T) {
251
-	now := unversioned.Now()
251
+	now := nowFn()
252 252
 	nowFn = func() unversioned.Time { return now }
253 253
 	touched := unversioned.Time{Time: now.Add(-time.Minute)}
254 254
 	p := &fakePlugin{}
... ...
@@ -285,7 +285,7 @@ func TestStatusRecordRejectionNoChange(t *testing.T) {
285 285
 }
286 286
 
287 287
 func TestStatusRecordRejectionWithStatus(t *testing.T) {
288
-	now := unversioned.Now()
288
+	now := nowFn()
289 289
 	nowFn = func() unversioned.Time { return now }
290 290
 	touched := unversioned.Time{Time: now.Add(-time.Minute)}
291 291
 	p := &fakePlugin{}
... ...
@@ -332,7 +332,7 @@ func TestStatusRecordRejectionWithStatus(t *testing.T) {
332 332
 }
333 333
 
334 334
 func TestStatusRecordRejectionConflict(t *testing.T) {
335
-	now := unversioned.Now()
335
+	now := nowFn()
336 336
 	nowFn = func() unversioned.Time { return now }
337 337
 	touched := unversioned.Time{Time: now.Add(-time.Minute)}
338 338
 	p := &fakePlugin{}
... ...
@@ -40,9 +40,8 @@ func (d *sccExecRestrictions) Admit(a admission.Attributes) (err error) {
40 40
 		return admission.NewForbidden(a, err)
41 41
 	}
42 42
 
43
-	// create a synthentic admission attribute to check SCC admission status for this pod
44
-	// clear the SA name, so that any permissions MUST be based on your user's power, not the SAs power.
45
-	pod.Spec.ServiceAccountName = ""
43
+	// TODO, if we want to actually limit who can use which service account, then we'll need to add logic here to make sure that
44
+	// we're allowed to use the SA the pod is using.  Otherwise, user-A creates pod and user-B (who can't use the SA) can exec into it.
46 45
 	createAttributes := admission.NewAttributesRecord(pod, kapi.Kind("Pod"), a.GetNamespace(), a.GetName(), a.GetResource(), a.GetSubresource(), admission.Create, a.GetUserInfo())
47 46
 	if err := d.constraintAdmission.Admit(createAttributes); err != nil {
48 47
 		return admission.NewForbidden(a, err)
... ...
@@ -110,10 +110,5 @@ func TestExecAdmit(t *testing.T) {
110 110
 			t.Errorf("%s: no actions found", k)
111 111
 		}
112 112
 
113
-		if v.shouldHaveClientAction {
114
-			if len(v.pod.Spec.ServiceAccountName) != 0 {
115
-				t.Errorf("%s: sa name should have been cleared: %v", k, v.pod.Spec.ServiceAccountName)
116
-			}
117
-		}
118 113
 	}
119 114
 }
... ...
@@ -65,10 +65,18 @@ func Clients(config restclient.Config, tokenRetriever TokenRetriever, namespace,
65 65
 	config.KeyData = []byte{}
66 66
 	config.BearerToken = ""
67 67
 
68
+	kubeUserAgent := ""
69
+	openshiftUserAgent := ""
70
+
71
+	// they specified, don't mess with it
68 72
 	if len(config.UserAgent) > 0 {
69
-		config.UserAgent += " "
73
+		kubeUserAgent = config.UserAgent
74
+		openshiftUserAgent = config.UserAgent
75
+
76
+	} else {
77
+		kubeUserAgent = fmt.Sprintf("%s system:serviceaccount:%s:%s", restclient.DefaultKubernetesUserAgent(), namespace, name)
78
+		openshiftUserAgent = fmt.Sprintf("%s system:serviceaccount:%s:%s", client.DefaultOpenShiftUserAgent(), namespace, name)
70 79
 	}
71
-	config.UserAgent += fmt.Sprintf("system:serviceaccount:%s:%s", namespace, name)
72 80
 
73 81
 	// For now, just initialize the token once
74 82
 	// TODO: refetch the token if the client encounters 401 errors
... ...
@@ -78,11 +86,13 @@ func Clients(config restclient.Config, tokenRetriever TokenRetriever, namespace,
78 78
 	}
79 79
 	config.BearerToken = token
80 80
 
81
+	config.UserAgent = openshiftUserAgent
81 82
 	c, err := client.New(&config)
82 83
 	if err != nil {
83 84
 		return nil, nil, nil, err
84 85
 	}
85 86
 
87
+	config.UserAgent = kubeUserAgent
86 88
 	kc, err := kclient.New(&config)
87 89
 	if err != nil {
88 90
 		return nil, nil, nil, err
89 91
deleted file mode 100644
... ...
@@ -1,104 +0,0 @@
1
-package empty_dir
2
-
3
-import (
4
-	"k8s.io/kubernetes/pkg/api"
5
-	"k8s.io/kubernetes/pkg/api/resource"
6
-	"k8s.io/kubernetes/pkg/types"
7
-	"k8s.io/kubernetes/pkg/volume"
8
-)
9
-
10
-var _ volume.VolumePlugin = &EmptyDirQuotaPlugin{}
11
-var _ volume.Builder = &emptyDirQuotaBuilder{}
12
-
13
-// EmptyDirQuotaPlugin is a simple wrapper for the k8s empty dir plugin builder.
14
-type EmptyDirQuotaPlugin struct {
15
-	// wrapped is the actual k8s emptyDir volume plugin we will pass method calls to.
16
-	Wrapped volume.VolumePlugin
17
-
18
-	// The default quota to apply to each node:
19
-	Quota resource.Quantity
20
-
21
-	// QuotaApplicator is passed to actual volume builders so they can apply
22
-	// quota for the supported filesystem.
23
-	QuotaApplicator QuotaApplicator
24
-}
25
-
26
-func (plugin *EmptyDirQuotaPlugin) NewBuilder(spec *volume.Spec, pod *api.Pod, opts volume.VolumeOptions) (volume.Builder, error) {
27
-	volBuilder, err := plugin.Wrapped.NewBuilder(spec, pod, opts)
28
-	if err != nil {
29
-		return volBuilder, err
30
-	}
31
-
32
-	// Because we cannot access several fields on the k8s emptyDir struct, and
33
-	// we do not wish to modify k8s code for this, we have to grab a reference
34
-	// to them ourselves.
35
-	// This logic is the same as k8s.io/kubernetes/pkg/volume/empty_dir:
36
-	medium := api.StorageMediumDefault
37
-	if spec.Volume.EmptyDir != nil { // Support a non-specified source as EmptyDir.
38
-		medium = spec.Volume.EmptyDir.Medium
39
-	}
40
-
41
-	// Wrap the builder object with our own to add quota functionality:
42
-	wrapperEmptyDir := &emptyDirQuotaBuilder{
43
-		wrapped:         volBuilder,
44
-		pod:             pod,
45
-		medium:          medium,
46
-		quota:           plugin.Quota,
47
-		quotaApplicator: plugin.QuotaApplicator,
48
-	}
49
-	return wrapperEmptyDir, err
50
-}
51
-
52
-func (plugin *EmptyDirQuotaPlugin) Init(host volume.VolumeHost) error {
53
-	return plugin.Wrapped.Init(host)
54
-}
55
-
56
-func (plugin *EmptyDirQuotaPlugin) Name() string {
57
-	return plugin.Wrapped.Name()
58
-}
59
-
60
-func (plugin *EmptyDirQuotaPlugin) CanSupport(spec *volume.Spec) bool {
61
-	return plugin.Wrapped.CanSupport(spec)
62
-}
63
-
64
-func (plugin *EmptyDirQuotaPlugin) NewCleaner(volName string, podUID types.UID) (volume.Cleaner, error) {
65
-	return plugin.Wrapped.NewCleaner(volName, podUID)
66
-}
67
-
68
-// emptyDirQuotaBuilder is a wrapper plugin builder for the k8s empty dir builder itself.
69
-// This plugin just extends and adds the functionality to apply a
70
-// quota for the pods FSGroup on an XFS filesystem.
71
-type emptyDirQuotaBuilder struct {
72
-	wrapped         volume.Builder
73
-	pod             *api.Pod
74
-	medium          api.StorageMedium
75
-	quota           resource.Quantity
76
-	quotaApplicator QuotaApplicator
77
-}
78
-
79
-// Must implement SetUp as well, otherwise the internal Builder.SetUp calls it's
80
-// own SetUpAt method, not the one we need.
81
-
82
-func (edq *emptyDirQuotaBuilder) SetUp(fsGroup *int64) error {
83
-	return edq.SetUpAt(edq.GetPath(), fsGroup)
84
-}
85
-
86
-func (edq *emptyDirQuotaBuilder) SetUpAt(dir string, fsGroup *int64) error {
87
-	err := edq.wrapped.SetUpAt(dir, fsGroup)
88
-	if err == nil {
89
-		err = edq.quotaApplicator.Apply(dir, edq.medium, edq.pod, fsGroup, edq.quota)
90
-	}
91
-	return err
92
-}
93
-
94
-func (edq *emptyDirQuotaBuilder) GetAttributes() volume.Attributes {
95
-	return edq.wrapped.GetAttributes()
96
-}
97
-
98
-func (edq *emptyDirQuotaBuilder) GetMetrics() (*volume.Metrics, error) {
99
-	return edq.wrapped.GetMetrics()
100
-}
101
-
102
-func (edq *emptyDirQuotaBuilder) GetPath() string {
103
-	return edq.wrapped.GetPath()
104
-}
105 1
deleted file mode 100644
... ...
@@ -1,192 +0,0 @@
1
-package empty_dir
2
-
3
-import (
4
-	"bytes"
5
-	"fmt"
6
-	"os/exec"
7
-	"strings"
8
-
9
-	"github.com/golang/glog"
10
-	"k8s.io/kubernetes/pkg/api"
11
-	"k8s.io/kubernetes/pkg/api/resource"
12
-)
13
-
14
-// QuotaApplicator is used to apply quota to an emptyDir volume.
15
-type QuotaApplicator interface {
16
-	// Apply the quota to the given EmptyDir path:
17
-	Apply(dir string, medium api.StorageMedium, pod *api.Pod, fsGroup *int64, quota resource.Quantity) error
18
-}
19
-
20
-type xfsQuotaApplicator struct {
21
-	cmdRunner quotaCommandRunner
22
-}
23
-
24
-// NewQuotaApplicator checks the filesystem type for the configured volume directory
25
-// and returns an appropriate implementation of the quota applicator. If the filesystem
26
-// does not appear to be a type we support quotas on, an error is returned.
27
-func NewQuotaApplicator(volumeDirectory string) (QuotaApplicator, error) {
28
-
29
-	cmdRunner := &realQuotaCommandRunner{}
30
-	isXFS, err := isXFS(cmdRunner, volumeDirectory)
31
-	if err != nil {
32
-		return nil, err
33
-	}
34
-	if isXFS {
35
-		// Make sure xfs_quota is on the PATH, otherwise we're not going to get very far:
36
-		_, pathErr := exec.LookPath("xfs_quota")
37
-		if pathErr != nil {
38
-			return nil, pathErr
39
-		}
40
-
41
-		return &xfsQuotaApplicator{
42
-			cmdRunner: cmdRunner,
43
-		}, nil
44
-	}
45
-
46
-	// If we were unable to find a quota supported filesystem type, return an error:
47
-	return nil, fmt.Errorf("%s is not on a supported filesystem for local volume quota", volumeDirectory)
48
-}
49
-
50
-// quotaCommandRunner interface is used to abstract the actual running of
51
-// commands so we can unit test more behavior.
52
-type quotaCommandRunner interface {
53
-	RunFSTypeCommand(dir string) (string, string, error)
54
-	RunFSDeviceCommand(dir string) (string, string, error)
55
-	RunApplyQuotaCommand(fsDevice string, quota resource.Quantity, fsGroup int64) (string, string, error)
56
-}
57
-
58
-type realQuotaCommandRunner struct {
59
-}
60
-
61
-func (cr *realQuotaCommandRunner) RunFSTypeCommand(dir string) (string, string, error) {
62
-	args := []string{"-f", "-c", "%T", dir}
63
-	outBytes, err := exec.Command("stat", args...).Output()
64
-	return string(outBytes), "", err
65
-}
66
-
67
-func (cr *realQuotaCommandRunner) RunFSDeviceCommand(dir string) (string, string, error) {
68
-	outBytes, err := exec.Command("df", "--output=source", dir).Output()
69
-	return string(outBytes), "", err
70
-}
71
-
72
-func (cr *realQuotaCommandRunner) RunApplyQuotaCommand(fsDevice string, quota resource.Quantity, fsGroup int64) (string, string, error) {
73
-	args := []string{"-x", "-c",
74
-		fmt.Sprintf("limit -g bsoft=%d bhard=%d %d", quota.Value(), quota.Value(), fsGroup),
75
-		fsDevice,
76
-	}
77
-
78
-	cmd := exec.Command("xfs_quota", args...)
79
-	var stderr bytes.Buffer
80
-	cmd.Stderr = &stderr
81
-
82
-	err := cmd.Run()
83
-	glog.V(5).Infof("Ran: xfs_quota %s", args)
84
-	return "", stderr.String(), err
85
-}
86
-
87
-// Apply sets the actual quota on a device for an emptyDir volume if possible. Will return an error
88
-// if anything goes wrong during the process. (not an XFS filesystem, etc) If the volume medium is set
89
-// to memory, or no FSGroup is provided (indicating the request matched an SCC set to RunAsAny), this
90
-// method will effectively no-op.
91
-func (xqa *xfsQuotaApplicator) Apply(dir string, medium api.StorageMedium, pod *api.Pod, fsGroup *int64, quota resource.Quantity) error {
92
-
93
-	if medium == api.StorageMediumMemory {
94
-		glog.V(5).Infof("Skipping quota application due to memory storage medium.")
95
-		return nil
96
-	}
97
-	isXFS, err := isXFS(xqa.cmdRunner, dir)
98
-	if err != nil {
99
-		return err
100
-	}
101
-	if !isXFS {
102
-		return fmt.Errorf("unable to apply quota: %s is not on an XFS filesystem", dir)
103
-	}
104
-	if fsGroup == nil {
105
-		// This indicates the operation matched an SCC with FSGroup strategy RunAsAny.
106
-		// Not an error condition.
107
-		glog.V(5).Infof("Unable to apply XFS quota, no FSGroup specified.")
108
-		return nil
109
-	}
110
-
111
-	volDevice, err := xqa.getFSDevice(dir)
112
-	if err != nil {
113
-		return err
114
-	}
115
-
116
-	err = xqa.applyQuota(volDevice, quota, *fsGroup)
117
-	if err != nil {
118
-		return err
119
-	}
120
-
121
-	return nil
122
-}
123
-
124
-func (xqa *xfsQuotaApplicator) applyQuota(volDevice string, quota resource.Quantity, fsGroupID int64) error {
125
-	_, stderr, err := xqa.cmdRunner.RunApplyQuotaCommand(volDevice, quota, fsGroupID)
126
-	if err != nil {
127
-		return err
128
-	}
129
-	// xfs_quota is very happy to fail but return a success code, likely due to its
130
-	// interactive shell approach. Grab stderr, if we see anything written to it we'll
131
-	// consider this an error.
132
-	if len(stderr) > 0 {
133
-		return fmt.Errorf("xfs_quota wrote to stderr: %s", stderr)
134
-	}
135
-
136
-	glog.V(4).Infof("XFS quota applied: device=%s, quota=%d, fsGroup=%d", volDevice, quota.Value(), fsGroupID)
137
-	return nil
138
-}
139
-
140
-func (xqa *xfsQuotaApplicator) getFSDevice(dir string) (string, error) {
141
-	return getFSDevice(dir, xqa.cmdRunner)
142
-}
143
-
144
-// GetFSDevice returns the filesystem device for a given path. To do this we
145
-// run df on the path, returning a header line and the line we're
146
-// interested in. The first string token in that line will be the device name.
147
-func GetFSDevice(dir string) (string, error) {
148
-	return getFSDevice(dir, &realQuotaCommandRunner{})
149
-}
150
-
151
-func getFSDevice(dir string, cmdRunner quotaCommandRunner) (string, error) {
152
-	out, _, err := cmdRunner.RunFSDeviceCommand(dir)
153
-	if err != nil {
154
-		return "", fmt.Errorf("unable to find filesystem device for emptyDir volume %s: %s", dir, err)
155
-	}
156
-	fsDevice, parseErr := parseFSDevice(out)
157
-	return fsDevice, parseErr
158
-}
159
-
160
-func parseFSDevice(dfOutput string) (string, error) {
161
-	// Need to skip the df header line starting with "Filesystem", and grab the first
162
-	// word of the following line which will be our device path.
163
-	lines := strings.Split(dfOutput, "\n")
164
-	if len(lines) < 2 {
165
-		return "", fmt.Errorf("%s: %s", unexpectedLineCountError, dfOutput)
166
-	}
167
-
168
-	fsDevice := strings.Split(lines[1], " ")[0]
169
-	// Make sure it looks like a device:
170
-	if !strings.HasPrefix(fsDevice, "/") {
171
-		return "", fmt.Errorf("%s: %s", invalidFilesystemError, fsDevice)
172
-	}
173
-
174
-	return fsDevice, nil
175
-}
176
-
177
-// isXFS checks if the empty dir is on an XFS filesystem.
178
-func isXFS(cmdRunner quotaCommandRunner, dir string) (bool, error) {
179
-	out, _, err := cmdRunner.RunFSTypeCommand(dir)
180
-	if err != nil {
181
-		return false, fmt.Errorf("unable to check filesystem type for emptydir volume %s: %s", dir, err)
182
-	}
183
-	if strings.TrimSpace(out) == "xfs" {
184
-		return true, nil
185
-	}
186
-	return false, nil
187
-}
188
-
189
-const (
190
-	invalidFilesystemError   = "found invalid filesystem device"
191
-	unexpectedLineCountError = "unexpected line count in df output"
192
-)
193 1
deleted file mode 100644
... ...
@@ -1,249 +0,0 @@
1
-package empty_dir
2
-
3
-import (
4
-	"errors"
5
-	"strings"
6
-	"testing"
7
-
8
-	kapi "k8s.io/kubernetes/pkg/api"
9
-	"k8s.io/kubernetes/pkg/api/resource"
10
-)
11
-
12
-const expectedDevice = "/dev/sdb2"
13
-
14
-func TestParseFSDevice(t *testing.T) {
15
-	tests := map[string]struct {
16
-		dfOutput  string
17
-		expDevice string
18
-		expError  string
19
-	}{
20
-		"happy path": {
21
-			dfOutput:  "Filesystem\n/dev/sdb2",
22
-			expDevice: expectedDevice,
23
-		},
24
-		"happy path multi-token": {
25
-			dfOutput:  "Filesystem\n/dev/sdb2           16444592     8  16444584   1% /var/openshift.local.volumes/",
26
-			expDevice: expectedDevice,
27
-		},
28
-		"invalid tmpfs": {
29
-			dfOutput: "Filesystem\ntmpfs",
30
-			expError: invalidFilesystemError,
31
-		},
32
-		"invalid empty": {
33
-			dfOutput: "",
34
-			expError: unexpectedLineCountError,
35
-		},
36
-		"invalid one line": {
37
-			dfOutput: "Filesystem\n",
38
-			expError: invalidFilesystemError,
39
-		},
40
-		"invalid blank second line": {
41
-			dfOutput: "Filesystem\n\n",
42
-			expError: invalidFilesystemError,
43
-		},
44
-		"invalid too many lines": {
45
-			dfOutput:  "Filesystem\n/dev/sdb2\ntmpfs\nwhatisgoingon",
46
-			expDevice: expectedDevice,
47
-		},
48
-	}
49
-	for name, test := range tests {
50
-		t.Logf("running TestParseFSDevice: %s", name)
51
-		device, err := parseFSDevice(test.dfOutput)
52
-		if test.expDevice != "" && test.expDevice != device {
53
-			t.Errorf("Unexpected filesystem device, expected: %s, got: %s", test.expDevice, device)
54
-		}
55
-		if test.expError != "" && (err == nil || !strings.Contains(err.Error(), test.expError)) {
56
-			t.Errorf("Unexpected filesystem error, expected: %s, got: %s", test.expError, err)
57
-		}
58
-	}
59
-}
60
-
61
-// Avoid running actual commands to manage XFS quota:
62
-type mockQuotaCommandRunner struct {
63
-	RunFSDeviceCommandResponse *cmdResponse
64
-	RunFSTypeCommandResponse   *cmdResponse
65
-
66
-	RanApplyQuotaFSDevice string
67
-	RanApplyQuota         *resource.Quantity
68
-	RanApplyQuotaFSGroup  int64
69
-}
70
-
71
-func (m *mockQuotaCommandRunner) RunFSTypeCommand(dir string) (string, string, error) {
72
-	if m.RunFSTypeCommandResponse != nil {
73
-		return m.RunFSTypeCommandResponse.Stdout, m.RunFSTypeCommandResponse.Stderr, m.RunFSTypeCommandResponse.Error
74
-	}
75
-	return "xfs", "", nil
76
-}
77
-
78
-func (m *mockQuotaCommandRunner) RunFSDeviceCommand(dir string) (string, string, error) {
79
-	if m.RunFSDeviceCommandResponse != nil {
80
-		return m.RunFSDeviceCommandResponse.Stdout, m.RunFSDeviceCommandResponse.Stderr, m.RunFSDeviceCommandResponse.Error
81
-	}
82
-	return "Filesystem\n/dev/sdb2", "", nil
83
-}
84
-
85
-func (m *mockQuotaCommandRunner) RunApplyQuotaCommand(fsDevice string, quota resource.Quantity, fsGroup int64) (string, string, error) {
86
-	// Store these for assertions in tests:
87
-	m.RanApplyQuotaFSDevice = fsDevice
88
-	m.RanApplyQuota = &quota
89
-	m.RanApplyQuotaFSGroup = fsGroup
90
-	return "", "", nil
91
-}
92
-
93
-// Small struct for specifying how we want the various quota command runners to
94
-// respond in tests:
95
-type cmdResponse struct {
96
-	Stdout string
97
-	Stderr string
98
-	Error  error
99
-}
100
-
101
-func TestApplyQuota(t *testing.T) {
102
-
103
-	var defaultFSGroup int64
104
-	defaultFSGroup = 1000050000
105
-
106
-	tests := map[string]struct {
107
-		FSGroupID *int64
108
-		Quota     string
109
-
110
-		FSTypeCmdResponse     *cmdResponse
111
-		FSDeviceCmdResponse   *cmdResponse
112
-		ApplyQuotaCmdResponse *cmdResponse
113
-
114
-		ExpFSDevice string
115
-		ExpError    string // sub-string to be searched for in error message
116
-		ExpSkipped  bool
117
-	}{
118
-		"happy path": {
119
-			Quota:     "512",
120
-			FSGroupID: &defaultFSGroup,
121
-		},
122
-		"zero quota": {
123
-			Quota:     "0",
124
-			FSGroupID: &defaultFSGroup,
125
-		},
126
-		"invalid filesystem device": {
127
-			Quota:     "512",
128
-			FSGroupID: &defaultFSGroup,
129
-			FSDeviceCmdResponse: &cmdResponse{
130
-				Stdout: "Filesystem\ntmpfs",
131
-				Stderr: "",
132
-				Error:  nil,
133
-			},
134
-			ExpError:   invalidFilesystemError,
135
-			ExpSkipped: true,
136
-		},
137
-		"error checking filesystem device": {
138
-			Quota:     "512",
139
-			FSGroupID: &defaultFSGroup,
140
-			FSDeviceCmdResponse: &cmdResponse{
141
-				Stdout: "",
142
-				Stderr: "no such file or directory",
143
-				Error:  errors.New("no such file or directory"), // Would be exit error in real life
144
-			},
145
-			ExpError:   "no such file or directory",
146
-			ExpSkipped: true,
147
-		},
148
-		"non-xfs filesystem type": {
149
-			Quota:     "512",
150
-			FSGroupID: &defaultFSGroup,
151
-			FSTypeCmdResponse: &cmdResponse{
152
-				Stdout: "ext4",
153
-				Stderr: "",
154
-				Error:  nil,
155
-			},
156
-			ExpError:   "not on an XFS filesystem",
157
-			ExpSkipped: true,
158
-		},
159
-		"error checking filesystem type": {
160
-			Quota:     "512",
161
-			FSGroupID: &defaultFSGroup,
162
-			FSTypeCmdResponse: &cmdResponse{
163
-				Stdout: "",
164
-				Stderr: "no such file or directory",
165
-				Error:  errors.New("no such file or directory"), // Would be exit error in real life
166
-			},
167
-			ExpError:   "unable to check filesystem type",
168
-			ExpSkipped: true,
169
-		},
170
-		// Should result in success, but no quota actually gets applied:
171
-		"no FSGroup": {
172
-			Quota:      "512",
173
-			ExpSkipped: true,
174
-		},
175
-	}
176
-
177
-	for name, test := range tests {
178
-		t.Logf("running TestApplyQuota: %s", name)
179
-		quotaApplicator := xfsQuotaApplicator{}
180
-		// Replace the real command runner with our mock:
181
-		mockCmdRunner := mockQuotaCommandRunner{}
182
-		quotaApplicator.cmdRunner = &mockCmdRunner
183
-		fakeDir := "/var/lib/origin/openshift.local.volumes/pods/d71f6949-cb3f-11e5-aedf-989096de63cb"
184
-
185
-		// Configure the default happy path command responses if nothing was specified
186
-		// by the test:
187
-		if test.FSTypeCmdResponse == nil {
188
-			// Configure the default happy path response:
189
-			test.FSTypeCmdResponse = &cmdResponse{
190
-				Stdout: "xfs",
191
-				Stderr: "",
192
-				Error:  nil,
193
-			}
194
-		}
195
-		if test.FSDeviceCmdResponse == nil {
196
-			test.FSDeviceCmdResponse = &cmdResponse{
197
-				Stdout: "Filesystem\n/dev/sdb2",
198
-				Stderr: "",
199
-				Error:  nil,
200
-			}
201
-		}
202
-
203
-		if test.ApplyQuotaCmdResponse == nil {
204
-			test.ApplyQuotaCmdResponse = &cmdResponse{
205
-				Stdout: "",
206
-				Stderr: "",
207
-				Error:  nil,
208
-			}
209
-		}
210
-
211
-		mockCmdRunner.RunFSDeviceCommandResponse = test.FSDeviceCmdResponse
212
-		mockCmdRunner.RunFSTypeCommandResponse = test.FSTypeCmdResponse
213
-
214
-		quota := resource.MustParse(test.Quota)
215
-		err := quotaApplicator.Apply(fakeDir, kapi.StorageMediumDefault, &kapi.Pod{}, test.FSGroupID, quota)
216
-		if test.ExpError == "" && !test.ExpSkipped {
217
-			// Expecting success case:
218
-			if mockCmdRunner.RanApplyQuotaFSDevice != "/dev/sdb2" {
219
-				t.Errorf("failed: '%s', expected quota applied to: %s, got: %s", name, "/dev/sdb2", mockCmdRunner.RanApplyQuotaFSDevice)
220
-			}
221
-			if mockCmdRunner.RanApplyQuota.Value() != quota.Value() {
222
-				t.Errorf("failed: '%s', expected quota: %d, got: %d", name, quota.Value(),
223
-					mockCmdRunner.RanApplyQuota.Value())
224
-			}
225
-			if mockCmdRunner.RanApplyQuotaFSGroup != *test.FSGroupID {
226
-				t.Errorf("failed: '%s', expected FSGroup: %d, got: %d", name, test.FSGroupID, mockCmdRunner.RanApplyQuotaFSGroup)
227
-			}
228
-		} else if test.ExpError != "" {
229
-			// Expecting error case:
230
-			if err == nil {
231
-				t.Errorf("failed: '%s', expected error but got none", name)
232
-			} else if !strings.Contains(err.Error(), test.ExpError) {
233
-				t.Errorf("failed: '%s', expected error containing '%s', got: '%s'", name, test.ExpError, err)
234
-			}
235
-		}
236
-
237
-		if test.ExpSkipped {
238
-			if mockCmdRunner.RanApplyQuota != nil {
239
-				t.Errorf("failed: '%s', expected error but quota was applied", name)
240
-			}
241
-			if mockCmdRunner.RanApplyQuotaFSGroup != 0 {
242
-				t.Errorf("failed: '%s', expected error but quota was applied", name)
243
-			}
244
-			if mockCmdRunner.RanApplyQuotaFSDevice != "" {
245
-				t.Errorf("failed: '%s', expected error but quota was applied", name)
246
-			}
247
-		}
248
-	}
249
-}
250 1
new file mode 100644
... ...
@@ -0,0 +1,104 @@
0
+package emptydir
1
+
2
+import (
3
+	"k8s.io/kubernetes/pkg/api"
4
+	"k8s.io/kubernetes/pkg/api/resource"
5
+	"k8s.io/kubernetes/pkg/types"
6
+	"k8s.io/kubernetes/pkg/volume"
7
+)
8
+
9
+var _ volume.VolumePlugin = &EmptyDirQuotaPlugin{}
10
+var _ volume.Builder = &emptyDirQuotaBuilder{}
11
+
12
+// EmptyDirQuotaPlugin is a simple wrapper for the k8s empty dir plugin builder.
13
+type EmptyDirQuotaPlugin struct {
14
+	// wrapped is the actual k8s emptyDir volume plugin we will pass method calls to.
15
+	Wrapped volume.VolumePlugin
16
+
17
+	// The default quota to apply to each node:
18
+	Quota resource.Quantity
19
+
20
+	// QuotaApplicator is passed to actual volume builders so they can apply
21
+	// quota for the supported filesystem.
22
+	QuotaApplicator QuotaApplicator
23
+}
24
+
25
+func (plugin *EmptyDirQuotaPlugin) NewBuilder(spec *volume.Spec, pod *api.Pod, opts volume.VolumeOptions) (volume.Builder, error) {
26
+	volBuilder, err := plugin.Wrapped.NewBuilder(spec, pod, opts)
27
+	if err != nil {
28
+		return volBuilder, err
29
+	}
30
+
31
+	// Because we cannot access several fields on the k8s emptyDir struct, and
32
+	// we do not wish to modify k8s code for this, we have to grab a reference
33
+	// to them ourselves.
34
+	// This logic is the same as k8s.io/kubernetes/pkg/volume/empty_dir:
35
+	medium := api.StorageMediumDefault
36
+	if spec.Volume.EmptyDir != nil { // Support a non-specified source as EmptyDir.
37
+		medium = spec.Volume.EmptyDir.Medium
38
+	}
39
+
40
+	// Wrap the builder object with our own to add quota functionality:
41
+	wrapperEmptyDir := &emptyDirQuotaBuilder{
42
+		wrapped:         volBuilder,
43
+		pod:             pod,
44
+		medium:          medium,
45
+		quota:           plugin.Quota,
46
+		quotaApplicator: plugin.QuotaApplicator,
47
+	}
48
+	return wrapperEmptyDir, err
49
+}
50
+
51
+func (plugin *EmptyDirQuotaPlugin) Init(host volume.VolumeHost) error {
52
+	return plugin.Wrapped.Init(host)
53
+}
54
+
55
+func (plugin *EmptyDirQuotaPlugin) Name() string {
56
+	return plugin.Wrapped.Name()
57
+}
58
+
59
+func (plugin *EmptyDirQuotaPlugin) CanSupport(spec *volume.Spec) bool {
60
+	return plugin.Wrapped.CanSupport(spec)
61
+}
62
+
63
+func (plugin *EmptyDirQuotaPlugin) NewCleaner(volName string, podUID types.UID) (volume.Cleaner, error) {
64
+	return plugin.Wrapped.NewCleaner(volName, podUID)
65
+}
66
+
67
+// emptyDirQuotaBuilder is a wrapper plugin builder for the k8s empty dir builder itself.
68
+// This plugin just extends and adds the functionality to apply a
69
+// quota for the pods FSGroup on an XFS filesystem.
70
+type emptyDirQuotaBuilder struct {
71
+	wrapped         volume.Builder
72
+	pod             *api.Pod
73
+	medium          api.StorageMedium
74
+	quota           resource.Quantity
75
+	quotaApplicator QuotaApplicator
76
+}
77
+
78
+// Must implement SetUp as well, otherwise the internal Builder.SetUp calls it's
79
+// own SetUpAt method, not the one we need.
80
+
81
+func (edq *emptyDirQuotaBuilder) SetUp(fsGroup *int64) error {
82
+	return edq.SetUpAt(edq.GetPath(), fsGroup)
83
+}
84
+
85
+func (edq *emptyDirQuotaBuilder) SetUpAt(dir string, fsGroup *int64) error {
86
+	err := edq.wrapped.SetUpAt(dir, fsGroup)
87
+	if err == nil {
88
+		err = edq.quotaApplicator.Apply(dir, edq.medium, edq.pod, fsGroup, edq.quota)
89
+	}
90
+	return err
91
+}
92
+
93
+func (edq *emptyDirQuotaBuilder) GetAttributes() volume.Attributes {
94
+	return edq.wrapped.GetAttributes()
95
+}
96
+
97
+func (edq *emptyDirQuotaBuilder) GetMetrics() (*volume.Metrics, error) {
98
+	return edq.wrapped.GetMetrics()
99
+}
100
+
101
+func (edq *emptyDirQuotaBuilder) GetPath() string {
102
+	return edq.wrapped.GetPath()
103
+}
0 104
new file mode 100644
... ...
@@ -0,0 +1,192 @@
0
+package emptydir
1
+
2
+import (
3
+	"bytes"
4
+	"fmt"
5
+	"os/exec"
6
+	"strings"
7
+
8
+	"github.com/golang/glog"
9
+	"k8s.io/kubernetes/pkg/api"
10
+	"k8s.io/kubernetes/pkg/api/resource"
11
+)
12
+
13
+// QuotaApplicator is used to apply quota to an emptyDir volume.
14
+type QuotaApplicator interface {
15
+	// Apply the quota to the given EmptyDir path:
16
+	Apply(dir string, medium api.StorageMedium, pod *api.Pod, fsGroup *int64, quota resource.Quantity) error
17
+}
18
+
19
+type xfsQuotaApplicator struct {
20
+	cmdRunner quotaCommandRunner
21
+}
22
+
23
+// NewQuotaApplicator checks the filesystem type for the configured volume directory
24
+// and returns an appropriate implementation of the quota applicator. If the filesystem
25
+// does not appear to be a type we support quotas on, an error is returned.
26
+func NewQuotaApplicator(volumeDirectory string) (QuotaApplicator, error) {
27
+
28
+	cmdRunner := &realQuotaCommandRunner{}
29
+	isXFS, err := isXFS(cmdRunner, volumeDirectory)
30
+	if err != nil {
31
+		return nil, err
32
+	}
33
+	if isXFS {
34
+		// Make sure xfs_quota is on the PATH, otherwise we're not going to get very far:
35
+		_, pathErr := exec.LookPath("xfs_quota")
36
+		if pathErr != nil {
37
+			return nil, pathErr
38
+		}
39
+
40
+		return &xfsQuotaApplicator{
41
+			cmdRunner: cmdRunner,
42
+		}, nil
43
+	}
44
+
45
+	// If we were unable to find a quota supported filesystem type, return an error:
46
+	return nil, fmt.Errorf("%s is not on a supported filesystem for local volume quota", volumeDirectory)
47
+}
48
+
49
+// quotaCommandRunner interface is used to abstract the actual running of
50
+// commands so we can unit test more behavior.
51
+type quotaCommandRunner interface {
52
+	RunFSTypeCommand(dir string) (string, string, error)
53
+	RunFSDeviceCommand(dir string) (string, string, error)
54
+	RunApplyQuotaCommand(fsDevice string, quota resource.Quantity, fsGroup int64) (string, string, error)
55
+}
56
+
57
+type realQuotaCommandRunner struct {
58
+}
59
+
60
+func (cr *realQuotaCommandRunner) RunFSTypeCommand(dir string) (string, string, error) {
61
+	args := []string{"-f", "-c", "%T", dir}
62
+	outBytes, err := exec.Command("stat", args...).Output()
63
+	return string(outBytes), "", err
64
+}
65
+
66
+func (cr *realQuotaCommandRunner) RunFSDeviceCommand(dir string) (string, string, error) {
67
+	outBytes, err := exec.Command("df", "--output=source", dir).Output()
68
+	return string(outBytes), "", err
69
+}
70
+
71
+func (cr *realQuotaCommandRunner) RunApplyQuotaCommand(fsDevice string, quota resource.Quantity, fsGroup int64) (string, string, error) {
72
+	args := []string{"-x", "-c",
73
+		fmt.Sprintf("limit -g bsoft=%d bhard=%d %d", quota.Value(), quota.Value(), fsGroup),
74
+		fsDevice,
75
+	}
76
+
77
+	cmd := exec.Command("xfs_quota", args...)
78
+	var stderr bytes.Buffer
79
+	cmd.Stderr = &stderr
80
+
81
+	err := cmd.Run()
82
+	glog.V(5).Infof("Ran: xfs_quota %s", args)
83
+	return "", stderr.String(), err
84
+}
85
+
86
+// Apply sets the actual quota on a device for an emptyDir volume if possible. Will return an error
87
+// if anything goes wrong during the process. (not an XFS filesystem, etc) If the volume medium is set
88
+// to memory, or no FSGroup is provided (indicating the request matched an SCC set to RunAsAny), this
89
+// method will effectively no-op.
90
+func (xqa *xfsQuotaApplicator) Apply(dir string, medium api.StorageMedium, pod *api.Pod, fsGroup *int64, quota resource.Quantity) error {
91
+
92
+	if medium == api.StorageMediumMemory {
93
+		glog.V(5).Infof("Skipping quota application due to memory storage medium.")
94
+		return nil
95
+	}
96
+	isXFS, err := isXFS(xqa.cmdRunner, dir)
97
+	if err != nil {
98
+		return err
99
+	}
100
+	if !isXFS {
101
+		return fmt.Errorf("unable to apply quota: %s is not on an XFS filesystem", dir)
102
+	}
103
+	if fsGroup == nil {
104
+		// This indicates the operation matched an SCC with FSGroup strategy RunAsAny.
105
+		// Not an error condition.
106
+		glog.V(5).Infof("Unable to apply XFS quota, no FSGroup specified.")
107
+		return nil
108
+	}
109
+
110
+	volDevice, err := xqa.getFSDevice(dir)
111
+	if err != nil {
112
+		return err
113
+	}
114
+
115
+	err = xqa.applyQuota(volDevice, quota, *fsGroup)
116
+	if err != nil {
117
+		return err
118
+	}
119
+
120
+	return nil
121
+}
122
+
123
+func (xqa *xfsQuotaApplicator) applyQuota(volDevice string, quota resource.Quantity, fsGroupID int64) error {
124
+	_, stderr, err := xqa.cmdRunner.RunApplyQuotaCommand(volDevice, quota, fsGroupID)
125
+	if err != nil {
126
+		return err
127
+	}
128
+	// xfs_quota is very happy to fail but return a success code, likely due to its
129
+	// interactive shell approach. Grab stderr, if we see anything written to it we'll
130
+	// consider this an error.
131
+	if len(stderr) > 0 {
132
+		return fmt.Errorf("xfs_quota wrote to stderr: %s", stderr)
133
+	}
134
+
135
+	glog.V(4).Infof("XFS quota applied: device=%s, quota=%d, fsGroup=%d", volDevice, quota.Value(), fsGroupID)
136
+	return nil
137
+}
138
+
139
+func (xqa *xfsQuotaApplicator) getFSDevice(dir string) (string, error) {
140
+	return getFSDevice(dir, xqa.cmdRunner)
141
+}
142
+
143
+// GetFSDevice returns the filesystem device for a given path. To do this we
144
+// run df on the path, returning a header line and the line we're
145
+// interested in. The first string token in that line will be the device name.
146
+func GetFSDevice(dir string) (string, error) {
147
+	return getFSDevice(dir, &realQuotaCommandRunner{})
148
+}
149
+
150
+func getFSDevice(dir string, cmdRunner quotaCommandRunner) (string, error) {
151
+	out, _, err := cmdRunner.RunFSDeviceCommand(dir)
152
+	if err != nil {
153
+		return "", fmt.Errorf("unable to find filesystem device for emptyDir volume %s: %s", dir, err)
154
+	}
155
+	fsDevice, parseErr := parseFSDevice(out)
156
+	return fsDevice, parseErr
157
+}
158
+
159
+func parseFSDevice(dfOutput string) (string, error) {
160
+	// Need to skip the df header line starting with "Filesystem", and grab the first
161
+	// word of the following line which will be our device path.
162
+	lines := strings.Split(dfOutput, "\n")
163
+	if len(lines) < 2 {
164
+		return "", fmt.Errorf("%s: %s", unexpectedLineCountError, dfOutput)
165
+	}
166
+
167
+	fsDevice := strings.Split(lines[1], " ")[0]
168
+	// Make sure it looks like a device:
169
+	if !strings.HasPrefix(fsDevice, "/") {
170
+		return "", fmt.Errorf("%s: %s", invalidFilesystemError, fsDevice)
171
+	}
172
+
173
+	return fsDevice, nil
174
+}
175
+
176
+// isXFS checks if the empty dir is on an XFS filesystem.
177
+func isXFS(cmdRunner quotaCommandRunner, dir string) (bool, error) {
178
+	out, _, err := cmdRunner.RunFSTypeCommand(dir)
179
+	if err != nil {
180
+		return false, fmt.Errorf("unable to check filesystem type for emptydir volume %s: %s", dir, err)
181
+	}
182
+	if strings.TrimSpace(out) == "xfs" {
183
+		return true, nil
184
+	}
185
+	return false, nil
186
+}
187
+
188
+const (
189
+	invalidFilesystemError   = "found invalid filesystem device"
190
+	unexpectedLineCountError = "unexpected line count in df output"
191
+)
0 192
new file mode 100644
... ...
@@ -0,0 +1,249 @@
0
+package emptydir
1
+
2
+import (
3
+	"errors"
4
+	"strings"
5
+	"testing"
6
+
7
+	kapi "k8s.io/kubernetes/pkg/api"
8
+	"k8s.io/kubernetes/pkg/api/resource"
9
+)
10
+
11
+const expectedDevice = "/dev/sdb2"
12
+
13
+func TestParseFSDevice(t *testing.T) {
14
+	tests := map[string]struct {
15
+		dfOutput  string
16
+		expDevice string
17
+		expError  string
18
+	}{
19
+		"happy path": {
20
+			dfOutput:  "Filesystem\n/dev/sdb2",
21
+			expDevice: expectedDevice,
22
+		},
23
+		"happy path multi-token": {
24
+			dfOutput:  "Filesystem\n/dev/sdb2           16444592     8  16444584   1% /var/openshift.local.volumes/",
25
+			expDevice: expectedDevice,
26
+		},
27
+		"invalid tmpfs": {
28
+			dfOutput: "Filesystem\ntmpfs",
29
+			expError: invalidFilesystemError,
30
+		},
31
+		"invalid empty": {
32
+			dfOutput: "",
33
+			expError: unexpectedLineCountError,
34
+		},
35
+		"invalid one line": {
36
+			dfOutput: "Filesystem\n",
37
+			expError: invalidFilesystemError,
38
+		},
39
+		"invalid blank second line": {
40
+			dfOutput: "Filesystem\n\n",
41
+			expError: invalidFilesystemError,
42
+		},
43
+		"invalid too many lines": {
44
+			dfOutput:  "Filesystem\n/dev/sdb2\ntmpfs\nwhatisgoingon",
45
+			expDevice: expectedDevice,
46
+		},
47
+	}
48
+	for name, test := range tests {
49
+		t.Logf("running TestParseFSDevice: %s", name)
50
+		device, err := parseFSDevice(test.dfOutput)
51
+		if test.expDevice != "" && test.expDevice != device {
52
+			t.Errorf("Unexpected filesystem device, expected: %s, got: %s", test.expDevice, device)
53
+		}
54
+		if test.expError != "" && (err == nil || !strings.Contains(err.Error(), test.expError)) {
55
+			t.Errorf("Unexpected filesystem error, expected: %s, got: %s", test.expError, err)
56
+		}
57
+	}
58
+}
59
+
60
+// Avoid running actual commands to manage XFS quota:
61
+type mockQuotaCommandRunner struct {
62
+	RunFSDeviceCommandResponse *cmdResponse
63
+	RunFSTypeCommandResponse   *cmdResponse
64
+
65
+	RanApplyQuotaFSDevice string
66
+	RanApplyQuota         *resource.Quantity
67
+	RanApplyQuotaFSGroup  int64
68
+}
69
+
70
+func (m *mockQuotaCommandRunner) RunFSTypeCommand(dir string) (string, string, error) {
71
+	if m.RunFSTypeCommandResponse != nil {
72
+		return m.RunFSTypeCommandResponse.Stdout, m.RunFSTypeCommandResponse.Stderr, m.RunFSTypeCommandResponse.Error
73
+	}
74
+	return "xfs", "", nil
75
+}
76
+
77
+func (m *mockQuotaCommandRunner) RunFSDeviceCommand(dir string) (string, string, error) {
78
+	if m.RunFSDeviceCommandResponse != nil {
79
+		return m.RunFSDeviceCommandResponse.Stdout, m.RunFSDeviceCommandResponse.Stderr, m.RunFSDeviceCommandResponse.Error
80
+	}
81
+	return "Filesystem\n/dev/sdb2", "", nil
82
+}
83
+
84
+func (m *mockQuotaCommandRunner) RunApplyQuotaCommand(fsDevice string, quota resource.Quantity, fsGroup int64) (string, string, error) {
85
+	// Store these for assertions in tests:
86
+	m.RanApplyQuotaFSDevice = fsDevice
87
+	m.RanApplyQuota = &quota
88
+	m.RanApplyQuotaFSGroup = fsGroup
89
+	return "", "", nil
90
+}
91
+
92
+// Small struct for specifying how we want the various quota command runners to
93
+// respond in tests:
94
+type cmdResponse struct {
95
+	Stdout string
96
+	Stderr string
97
+	Error  error
98
+}
99
+
100
+func TestApplyQuota(t *testing.T) {
101
+
102
+	var defaultFSGroup int64
103
+	defaultFSGroup = 1000050000
104
+
105
+	tests := map[string]struct {
106
+		FSGroupID *int64
107
+		Quota     string
108
+
109
+		FSTypeCmdResponse     *cmdResponse
110
+		FSDeviceCmdResponse   *cmdResponse
111
+		ApplyQuotaCmdResponse *cmdResponse
112
+
113
+		ExpFSDevice string
114
+		ExpError    string // sub-string to be searched for in error message
115
+		ExpSkipped  bool
116
+	}{
117
+		"happy path": {
118
+			Quota:     "512",
119
+			FSGroupID: &defaultFSGroup,
120
+		},
121
+		"zero quota": {
122
+			Quota:     "0",
123
+			FSGroupID: &defaultFSGroup,
124
+		},
125
+		"invalid filesystem device": {
126
+			Quota:     "512",
127
+			FSGroupID: &defaultFSGroup,
128
+			FSDeviceCmdResponse: &cmdResponse{
129
+				Stdout: "Filesystem\ntmpfs",
130
+				Stderr: "",
131
+				Error:  nil,
132
+			},
133
+			ExpError:   invalidFilesystemError,
134
+			ExpSkipped: true,
135
+		},
136
+		"error checking filesystem device": {
137
+			Quota:     "512",
138
+			FSGroupID: &defaultFSGroup,
139
+			FSDeviceCmdResponse: &cmdResponse{
140
+				Stdout: "",
141
+				Stderr: "no such file or directory",
142
+				Error:  errors.New("no such file or directory"), // Would be exit error in real life
143
+			},
144
+			ExpError:   "no such file or directory",
145
+			ExpSkipped: true,
146
+		},
147
+		"non-xfs filesystem type": {
148
+			Quota:     "512",
149
+			FSGroupID: &defaultFSGroup,
150
+			FSTypeCmdResponse: &cmdResponse{
151
+				Stdout: "ext4",
152
+				Stderr: "",
153
+				Error:  nil,
154
+			},
155
+			ExpError:   "not on an XFS filesystem",
156
+			ExpSkipped: true,
157
+		},
158
+		"error checking filesystem type": {
159
+			Quota:     "512",
160
+			FSGroupID: &defaultFSGroup,
161
+			FSTypeCmdResponse: &cmdResponse{
162
+				Stdout: "",
163
+				Stderr: "no such file or directory",
164
+				Error:  errors.New("no such file or directory"), // Would be exit error in real life
165
+			},
166
+			ExpError:   "unable to check filesystem type",
167
+			ExpSkipped: true,
168
+		},
169
+		// Should result in success, but no quota actually gets applied:
170
+		"no FSGroup": {
171
+			Quota:      "512",
172
+			ExpSkipped: true,
173
+		},
174
+	}
175
+
176
+	for name, test := range tests {
177
+		t.Logf("running TestApplyQuota: %s", name)
178
+		quotaApplicator := xfsQuotaApplicator{}
179
+		// Replace the real command runner with our mock:
180
+		mockCmdRunner := mockQuotaCommandRunner{}
181
+		quotaApplicator.cmdRunner = &mockCmdRunner
182
+		fakeDir := "/var/lib/origin/openshift.local.volumes/pods/d71f6949-cb3f-11e5-aedf-989096de63cb"
183
+
184
+		// Configure the default happy path command responses if nothing was specified
185
+		// by the test:
186
+		if test.FSTypeCmdResponse == nil {
187
+			// Configure the default happy path response:
188
+			test.FSTypeCmdResponse = &cmdResponse{
189
+				Stdout: "xfs",
190
+				Stderr: "",
191
+				Error:  nil,
192
+			}
193
+		}
194
+		if test.FSDeviceCmdResponse == nil {
195
+			test.FSDeviceCmdResponse = &cmdResponse{
196
+				Stdout: "Filesystem\n/dev/sdb2",
197
+				Stderr: "",
198
+				Error:  nil,
199
+			}
200
+		}
201
+
202
+		if test.ApplyQuotaCmdResponse == nil {
203
+			test.ApplyQuotaCmdResponse = &cmdResponse{
204
+				Stdout: "",
205
+				Stderr: "",
206
+				Error:  nil,
207
+			}
208
+		}
209
+
210
+		mockCmdRunner.RunFSDeviceCommandResponse = test.FSDeviceCmdResponse
211
+		mockCmdRunner.RunFSTypeCommandResponse = test.FSTypeCmdResponse
212
+
213
+		quota := resource.MustParse(test.Quota)
214
+		err := quotaApplicator.Apply(fakeDir, kapi.StorageMediumDefault, &kapi.Pod{}, test.FSGroupID, quota)
215
+		if test.ExpError == "" && !test.ExpSkipped {
216
+			// Expecting success case:
217
+			if mockCmdRunner.RanApplyQuotaFSDevice != "/dev/sdb2" {
218
+				t.Errorf("failed: '%s', expected quota applied to: %s, got: %s", name, "/dev/sdb2", mockCmdRunner.RanApplyQuotaFSDevice)
219
+			}
220
+			if mockCmdRunner.RanApplyQuota.Value() != quota.Value() {
221
+				t.Errorf("failed: '%s', expected quota: %d, got: %d", name, quota.Value(),
222
+					mockCmdRunner.RanApplyQuota.Value())
223
+			}
224
+			if mockCmdRunner.RanApplyQuotaFSGroup != *test.FSGroupID {
225
+				t.Errorf("failed: '%s', expected FSGroup: %d, got: %d", name, test.FSGroupID, mockCmdRunner.RanApplyQuotaFSGroup)
226
+			}
227
+		} else if test.ExpError != "" {
228
+			// Expecting error case:
229
+			if err == nil {
230
+				t.Errorf("failed: '%s', expected error but got none", name)
231
+			} else if !strings.Contains(err.Error(), test.ExpError) {
232
+				t.Errorf("failed: '%s', expected error containing '%s', got: '%s'", name, test.ExpError, err)
233
+			}
234
+		}
235
+
236
+		if test.ExpSkipped {
237
+			if mockCmdRunner.RanApplyQuota != nil {
238
+				t.Errorf("failed: '%s', expected error but quota was applied", name)
239
+			}
240
+			if mockCmdRunner.RanApplyQuotaFSGroup != 0 {
241
+				t.Errorf("failed: '%s', expected error but quota was applied", name)
242
+			}
243
+			if mockCmdRunner.RanApplyQuotaFSDevice != "" {
244
+				t.Errorf("failed: '%s', expected error but quota was applied", name)
245
+			}
246
+		}
247
+	}
248
+}
... ...
@@ -116,6 +116,13 @@ echo "groups: ok"
116 116
 os::cmd::expect_success 'oadm policy who-can get pods'
117 117
 os::cmd::expect_success 'oadm policy who-can get pods -n default'
118 118
 os::cmd::expect_success 'oadm policy who-can get pods --all-namespaces'
119
+# check to make sure that the resource arg conforms to resource rules
120
+os::cmd::expect_success_and_text 'oadm policy who-can get Pod' "Resource:  pods"
121
+os::cmd::expect_success_and_text 'oadm policy who-can get PodASDF' "Resource:  PodASDF"
122
+os::cmd::expect_success_and_text 'oadm policy who-can get hpa.autoscaling -n default' "Resource:  horizontalpodautoscalers.autoscaling"
123
+os::cmd::expect_success_and_text 'oadm policy who-can get hpa.v1.autoscaling -n default' "Resource:  horizontalpodautoscalers.autoscaling"
124
+os::cmd::expect_success_and_text 'oadm policy who-can get hpa.extensions -n default' "Resource:  horizontalpodautoscalers.extensions"
125
+os::cmd::expect_success_and_text 'oadm policy who-can get hpa -n default' "Resource:  horizontalpodautoscalers.extensions"
119 126
 
120 127
 os::cmd::expect_success 'oadm policy add-role-to-group cluster-admin system:unauthenticated'
121 128
 os::cmd::expect_success 'oadm policy add-role-to-user cluster-admin system:no-user'
... ...
@@ -24,11 +24,15 @@ os::cmd::expect_success_and_text "oc debug dc/test-deployment-config --keep-anno
24 24
 os::cmd::expect_success_and_text "oc debug dc/test-deployment-config --as-root -o yaml" 'runAsUser: 0'
25 25
 os::cmd::expect_success_and_text "oc debug dc/test-deployment-config --keep-liveness --keep-readiness -o yaml" ''
26 26
 os::cmd::expect_success_and_text "oc debug dc/test-deployment-config -o yaml -- /bin/env" '\- /bin/env'
27
+os::cmd::expect_success_and_text "oc debug -t dc/test-deployment-config -o yaml" 'stdinOnce'
28
+os::cmd::expect_success_and_text "oc debug -t dc/test-deployment-config -o yaml" 'tty'
27 29
 os::cmd::expect_success_and_not_text "oc debug dc/test-deployment-config -o yaml -- /bin/env" 'stdin'
28 30
 os::cmd::expect_success_and_not_text "oc debug dc/test-deployment-config -o yaml -- /bin/env" 'tty'
29 31
 # Does not require a real resource on the server
32
+os::cmd::expect_success_and_not_text "oc debug -T -f examples/hello-openshift/hello-pod.json -o yaml" 'tty'
30 33
 os::cmd::expect_success_and_text "oc debug -f examples/hello-openshift/hello-pod.json --keep-liveness --keep-readiness -o yaml" ''
31 34
 os::cmd::expect_success_and_text "oc debug -f examples/hello-openshift/hello-pod.json -o yaml -- /bin/env" '\- /bin/env'
32 35
 os::cmd::expect_success_and_not_text "oc debug -f examples/hello-openshift/hello-pod.json -o yaml -- /bin/env" 'stdin'
33 36
 os::cmd::expect_success_and_not_text "oc debug -f examples/hello-openshift/hello-pod.json -o yaml -- /bin/env" 'tty'
37
+# TODO: write a test that emulates a TTY to verify the correct defaulting of what the pod is created
34 38
 echo "debug: ok"
... ...
@@ -49,19 +49,22 @@ os::cmd::expect_success "oc config view --raw > $new_kubeconfig"
49 49
 os::cmd::expect_success "oc login -u alternate-cluster-admin-user -p anything --config=${new_kubeconfig}"
50 50
 
51 51
 # alternate-cluster-admin should default to having star rights, so he should be able to update his role to that
52
-resourceversion=$(oc get  clusterrole/alternate-cluster-admin -o=jsonpath="{.metadata.resourceVersion}")
52
+os::cmd::try_until_text "oc policy who-can update clusterrroles" "alternate-cluster-admin-user"
53
+resourceversion=$(oc get clusterrole/alternate-cluster-admin -o=jsonpath="{.metadata.resourceVersion}")
53 54
 cp ${OS_ROOT}/test/fixtures/bootstrappolicy/alternate_cluster_admin.yaml ${workingdir}
54 55
 os::util::sed "s/RESOURCE_VERSION/${resourceversion}/g" ${workingdir}/alternate_cluster_admin.yaml
55 56
 os::cmd::expect_success "oc replace --config=${new_kubeconfig} clusterrole/alternate-cluster-admin -f ${workingdir}/alternate_cluster_admin.yaml"
56 57
 
57 58
 # alternate-cluster-admin can restrict himself to no groups
58
-resourceversion=$(oc get  clusterrole/alternate-cluster-admin -o=jsonpath="{.metadata.resourceVersion}")
59
+os::cmd::try_until_text "oc policy who-can update clusterrroles" "alternate-cluster-admin-user"
60
+resourceversion=$(oc get clusterrole/alternate-cluster-admin -o=jsonpath="{.metadata.resourceVersion}")
59 61
 cp ${OS_ROOT}/test/fixtures/bootstrappolicy/cluster_admin_without_apigroups.yaml ${workingdir}
60 62
 os::util::sed "s/RESOURCE_VERSION/${resourceversion}/g" ${workingdir}/cluster_admin_without_apigroups.yaml
61 63
 os::cmd::expect_success "oc replace --config=${new_kubeconfig} clusterrole/alternate-cluster-admin -f ${workingdir}/cluster_admin_without_apigroups.yaml"
62 64
 
63 65
 # alternate-cluster-admin should NOT have the power add back star now
64
-resourceversion=$(oc get  clusterrole/alternate-cluster-admin -o=jsonpath="{.metadata.resourceVersion}")
66
+os::cmd::try_until_failure "oc policy who-can update hpa.extensions | grep -q alternate-cluster-admin-user"
67
+resourceversion=$(oc get clusterrole/alternate-cluster-admin -o=jsonpath="{.metadata.resourceVersion}")
65 68
 cp ${OS_ROOT}/test/fixtures/bootstrappolicy/alternate_cluster_admin.yaml ${workingdir}
66 69
 os::util::sed "s/RESOURCE_VERSION/${resourceversion}/g" ${workingdir}/alternate_cluster_admin.yaml
67 70
 os::cmd::expect_failure_and_text "oc replace --config=${new_kubeconfig} clusterrole/alternate-cluster-admin -f ${workingdir}/alternate_cluster_admin.yaml" "attempt to grant extra privileges"
... ...
@@ -293,9 +293,6 @@ os::cmd::try_until_text "oc get endpoints router --output-version=v1beta3 --temp
293 293
 echo "[INFO] Validating privileged pod exec"
294 294
 router_pod=$(oc get pod -n default -l deploymentconfig=router --template='{{(index .items 0).metadata.name}}')
295 295
 os::cmd::expect_success 'oc policy add-role-to-user admin e2e-default-admin'
296
-# login as a user that can't run privileged pods
297
-os::cmd::expect_success 'oc login -u e2e-default-admin -p pass'
298
-os::cmd::expect_failure_and_text "oc exec -n default -tip ${router_pod} ls" 'unable to validate against any security context constraint'
299 296
 # system:admin should be able to exec into it
300 297
 os::cmd::expect_success "oc project ${CLUSTER_ADMIN_CONTEXT}"
301 298
 os::cmd::expect_success "oc exec -n default -tip ${router_pod} ls"
... ...
@@ -65,8 +65,7 @@ var _ = g.Describe("[builds][Slow] can use private repositories as build input",
65 65
 		o.Expect(err).NotTo(o.HaveOccurred())
66 66
 
67 67
 		g.By("expecting the deployment of the gitserver to be in the Complete phase")
68
-		err = exutil.WaitForADeployment(oc.KubeREST().ReplicationControllers(oc.Namespace()), gitServerDeploymentConfigName,
69
-			exutil.CheckDeploymentCompletedFn, exutil.CheckDeploymentFailedFn)
68
+		err = exutil.WaitForADeploymentToComplete(oc.KubeREST().ReplicationControllers(oc.Namespace()), gitServerDeploymentConfigName)
70 69
 		o.Expect(err).NotTo(o.HaveOccurred())
71 70
 
72 71
 		sourceSecretName := secretFunc()
... ...
@@ -26,11 +26,11 @@ var _ = g.Describe("[builds] build have source revision metadata", func() {
26 26
 	g.Describe("started build", func() {
27 27
 		g.It("should contain source revision information", func() {
28 28
 			g.By("starting the build with --wait flag")
29
-			out, err := oc.Run("start-build").Args("sample-build", "--wait").Output()
29
+			err := oc.Run("start-build").Args("sample-build", "--wait").Execute()
30 30
 			o.Expect(err).NotTo(o.HaveOccurred())
31 31
 
32
-			g.By(fmt.Sprintf("verifying the build %q status", out))
33
-			build, err := oc.REST().Builds(oc.Namespace()).Get(out)
32
+			g.By(fmt.Sprintf("verifying the build %q status", "sample-build-1"))
33
+			build, err := oc.REST().Builds(oc.Namespace()).Get("sample-build-1")
34 34
 			o.Expect(err).NotTo(o.HaveOccurred())
35 35
 			o.Expect(build.Spec.Revision).NotTo(o.BeNil())
36 36
 			o.Expect(build.Spec.Revision.Git).NotTo(o.BeNil())
... ...
@@ -13,6 +13,7 @@ OS_ROOT=$(dirname "${BASH_SOURCE}")/../..
13 13
 source "${OS_ROOT}/hack/util.sh"
14 14
 source "${OS_ROOT}/hack/common.sh"
15 15
 source "${OS_ROOT}/hack/lib/log.sh"
16
+source "${OS_ROOT}/hack/cmd_util.sh"
16 17
 os::log::install_errexit
17 18
 
18 19
 source "${OS_ROOT}/hack/lib/util/environment.sh"
... ...
@@ -98,51 +99,43 @@ oc login ${MASTER_ADDR} -u pull-secrets-user -p password --certificate-authority
98 98
 
99 99
 # create a new project and push a busybox image in there
100 100
 oc new-project image-ns
101
-oc delete all --all
102
-IMAGE_NS_TOKEN=`oc get sa/builder --template='{{range .secrets}}{{ .name }} {{end}}' | xargs -n 1 oc get secret --template='{{ if .data.token }}{{ .data.token }}{{end}}' | base64 -d -`
103
-docker login -u imagensbuilder -p ${IMAGE_NS_TOKEN} -e fake@example.org ${DOCKER_REGISTRY}
104
-oc tag --source=docker busybox:latest image-ns/busybox:latest
105
-oc import-image busybox
106
-docker pull busybox
107
-docker tag -f docker.io/busybox:latest ${DOCKER_REGISTRY}/image-ns/busybox:latest
108
-docker push ${DOCKER_REGISTRY}/image-ns/busybox:latest
109
-docker rmi -f ${DOCKER_REGISTRY}/image-ns/busybox:latest
101
+os::cmd::expect_success "oc delete all --all"
102
+IMAGE_NS_TOKEN=$(oc sa get-token builder)
103
+os::cmd::expect_success "docker login -u imagensbuilder -p ${IMAGE_NS_TOKEN} -e fake@example.org ${DOCKER_REGISTRY}"
104
+os::cmd::expect_success "oc import-image busybox:latest --confirm"
105
+os::cmd::expect_success "docker pull busybox"
106
+os::cmd::expect_success "docker tag -f docker.io/busybox:latest ${DOCKER_REGISTRY}/image-ns/busybox:latest"
107
+os::cmd::expect_success "docker push ${DOCKER_REGISTRY}/image-ns/busybox:latest"
108
+os::cmd::expect_success "docker rmi -f ${DOCKER_REGISTRY}/image-ns/busybox:latest"
110 109
 
111 110
 
112 111
 DOCKER_CONFIG_JSON=${HOME}/.docker/config.json
113 112
 oc new-project dc-ns
114
-oc delete all --all
115
-oc delete secrets --all
116
-oc secrets new image-ns-pull .dockerconfigjson=${DOCKER_CONFIG_JSON}
117
-oc secrets new-dockercfg image-ns-pull-old --docker-email=fake@example.org --docker-username=imagensbuilder --docker-server=${DOCKER_REGISTRY} --docker-password=${IMAGE_NS_TOKEN}
118
-
119
-oc process -f test/extended/fixtures/image-pull-secrets/pod-with-no-pull-secret.yaml --value=DOCKER_REGISTRY=${DOCKER_REGISTRY} | oc create -f - 
120
-wait_for_command "oc describe pod/no-pull-pod | grep 'Back-off pulling image'" 30*TIME_SEC
121
-oc delete pods --all
122
-
123
-# TODO remove sleeps once jsonpath stops panicing.  The code still works without the sleep, it just looks nasty
124
-
125
-oc process -f test/extended/fixtures/image-pull-secrets/pod-with-new-pull-secret.yaml --value=DOCKER_REGISTRY=${DOCKER_REGISTRY} | oc create -f - 
126
-sleep 1
127
-wait_for_command "oc get pods/new-pull-pod -o jsonpath='{.status.containerStatuses[0].imageID}' | grep 'docker'" 30*TIME_SEC
128
-oc delete pods --all
129
-docker rmi -f ${DOCKER_REGISTRY}/image-ns/busybox:latest
130
-
131
-
132
-oc process -f test/extended/fixtures/image-pull-secrets/pod-with-old-pull-secret.yaml --value=DOCKER_REGISTRY=${DOCKER_REGISTRY} | oc create -f - 
133
-sleep 1
134
-wait_for_command "oc get pods/old-pull-pod -o jsonpath={.status.containerStatuses[0].imageID} | grep 'docker'" 30*TIME_SEC
135
-oc delete pods --all
136
-docker rmi -f ${DOCKER_REGISTRY}/image-ns/busybox:latest
137
-
138
-oc process -f test/extended/fixtures/image-pull-secrets/dc-with-old-pull-secret.yaml --value=DOCKER_REGISTRY=${DOCKER_REGISTRY} | oc create -f - 
139
-sleep 4
140
-wait_for_command "oc get pods/my-dc-old-1-prehook -o jsonpath={.status.containerStatuses[0].imageID} | grep 'docker'" 30*TIME_SEC
141
-oc delete all --all
142
-docker rmi -f ${DOCKER_REGISTRY}/image-ns/busybox:latest
143
-
144
-oc process -f test/extended/fixtures/image-pull-secrets/dc-with-new-pull-secret.yaml --value=DOCKER_REGISTRY=${DOCKER_REGISTRY} | oc create -f - 
145
-sleep 4
146
-wait_for_command "oc get pods/my-dc-1-prehook -o jsonpath={.status.containerStatuses[0].imageID} | grep 'docker'" 30*TIME_SEC
147
-oc delete all --all
148
-docker rmi -f ${DOCKER_REGISTRY}/image-ns/busybox:latest
113
+os::cmd::expect_success "oc delete all --all"
114
+os::cmd::expect_success "oc delete secrets --all"
115
+os::cmd::expect_success "oc secrets new image-ns-pull .dockerconfigjson=${DOCKER_CONFIG_JSON}"
116
+os::cmd::expect_success "oc secrets new-dockercfg image-ns-pull-old --docker-email=fake@example.org --docker-username=imagensbuilder --docker-server=${DOCKER_REGISTRY} --docker-password=${IMAGE_NS_TOKEN}"
117
+
118
+os::cmd::expect_success "oc process -f test/extended/fixtures/image-pull-secrets/pod-with-no-pull-secret.yaml --value=DOCKER_REGISTRY=${DOCKER_REGISTRY} | oc create -f - "
119
+os::cmd::try_until_text "oc describe pod/no-pull-pod" 'Back-off pulling image'
120
+os::cmd::expect_success "oc delete pods --all"
121
+
122
+os::cmd::expect_success "oc process -f test/extended/fixtures/image-pull-secrets/pod-with-new-pull-secret.yaml --value=DOCKER_REGISTRY=${DOCKER_REGISTRY} | oc create -f - "
123
+os::cmd::try_until_text 'oc get pods/new-pull-pod -o jsonpath={.status.containerStatuses[0].imageID}' 'docker'
124
+os::cmd::expect_success "oc delete pods --all"
125
+os::cmd::expect_success "docker rmi -f ${DOCKER_REGISTRY}/image-ns/busybox:latest"
126
+
127
+os::cmd::expect_success "oc process -f test/extended/fixtures/image-pull-secrets/pod-with-old-pull-secret.yaml --value=DOCKER_REGISTRY=${DOCKER_REGISTRY} | oc create -f - "
128
+os::cmd::try_until_text 'oc get pods/old-pull-pod -o jsonpath={.status.containerStatuses[0].imageID}' 'docker'
129
+os::cmd::expect_success "oc delete pods --all"
130
+os::cmd::expect_success "docker rmi -f ${DOCKER_REGISTRY}/image-ns/busybox:latest"
131
+
132
+os::cmd::expect_success "oc process -f test/extended/fixtures/image-pull-secrets/dc-with-old-pull-secret.yaml --value=DOCKER_REGISTRY=${DOCKER_REGISTRY} | oc create -f - "
133
+os::cmd::try_until_text 'oc get pods/my-dc-old-1-hook-pre -o jsonpath={.status.containerStatuses[0].imageID}' 'docker'
134
+os::cmd::expect_success "oc delete all --all"
135
+os::cmd::expect_success "docker rmi -f ${DOCKER_REGISTRY}/image-ns/busybox:latest"
136
+
137
+os::cmd::expect_success "oc process -f test/extended/fixtures/image-pull-secrets/dc-with-new-pull-secret.yaml --value=DOCKER_REGISTRY=${DOCKER_REGISTRY} | oc create -f - "
138
+os::cmd::try_until_text 'oc get pods/my-dc-1-hook-pre -o jsonpath={.status.containerStatuses[0].imageID}' 'docker'
139
+os::cmd::expect_success "oc delete all --all"
140
+os::cmd::expect_success "docker rmi -f ${DOCKER_REGISTRY}/image-ns/busybox:latest"
... ...
@@ -79,6 +79,10 @@ if [[ -z ${TEST_ONLY+x} ]]; then
79 79
   fi
80 80
   echo "[INFO] Using VOLUME_DIR=${VOLUME_DIR}"
81 81
 
82
+  # This is a bit hacky, but set the pod gc threshold appropriately for the garbage_collector test.
83
+  os::util::sed 's/\(controllerArguments:\ \)null/\1\n    terminated-pod-gc-threshold: ["100"]/' \
84
+    ${MASTER_CONFIG_DIR}/master-config.yaml
85
+
82 86
   start_os_server
83 87
 
84 88
   export KUBECONFIG="${ADMIN_KUBECONFIG}"
... ...
@@ -119,9 +123,11 @@ excluded_tests=(
119 119
   "Cluster level logging" # Not installed yet
120 120
   Kibana                  # Not installed
121 121
   DNS                     # Can't depend on kube-dns
122
+  Ubernetes               # Can't set zone labels today
122 123
   kube-ui                 # Not installed by default
123 124
   "^Kubernetes Dashboard"  # Not installed by default (also probbaly slow image pull)
124 125
   "\[Feature:Deployment\]" # Not enabled yet
126
+  "^Deployment\s" # Not enabled yet
125 127
   "paused deployment should be ignored by the controller" # Not enabled yet
126 128
   "deployment should create new pods" # Not enabled yet
127 129
   Ingress                 # Not enabled yet
... ...
@@ -24,8 +24,9 @@ var _ = g.Describe("[images][mongodb] openshift mongodb image", func() {
24 24
 			g.By("creating a new app")
25 25
 			o.Expect(oc.Run("new-app").Args("-f", templatePath).Execute()).Should(o.Succeed())
26 26
 
27
-			g.By("expecting the mongodb service get endpoints")
28
-			o.Expect(oc.KubeFramework().WaitForAnEndpoint("mongodb")).Should(o.Succeed())
27
+			g.By("waiting for the deployment to complete")
28
+			err := exutil.WaitForADeploymentToComplete(oc.KubeREST().ReplicationControllers(oc.Namespace()), "mongodb")
29
+			o.Expect(err).ShouldNot(o.HaveOccurred())
29 30
 
30 31
 			g.By("expecting the mongodb pod is running")
31 32
 			podNames, err := exutil.WaitForPods(
... ...
@@ -39,22 +40,10 @@ var _ = g.Describe("[images][mongodb] openshift mongodb image", func() {
39 39
 			o.Expect(podNames).Should(o.HaveLen(1))
40 40
 
41 41
 			g.By("expecting the mongodb service is answering for ping")
42
-			mongo := db.NewMongoDB(podNames[0], "")
43
-
44
-			for times := 0; times < 10; times++ {
45
-				ok, err := mongo.IsReady(oc)
46
-				if ok {
47
-					break
48
-				}
49
-
50
-				if times == 10 {
51
-					o.Expect(err).ShouldNot(o.HaveOccurred())
52
-					o.Expect(ok).Should(o.BeTrue())
53
-					break
54
-				}
55
-
56
-				time.Sleep(1 * time.Second)
57
-			}
42
+			mongo := db.NewMongoDB(podNames[0])
43
+			ok, err := mongo.IsReady(oc)
44
+			o.Expect(err).ShouldNot(o.HaveOccurred())
45
+			o.Expect(ok).Should(o.BeTrue())
58 46
 
59 47
 			g.By("expecting that we can insert a new record")
60 48
 			result, err := mongo.Query(oc, `db.foo.save({ "status": "passed" })`)
61 49
new file mode 100644
... ...
@@ -0,0 +1,116 @@
0
+package images
1
+
2
+import (
3
+	"fmt"
4
+	"strconv"
5
+	"time"
6
+
7
+	g "github.com/onsi/ginkgo"
8
+	o "github.com/onsi/gomega"
9
+
10
+	exutil "github.com/openshift/origin/test/extended/util"
11
+	"github.com/openshift/origin/test/extended/util/db"
12
+)
13
+
14
+var _ = g.Describe("[images][mongodb] openshift mongodb replication", func() {
15
+	defer g.GinkgoRecover()
16
+
17
+	const (
18
+		templatePath         = "https://raw.githubusercontent.com/openshift/mongodb/master/2.4/examples/replica/mongodb-clustered.json"
19
+		deploymentConfigName = "mongodb"
20
+		expectedValue        = `{ "status" : "passed" }`
21
+		insertCmd            = "db.bar.save(" + expectedValue + ")"
22
+	)
23
+
24
+	const (
25
+		expectedReplicasAfterDeployment = 3
26
+		expectedReplicasAfterScalingUp  = expectedReplicasAfterDeployment + 2
27
+	)
28
+
29
+	oc := exutil.NewCLI("mongodb-replica", exutil.KubeConfigPath()).Verbose()
30
+
31
+	g.Describe("creating from a template", func() {
32
+		g.It(fmt.Sprintf("should process and create the %q template", templatePath), func() {
33
+
34
+			g.By("creating a new app")
35
+			o.Expect(oc.Run("new-app").Args("-f", templatePath).Execute()).Should(o.Succeed())
36
+
37
+			g.By("waiting for the deployment to complete")
38
+			o.Expect(exutil.WaitForADeploymentToComplete(oc.KubeREST().ReplicationControllers(oc.Namespace()), deploymentConfigName)).Should(o.Succeed())
39
+
40
+			podNames := waitForNumberOfPodsWithLabel(oc, expectedReplicasAfterDeployment, "mongodb-replica")
41
+			mongo := db.NewMongoDB(podNames[0])
42
+
43
+			g.By(fmt.Sprintf("expecting that replica set have %d members", expectedReplicasAfterDeployment))
44
+			assertMembersInReplica(oc, mongo, expectedReplicasAfterDeployment)
45
+
46
+			g.By("expecting that we can insert a new record on primary node")
47
+			replicaSet := mongo.(exutil.ReplicaSet)
48
+			_, err := replicaSet.QueryPrimary(oc, insertCmd)
49
+			o.Expect(err).ShouldNot(o.HaveOccurred())
50
+
51
+			g.By("expecting that we can read a record from all members")
52
+			for _, podName := range podNames {
53
+				tryToReadFromPod(oc, podName, expectedValue)
54
+			}
55
+
56
+			g.By(fmt.Sprintf("scaling deployment config %s to %d replicas", deploymentConfigName, expectedReplicasAfterScalingUp))
57
+
58
+			err = oc.Run("scale").Args("dc", deploymentConfigName, "--replicas="+fmt.Sprint(expectedReplicasAfterScalingUp), "--timeout=30s").Execute()
59
+			o.Expect(err).NotTo(o.HaveOccurred())
60
+
61
+			podNames = waitForNumberOfPodsWithLabel(oc, expectedReplicasAfterScalingUp, "mongodb-replica")
62
+			mongo = db.NewMongoDB(podNames[0])
63
+
64
+			g.By("expecting that scaling replica set up should have more members")
65
+			assertMembersInReplica(oc, mongo, expectedReplicasAfterScalingUp)
66
+		})
67
+	})
68
+
69
+})
70
+
71
+func tryToReadFromPod(oc *exutil.CLI, podName, expectedValue string) {
72
+	// don't include _id field to output because it changes every time
73
+	findCmd := "rs.slaveOk(); printjson(db.bar.find({}, {_id: 0}).toArray())"
74
+
75
+	fmt.Fprintf(g.GinkgoWriter, "DEBUG: reading record from pod %v\n", podName)
76
+
77
+	mongoPod := db.NewMongoDB(podName)
78
+	result, err := mongoPod.Query(oc, findCmd)
79
+	o.Expect(err).ShouldNot(o.HaveOccurred())
80
+	o.Expect(result).Should(o.ContainSubstring(expectedValue))
81
+}
82
+
83
+func waitForNumberOfPodsWithLabel(oc *exutil.CLI, number int, label string) []string {
84
+	g.By(fmt.Sprintf("expecting that there are %d running pods with label name=%s", number, label))
85
+
86
+	podNames, err := exutil.WaitForPods(
87
+		oc.KubeREST().Pods(oc.Namespace()),
88
+		exutil.ParseLabelsOrDie("name="+label),
89
+		exutil.CheckPodIsRunningFn,
90
+		number,
91
+		1*time.Minute,
92
+	)
93
+	o.Expect(err).ShouldNot(o.HaveOccurred())
94
+	o.Expect(podNames).Should(o.HaveLen(number))
95
+
96
+	return podNames
97
+}
98
+
99
+func assertMembersInReplica(oc *exutil.CLI, db exutil.Database, expectedReplicas int) {
100
+	isMasterCmd := "printjson(db.isMaster())"
101
+	getReplicaHostsCmd := "print(db.isMaster().hosts.length)"
102
+
103
+	// pod is running but we need to wait when it will be really ready (became member of the replica)
104
+	err := exutil.WaitForQueryOutputSatisfies(oc, db, 1*time.Minute, false, isMasterCmd, func(commandOutput string) bool {
105
+		return commandOutput != ""
106
+	})
107
+	o.Expect(err).ShouldNot(o.HaveOccurred())
108
+
109
+	isMasterOutput, _ := db.Query(oc, isMasterCmd)
110
+	fmt.Fprintf(g.GinkgoWriter, "DEBUG: Output of the db.isMaster() command: %v\n", isMasterOutput)
111
+
112
+	members, err := db.Query(oc, getReplicaHostsCmd)
113
+	o.Expect(err).ShouldNot(o.HaveOccurred())
114
+	o.Expect(members).Should(o.Equal(strconv.Itoa(expectedReplicas)))
115
+}
... ...
@@ -115,12 +115,12 @@ func replicationTestFactory(oc *exutil.CLI, tc testCase) func() {
115 115
 			o.Expect(err).NotTo(o.HaveOccurred())
116 116
 
117 117
 			// Make sure data is present on master
118
-			err = exutil.WaitForQueryOutput(oc, master, 10*time.Second, false, fmt.Sprintf("SELECT * FROM %s\\G;", table), "col1: val1\ncol2: val2")
118
+			err = exutil.WaitForQueryOutputContains(oc, master, 10*time.Second, false, fmt.Sprintf("SELECT * FROM %s\\G;", table), "col1: val1\ncol2: val2")
119 119
 			o.Expect(err).NotTo(o.HaveOccurred())
120 120
 
121 121
 			// Make sure data was replicated to all slaves
122 122
 			for _, slave := range slaves {
123
-				err = exutil.WaitForQueryOutput(oc, slave, 90*time.Second, false, fmt.Sprintf("SELECT * FROM %s\\G;", table), "col1: val1\ncol2: val2")
123
+				err = exutil.WaitForQueryOutputContains(oc, slave, 90*time.Second, false, fmt.Sprintf("SELECT * FROM %s\\G;", table), "col1: val1\ncol2: val2")
124 124
 				o.Expect(err).NotTo(o.HaveOccurred())
125 125
 			}
126 126
 
... ...
@@ -107,14 +107,14 @@ func PostgreSQLReplicationTestFactory(oc *exutil.CLI, image string) func() {
107 107
 			o.Expect(err).NotTo(o.HaveOccurred())
108 108
 
109 109
 			// Make sure data is present on master
110
-			err = exutil.WaitForQueryOutput(oc, master, 10*time.Second, false,
110
+			err = exutil.WaitForQueryOutputContains(oc, master, 10*time.Second, false,
111 111
 				fmt.Sprintf("SELECT * FROM %s;", table),
112 112
 				"col1 | val1\ncol2 | val2")
113 113
 			o.Expect(err).NotTo(o.HaveOccurred())
114 114
 
115 115
 			// Make sure data was replicated to all slaves
116 116
 			for _, slave := range slaves {
117
-				err = exutil.WaitForQueryOutput(oc, slave, 90*time.Second, false,
117
+				err = exutil.WaitForQueryOutputContains(oc, slave, 90*time.Second, false,
118 118
 					fmt.Sprintf("SELECT * FROM %s;", table),
119 119
 					"col1 | val1\ncol2 | val2")
120 120
 				o.Expect(err).NotTo(o.HaveOccurred())
... ...
@@ -55,7 +55,7 @@ func NewSampleRepoTest(c SampleRepoConfig) func() {
55 55
 				}
56 56
 
57 57
 				g.By("expecting the deployment to be complete")
58
-				err = exutil.WaitForADeployment(oc.KubeREST().ReplicationControllers(oc.Namespace()), c.deploymentConfigName, exutil.CheckDeploymentCompletedFn, exutil.CheckDeploymentFailedFn)
58
+				err = exutil.WaitForADeploymentToComplete(oc.KubeREST().ReplicationControllers(oc.Namespace()), c.deploymentConfigName)
59 59
 				o.Expect(err).NotTo(o.HaveOccurred())
60 60
 
61 61
 				g.By("expecting the service is available")
... ...
@@ -70,8 +70,7 @@ var _ = g.Describe("[jenkins] schedule jobs on pod slaves", func() {
70 70
 			o.Expect(err).NotTo(o.HaveOccurred())
71 71
 
72 72
 			g.By("wait for jenkins deployment")
73
-			err = exutil.WaitForADeployment(oc.KubeREST().ReplicationControllers(oc.Namespace()), "jenkins",
74
-				exutil.CheckDeploymentCompletedFn, exutil.CheckDeploymentFailedFn)
73
+			err = exutil.WaitForADeploymentToComplete(oc.KubeREST().ReplicationControllers(oc.Namespace()), "jenkins")
75 74
 			o.Expect(err).NotTo(o.HaveOccurred())
76 75
 
77 76
 			g.By("get ip and port for jenkins service")
... ...
@@ -111,7 +111,7 @@ var _ = g.Describe("[jenkins] openshift pipeline plugin", func() {
111 111
 		o.Expect(err).NotTo(o.HaveOccurred())
112 112
 
113 113
 		g.By("waiting for jenkins deployment")
114
-		err = exutil.WaitForADeployment(oc.KubeREST().ReplicationControllers(oc.Namespace()), "jenkins", exutil.CheckDeploymentCompletedFn, exutil.CheckDeploymentFailedFn)
114
+		err = exutil.WaitForADeploymentToComplete(oc.KubeREST().ReplicationControllers(oc.Namespace()), "jenkins")
115 115
 		o.Expect(err).NotTo(o.HaveOccurred())
116 116
 
117 117
 		g.By("get ip and port for jenkins service")
... ...
@@ -144,9 +144,9 @@ var _ = g.Describe("[jenkins] openshift pipeline plugin", func() {
144 144
 			// we leverage some of the openshift utilities for waiting for the deployment before we poll
145 145
 			// jenkins for the sucessful job completion
146 146
 			g.By("waiting for frontend, frontend-prod deployments as signs that the build has finished")
147
-			err := exutil.WaitForADeployment(oc.KubeREST().ReplicationControllers(oc.Namespace()), "frontend", exutil.CheckDeploymentCompletedFn, exutil.CheckDeploymentFailedFn)
147
+			err := exutil.WaitForADeploymentToComplete(oc.KubeREST().ReplicationControllers(oc.Namespace()), "frontend")
148 148
 			o.Expect(err).NotTo(o.HaveOccurred())
149
-			err = exutil.WaitForADeployment(oc.KubeREST().ReplicationControllers(oc.Namespace()), "frontend-prod", exutil.CheckDeploymentCompletedFn, exutil.CheckDeploymentFailedFn)
149
+			err = exutil.WaitForADeploymentToComplete(oc.KubeREST().ReplicationControllers(oc.Namespace()), "frontend-prod")
150 150
 			o.Expect(err).NotTo(o.HaveOccurred())
151 151
 
152 152
 			g.By("get build console logs and see if succeeded")
... ...
@@ -12,7 +12,7 @@ import (
12 12
 	g "github.com/onsi/ginkgo"
13 13
 	o "github.com/onsi/gomega"
14 14
 
15
-	"github.com/openshift/origin/pkg/volume/empty_dir"
15
+	"github.com/openshift/origin/pkg/volume/emptydir"
16 16
 	exutil "github.com/openshift/origin/test/extended/util"
17 17
 )
18 18
 
... ...
@@ -58,7 +58,7 @@ func lookupFSGroup(oc *exutil.CLI, project string) (int, error) {
58 58
 func lookupXFSQuota(oc *exutil.CLI, fsGroup int, volDir string) (int, error) {
59 59
 
60 60
 	// First lookup the filesystem device the volumeDir resides on:
61
-	fsDevice, err := empty_dir.GetFSDevice(volDir)
61
+	fsDevice, err := emptydir.GetFSDevice(volDir)
62 62
 	if err != nil {
63 63
 		return 0, err
64 64
 	}
... ...
@@ -9,18 +9,13 @@ import (
9 9
 
10 10
 // MongoDB is a MongoDB helper for executing commands.
11 11
 type MongoDB struct {
12
-	podName       string
13
-	masterPodName string
12
+	podName string
14 13
 }
15 14
 
16 15
 // NewMongoDB creates a new util.Database instance.
17
-func NewMongoDB(podName, masterPodName string) util.Database {
18
-	if masterPodName == "" {
19
-		masterPodName = podName
20
-	}
16
+func NewMongoDB(podName string) util.Database {
21 17
 	return &MongoDB{
22
-		podName:       podName,
23
-		masterPodName: masterPodName,
18
+		podName: podName,
24 19
 	}
25 20
 }
26 21
 
... ...
@@ -53,7 +48,19 @@ func (m MongoDB) QueryPrivileged(oc *util.CLI, query string) (string, error) {
53 53
 	return "", errors.New("not implemented")
54 54
 }
55 55
 
56
-// TestRemoteLogin tests wheather it is possible to remote login to hostAddress.
56
+// TestRemoteLogin tests whether it is possible to remote login to hostAddress.
57 57
 func (m MongoDB) TestRemoteLogin(oc *util.CLI, hostAddress string) error {
58 58
 	return errors.New("not implemented")
59 59
 }
60
+
61
+// // QueryPrimary queries the database on primary node as a regular user.
62
+func (m MongoDB) QueryPrimary(oc *util.CLI, query string) (string, error) {
63
+	return executeShellCommand(
64
+		oc,
65
+		m.podName,
66
+		fmt.Sprintf(
67
+			`mongo --quiet "$MONGODB_DATABASE" --username "$MONGODB_USER" --password "$MONGODB_PASSWORD" --host "$MONGODB_REPLICA_NAME/localhost" --eval '%s'`,
68
+			query,
69
+		),
70
+	)
71
+}
... ...
@@ -23,15 +23,19 @@ type Database interface {
23 23
 	// QueryPrivileged queries the database as a privileged user.
24 24
 	QueryPrivileged(oc *CLI, query string) (string, error)
25 25
 
26
-	// TestRemoteLogin tests wheather it is possible to remote login to hostAddress.
26
+	// TestRemoteLogin tests whether it is possible to remote login to hostAddress.
27 27
 	TestRemoteLogin(oc *CLI, hostAddress string) error
28 28
 }
29 29
 
30
-// WaitForQueryOutput will execute the query multiple times, until the
31
-// specified substring is found in the results. This function should be used for
32
-// testing replication, since it might take some time untill the data is propagated
33
-// to slaves.
34
-func WaitForQueryOutput(oc *CLI, d Database, timeout time.Duration, admin bool, query, resultSubstr string) error {
30
+// ReplicaSet interface allows to interact with database on multiple nodes.
31
+type ReplicaSet interface {
32
+	// QueryPrimary queries the database on primary node as a regular user.
33
+	QueryPrimary(oc *CLI, query string) (string, error)
34
+}
35
+
36
+// WaitForQueryOutputSatisfies will execute the query multiple times, until the
37
+// specified predicate function is return true.
38
+func WaitForQueryOutputSatisfies(oc *CLI, d Database, timeout time.Duration, admin bool, query string, predicate func(string) bool) error {
35 39
 	err := wait.Poll(5*time.Second, timeout, func() (bool, error) {
36 40
 		var (
37 41
 			out string
... ...
@@ -50,7 +54,7 @@ func WaitForQueryOutput(oc *CLI, d Database, timeout time.Duration, admin bool,
50 50
 		if err != nil {
51 51
 			return false, err
52 52
 		}
53
-		if strings.Contains(out, resultSubstr) {
53
+		if predicate(out) {
54 54
 			return true, nil
55 55
 		}
56 56
 		return false, nil
... ...
@@ -61,6 +65,16 @@ func WaitForQueryOutput(oc *CLI, d Database, timeout time.Duration, admin bool,
61 61
 	return err
62 62
 }
63 63
 
64
+// WaitForQueryOutputContains will execute the query multiple times, until the
65
+// specified substring is found in the results. This function should be used for
66
+// testing replication, since it might take some time until the data is propagated
67
+// to slaves.
68
+func WaitForQueryOutputContains(oc *CLI, d Database, timeout time.Duration, admin bool, query, resultSubstr string) error {
69
+	return WaitForQueryOutputSatisfies(oc, d, timeout, admin, query, func(resultOutput string) bool {
70
+		return strings.Contains(resultOutput, resultSubstr)
71
+	})
72
+}
73
+
64 74
 // WaitUntilUp continuously waits for the server to become ready, up until timeout.
65 75
 func WaitUntilUp(oc *CLI, d Database, timeout time.Duration) error {
66 76
 	err := wait.Poll(2*time.Second, timeout, func() (bool, error) {
... ...
@@ -274,6 +274,11 @@ func WaitForADeployment(client kclient.ReplicationControllerInterface, name stri
274 274
 	}
275 275
 }
276 276
 
277
+// WaitForADeploymentToComplete waits for a deployment to complete.
278
+func WaitForADeploymentToComplete(client kclient.ReplicationControllerInterface, name string) error {
279
+	return WaitForADeployment(client, name, CheckDeploymentCompletedFn, CheckDeploymentFailedFn)
280
+}
281
+
277 282
 func isUsageSynced(received, expected kapi.ResourceList, expectedIsUpperLimit bool) bool {
278 283
 	resourceNames := quota.ResourceNames(expected)
279 284
 	masked := quota.Mask(received, resourceNames)
... ...
@@ -112,11 +112,19 @@ func ExecuteTest(t *testing.T, suite string) {
112 112
 	}
113 113
 }
114 114
 
115
+// TODO: Use either explicit tags (k8s.io) or https://github.com/onsi/ginkgo/pull/228 to implement this.
116
+// isPackage determines wether the test is in a package.  Ideally would be implemented in ginkgo.
117
+func isPackage(pkg string) bool {
118
+	return strings.Contains(ginkgo.CurrentGinkgoTestDescription().FileName, pkg)
119
+}
120
+
121
+// TODO: For both is*Test functions, use either explicit tags (k8s.io) or https://github.com/onsi/ginkgo/pull/228
115 122
 func isOriginTest() bool {
116
-	return strings.Contains(ginkgo.CurrentGinkgoTestDescription().FileName, "/origin/test/")
123
+	return isPackage("/origin/test/")
117 124
 }
125
+
118 126
 func isKubernetesE2ETest() bool {
119
-	return strings.Contains(ginkgo.CurrentGinkgoTestDescription().FileName, "/kubernetes/test/e2e/")
127
+	return isPackage("/kubernetes/test/e2e/")
120 128
 }
121 129
 
122 130
 // Holds custom namespace creation functions so we can customize per-test
... ...
@@ -172,12 +180,12 @@ func createTestingNS(baseName string, c *kclient.Client, labels map[string]strin
172 172
 func checkSuiteSkips() {
173 173
 	switch {
174 174
 	case isOriginTest():
175
-		if strings.Contains(config.GinkgoConfig.SkipString, "[Origin]") {
176
-			ginkgo.Skip("skipping [Origin] tests")
175
+		if strings.Contains(config.GinkgoConfig.SkipString, "Synthetic Origin") {
176
+			ginkgo.Skip("skipping all openshift/origin tests")
177 177
 		}
178 178
 	case isKubernetesE2ETest():
179
-		if strings.Contains(config.GinkgoConfig.SkipString, "[Kubernetes]") {
180
-			ginkgo.Skip("skipping [Kubernetes] tests")
179
+		if strings.Contains(config.GinkgoConfig.SkipString, "Synthetic Kubernetes") {
180
+			ginkgo.Skip("skipping all k8s.io/kubernetes tests")
181 181
 		}
182 182
 	}
183 183
 }
... ...
@@ -271,7 +271,6 @@ items:
271 271
     - extensions
272 272
     attributeRestrictions: null
273 273
     resources:
274
-    - daemonsets
275 274
     - horizontalpodautoscalers
276 275
     - jobs
277 276
     - replicationcontrollers/scale
... ...
@@ -284,6 +283,15 @@ items:
284 284
     - patch
285 285
     - update
286 286
     - watch
287
+  - apiGroups:
288
+    - extensions
289
+    attributeRestrictions: null
290
+    resources:
291
+    - daemonsets
292
+    verbs:
293
+    - get
294
+    - list
295
+    - watch
287 296
   - apiGroups: null
288 297
     attributeRestrictions: null
289 298
     resources:
... ...
@@ -436,7 +444,6 @@ items:
436 436
     - extensions
437 437
     attributeRestrictions: null
438 438
     resources:
439
-    - daemonsets
440 439
     - horizontalpodautoscalers
441 440
     - jobs
442 441
     - replicationcontrollers/scale
... ...
@@ -449,6 +456,15 @@ items:
449 449
     - patch
450 450
     - update
451 451
     - watch
452
+  - apiGroups:
453
+    - extensions
454
+    attributeRestrictions: null
455
+    resources:
456
+    - daemonsets
457
+    verbs:
458
+    - get
459
+    - list
460
+    - watch
452 461
   - apiGroups: null
453 462
     attributeRestrictions: null
454 463
     resources:
... ...
@@ -1411,6 +1427,27 @@ items:
1411 1411
   kind: ClusterRole
1412 1412
   metadata:
1413 1413
     creationTimestamp: null
1414
+    name: system:gc-controller
1415
+  rules:
1416
+  - apiGroups:
1417
+    - ""
1418
+    attributeRestrictions: null
1419
+    resources:
1420
+    - pods
1421
+    verbs:
1422
+    - list
1423
+    - watch
1424
+  - apiGroups:
1425
+    - ""
1426
+    attributeRestrictions: null
1427
+    resources:
1428
+    - pods
1429
+    verbs:
1430
+    - delete
1431
+- apiVersion: v1
1432
+  kind: ClusterRole
1433
+  metadata:
1434
+    creationTimestamp: null
1414 1435
     name: system:hpa-controller
1415 1436
   rules:
1416 1437
   - apiGroups:
... ...
@@ -4,7 +4,6 @@ package integration
4 4
 
5 5
 import (
6 6
 	"testing"
7
-	"time"
8 7
 
9 8
 	testutil "github.com/openshift/origin/test/util"
10 9
 	testserver "github.com/openshift/origin/test/util/server"
... ...
@@ -12,9 +11,130 @@ import (
12 12
 	kapi "k8s.io/kubernetes/pkg/api"
13 13
 	"k8s.io/kubernetes/pkg/api/errors"
14 14
 	expapi "k8s.io/kubernetes/pkg/apis/extensions"
15
-	"k8s.io/kubernetes/pkg/util/wait"
16 15
 )
17 16
 
17
+func TestExtensionsAPIDisabledAutoscaleBatchEnabled(t *testing.T) {
18
+	const projName = "ext-disabled-batch-enabled-proj"
19
+
20
+	testutil.RequireEtcd(t)
21
+	masterConfig, err := testserver.DefaultMasterOptions()
22
+	if err != nil {
23
+		t.Fatalf("unexpected error: %v", err)
24
+	}
25
+
26
+	// Disable all extensions API versions
27
+	// Leave autoscaling/batch APIs enabled
28
+	masterConfig.KubernetesMasterConfig.DisabledAPIGroupVersions = map[string][]string{"extensions": {"*"}}
29
+
30
+	clusterAdminKubeConfig, err := testserver.StartConfiguredMaster(masterConfig)
31
+	if err != nil {
32
+		t.Fatalf("unexpected error: %v", err)
33
+	}
34
+
35
+	clusterAdminClient, err := testutil.GetClusterAdminClient(clusterAdminKubeConfig)
36
+	if err != nil {
37
+		t.Fatalf("unexpected error: %v", err)
38
+	}
39
+
40
+	clusterAdminKubeClient, err := testutil.GetClusterAdminKubeClient(clusterAdminKubeConfig)
41
+	if err != nil {
42
+		t.Fatalf("unexpected error: %v", err)
43
+	}
44
+
45
+	clusterAdminClientConfig, err := testutil.GetClusterAdminClientConfig(clusterAdminKubeConfig)
46
+	if err != nil {
47
+		t.Fatalf("unexpected error: %v", err)
48
+	}
49
+
50
+	// create the containing project
51
+	if _, err := testserver.CreateNewProject(clusterAdminClient, *clusterAdminClientConfig, projName, "admin"); err != nil {
52
+		t.Fatalf("unexpected error creating the project: %v", err)
53
+	}
54
+	projectAdminClient, projectAdminKubeClient, _, err := testutil.GetClientForUser(*clusterAdminClientConfig, "admin")
55
+	if err != nil {
56
+		t.Fatalf("unexpected error getting project admin client: %v", err)
57
+	}
58
+	if err := testutil.WaitForPolicyUpdate(projectAdminClient, projName, "get", expapi.Resource("horizontalpodautoscalers"), true); err != nil {
59
+		t.Fatalf("unexpected error waiting for policy update: %v", err)
60
+	}
61
+
62
+	validHPA := &expapi.HorizontalPodAutoscaler{
63
+		ObjectMeta: kapi.ObjectMeta{Name: "myjob"},
64
+		Spec: expapi.HorizontalPodAutoscalerSpec{
65
+			ScaleRef:    expapi.SubresourceReference{Name: "foo", Kind: "ReplicationController", Subresource: "scale"},
66
+			MaxReplicas: 1,
67
+		},
68
+	}
69
+	validJob := &expapi.Job{
70
+		ObjectMeta: kapi.ObjectMeta{Name: "myjob"},
71
+		Spec: expapi.JobSpec{
72
+			Template: kapi.PodTemplateSpec{
73
+				Spec: kapi.PodSpec{
74
+					Containers:    []kapi.Container{{Name: "mycontainer", Image: "myimage"}},
75
+					RestartPolicy: kapi.RestartPolicyNever,
76
+				},
77
+			},
78
+		},
79
+	}
80
+
81
+	// make sure extensions API objects cannot be listed or created
82
+	if _, err := projectAdminKubeClient.Extensions().HorizontalPodAutoscalers(projName).List(kapi.ListOptions{}); !errors.IsNotFound(err) {
83
+		t.Fatalf("expected NotFound error listing HPA, got %v", err)
84
+	}
85
+	if _, err := projectAdminKubeClient.Extensions().HorizontalPodAutoscalers(projName).Create(validHPA); !errors.IsNotFound(err) {
86
+		t.Fatalf("expected NotFound error creating HPA, got %v", err)
87
+	}
88
+	if _, err := projectAdminKubeClient.Extensions().Jobs(projName).List(kapi.ListOptions{}); !errors.IsNotFound(err) {
89
+		t.Fatalf("expected NotFound error listing jobs, got %v", err)
90
+	}
91
+	if _, err := projectAdminKubeClient.Extensions().Jobs(projName).Create(validJob); !errors.IsNotFound(err) {
92
+		t.Fatalf("expected NotFound error creating job, got %v", err)
93
+	}
94
+
95
+	// make sure autoscaling and batch API objects can be listed and created
96
+	if _, err := projectAdminKubeClient.Autoscaling().HorizontalPodAutoscalers(projName).List(kapi.ListOptions{}); err != nil {
97
+		t.Fatalf("unexpected error: %#v", err)
98
+	}
99
+	if _, err := projectAdminKubeClient.Autoscaling().HorizontalPodAutoscalers(projName).Create(validHPA); err != nil {
100
+		t.Fatalf("unexpected error: %#v", err)
101
+	}
102
+	if _, err := projectAdminKubeClient.Batch().Jobs(projName).List(kapi.ListOptions{}); err != nil {
103
+		t.Fatalf("unexpected error: %#v", err)
104
+	}
105
+	if _, err := projectAdminKubeClient.Batch().Jobs(projName).Create(validJob); err != nil {
106
+		t.Fatalf("unexpected error: %#v", err)
107
+	}
108
+
109
+	// Delete the containing project
110
+	if err := testutil.DeleteAndWaitForNamespaceTermination(clusterAdminKubeClient, projName); err != nil {
111
+		t.Fatalf("unexpected error: %#v", err)
112
+	}
113
+
114
+	// recreate the containing project
115
+	if _, err := testserver.CreateNewProject(clusterAdminClient, *clusterAdminClientConfig, projName, "admin"); err != nil {
116
+		t.Fatalf("unexpected error creating the project: %v", err)
117
+	}
118
+	projectAdminClient, projectAdminKubeClient, _, err = testutil.GetClientForUser(*clusterAdminClientConfig, "admin")
119
+	if err != nil {
120
+		t.Fatalf("unexpected error getting project admin client: %v", err)
121
+	}
122
+	if err := testutil.WaitForPolicyUpdate(projectAdminClient, projName, "get", expapi.Resource("horizontalpodautoscalers"), true); err != nil {
123
+		t.Fatalf("unexpected error waiting for policy update: %v", err)
124
+	}
125
+
126
+	// make sure the created objects got cleaned up by namespace deletion
127
+	if hpas, err := projectAdminKubeClient.Autoscaling().HorizontalPodAutoscalers(projName).List(kapi.ListOptions{}); err != nil {
128
+		t.Fatalf("unexpected error: %#v", err)
129
+	} else if len(hpas.Items) > 0 {
130
+		t.Fatalf("expected 0 HPA objects, got %#v", hpas.Items)
131
+	}
132
+	if jobs, err := projectAdminKubeClient.Batch().Jobs(projName).List(kapi.ListOptions{}); err != nil {
133
+		t.Fatalf("unexpected error: %#v", err)
134
+	} else if len(jobs.Items) > 0 {
135
+		t.Fatalf("expected 0 Job objects, got %#v", jobs.Items)
136
+	}
137
+}
138
+
18 139
 func TestExtensionsAPIDisabled(t *testing.T) {
19 140
 	const projName = "ext-disabled-proj"
20 141
 
... ...
@@ -73,17 +193,8 @@ func TestExtensionsAPIDisabled(t *testing.T) {
73 73
 		t.Fatalf("expected NotFound error creating job, got %v", err)
74 74
 	}
75 75
 
76
-	if err := clusterAdminClient.Projects().Delete(projName); err != nil {
77
-		t.Fatalf("unexpected error deleting the project: %v", err)
78
-	}
79
-	err = wait.PollImmediate(1*time.Second, 30*time.Second, func() (bool, error) {
80
-		_, err := clusterAdminKubeClient.Namespaces().Get(projName)
81
-		if errors.IsNotFound(err) {
82
-			return true, nil
83
-		}
84
-		return false, err
85
-	})
86
-	if err != nil {
87
-		t.Fatalf("unexpected error while waiting for project to delete: %v", err)
76
+	// Delete the containing project
77
+	if err := testutil.DeleteAndWaitForNamespaceTermination(clusterAdminKubeClient, projName); err != nil {
78
+		t.Fatalf("unexpected error: %#v", err)
88 79
 	}
89 80
 }
... ...
@@ -44,11 +44,6 @@ const (
44 44
 	statsPassword = "e2e"
45 45
 )
46 46
 
47
-// init ensures docker exists for this test
48
-func init() {
49
-	testutil.RequireDocker()
50
-}
51
-
52 47
 // TestRouter is the table based test for routers.  It will initialize a fake master/client and expect to deploy
53 48
 // a router image in docker.  It then sends watch events through the simulator and makes http client requests that
54 49
 // should go through the deployed router and return data from the client simulator.
... ...
@@ -1,14 +1,10 @@
1 1
 package util
2 2
 
3 3
 import (
4
-	"os"
5
-
6 4
 	dockerClient "github.com/fsouza/go-dockerclient"
7 5
 	"github.com/golang/glog"
8 6
 )
9 7
 
10
-const defaultDockerEndpoint = "unix:///var/run/docker.sock"
11
-
12 8
 // RequireDocker ensures that a new docker client can be created and that a ListImages command can be run on the client
13 9
 // or it fails with glog.Fatal
14 10
 func RequireDocker() {
... ...
@@ -29,11 +25,5 @@ func RequireDocker() {
29 29
 // newDockerClient creates a docker client using the env var DOCKER_ENDPOINT or, if not supplied, uses the default
30 30
 // docker endpoint /var/run/docker.sock
31 31
 func NewDockerClient() (*dockerClient.Client, error) {
32
-	endpoint := os.Getenv("DOCKER_ENDPOINT")
33
-
34
-	if len(endpoint) == 0 {
35
-		endpoint = defaultDockerEndpoint
36
-	}
37
-
38
-	return dockerClient.NewClient(endpoint)
32
+	return dockerClient.NewClientFromEnv()
39 33
 }
... ...
@@ -4,8 +4,12 @@ import (
4 4
 	"fmt"
5 5
 	"time"
6 6
 
7
-	"github.com/openshift/origin/pkg/cmd/util"
8 7
 	kapi "k8s.io/kubernetes/pkg/api"
8
+	kclient "k8s.io/kubernetes/pkg/client/unversioned"
9
+	"k8s.io/kubernetes/pkg/watch"
10
+
11
+	"github.com/openshift/origin/pkg/cmd/cli/cmd"
12
+	"github.com/openshift/origin/pkg/cmd/util"
9 13
 )
10 14
 
11 15
 // Namespace returns the test namespace. The default namespace is set to
... ...
@@ -33,3 +37,24 @@ func CreateNamespace(clusterAdminKubeConfig, name string) (err error) {
33 33
 	})
34 34
 	return err
35 35
 }
36
+
37
+func DeleteAndWaitForNamespaceTermination(c *kclient.Client, name string) error {
38
+	w, err := c.Namespaces().Watch(kapi.ListOptions{})
39
+	if err != nil {
40
+		return err
41
+	}
42
+	if err := c.Namespaces().Delete(name); err != nil {
43
+		return err
44
+	}
45
+	_, err = cmd.Until(30*time.Second, w, func(event watch.Event) (bool, error) {
46
+		if event.Type != watch.Deleted {
47
+			return false, nil
48
+		}
49
+		namespace, ok := event.Object.(*kapi.Namespace)
50
+		if !ok {
51
+			return false, nil
52
+		}
53
+		return namespace.Name == name, nil
54
+	})
55
+	return err
56
+}
... ...
@@ -37,11 +37,10 @@ import (
37 37
 // controllers to start up, and populate the service accounts in the test namespace
38 38
 const ServiceAccountWaitTimeout = 30 * time.Second
39 39
 
40
-// RequireServer verifies if the etcd, docker and the OpenShift server are
41
-// available and you can successfully connected to them.
40
+// RequireServer verifies if the etcd and the OpenShift server are
41
+// available and you can successfully connect to them.
42 42
 func RequireServer(t *testing.T) {
43 43
 	util.RequireEtcd(t)
44
-	util.RequireDocker()
45 44
 	if _, err := util.GetClusterAdminClient(util.KubeConfigPath()); err != nil {
46 45
 		os.Exit(1)
47 46
 	}
... ...
@@ -138,6 +138,10 @@ func (o *DebugAPIServerOptions) ImportEtcdDump(etcdClientInfo configapi.EtcdConn
138 138
 	nodeList = append(nodeList, etcdDump.Node)
139 139
 	for i := 0; i < len(nodeList); i++ {
140 140
 		node := nodeList[i]
141
+		if node == nil {
142
+			continue
143
+		}
144
+
141 145
 		for j := range node.Nodes {
142 146
 			nodeList = append(nodeList, node.Nodes[j])
143 147
 		}