| ... | ... |
@@ -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": {
|
| ... | ... |
@@ -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 |
|
| ... | ... |
@@ -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> |
| ... | ... |
@@ -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,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" 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 ? "&" :"<" == a ? "<" :">" == a ? ">" :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 = "a |
|
| 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 = "a |
|
| 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 |
} |