Commit 13ae9a4d authored by Eric Tune's avatar Eric Tune

Merge pull request #15520 from mikedanese/dne

Move job to generalized label selector
parents 5a1a0b30 a7a54cac
...@@ -1294,9 +1294,9 @@ func deepCopy_extensions_JobSpec(in JobSpec, out *JobSpec, c *conversion.Cloner) ...@@ -1294,9 +1294,9 @@ func deepCopy_extensions_JobSpec(in JobSpec, out *JobSpec, c *conversion.Cloner)
out.Completions = nil out.Completions = nil
} }
if in.Selector != nil { if in.Selector != nil {
out.Selector = make(map[string]string) out.Selector = new(PodSelector)
for key, val := range in.Selector { if err := deepCopy_extensions_PodSelector(*in.Selector, out.Selector, c); err != nil {
out.Selector[key] = val return err
} }
} else { } else {
out.Selector = nil out.Selector = nil
...@@ -1346,6 +1346,42 @@ func deepCopy_extensions_NodeUtilization(in NodeUtilization, out *NodeUtilizatio ...@@ -1346,6 +1346,42 @@ func deepCopy_extensions_NodeUtilization(in NodeUtilization, out *NodeUtilizatio
return nil return nil
} }
func deepCopy_extensions_PodSelector(in PodSelector, out *PodSelector, c *conversion.Cloner) error {
if in.MatchLabels != nil {
out.MatchLabels = make(map[string]string)
for key, val := range in.MatchLabels {
out.MatchLabels[key] = val
}
} else {
out.MatchLabels = nil
}
if in.MatchExpressions != nil {
out.MatchExpressions = make([]PodSelectorRequirement, len(in.MatchExpressions))
for i := range in.MatchExpressions {
if err := deepCopy_extensions_PodSelectorRequirement(in.MatchExpressions[i], &out.MatchExpressions[i], c); err != nil {
return err
}
}
} else {
out.MatchExpressions = nil
}
return nil
}
func deepCopy_extensions_PodSelectorRequirement(in PodSelectorRequirement, out *PodSelectorRequirement, c *conversion.Cloner) error {
out.Key = in.Key
out.Operator = in.Operator
if in.Values != nil {
out.Values = make([]string, len(in.Values))
for i := range in.Values {
out.Values[i] = in.Values[i]
}
} else {
out.Values = nil
}
return nil
}
func deepCopy_extensions_ReplicationControllerDummy(in ReplicationControllerDummy, out *ReplicationControllerDummy, c *conversion.Cloner) error { func deepCopy_extensions_ReplicationControllerDummy(in ReplicationControllerDummy, out *ReplicationControllerDummy, c *conversion.Cloner) error {
if err := deepCopy_unversioned_TypeMeta(in.TypeMeta, &out.TypeMeta, c); err != nil { if err := deepCopy_unversioned_TypeMeta(in.TypeMeta, &out.TypeMeta, c); err != nil {
return err return err
...@@ -1581,6 +1617,8 @@ func init() { ...@@ -1581,6 +1617,8 @@ func init() {
deepCopy_extensions_JobSpec, deepCopy_extensions_JobSpec,
deepCopy_extensions_JobStatus, deepCopy_extensions_JobStatus,
deepCopy_extensions_NodeUtilization, deepCopy_extensions_NodeUtilization,
deepCopy_extensions_PodSelector,
deepCopy_extensions_PodSelectorRequirement,
deepCopy_extensions_ReplicationControllerDummy, deepCopy_extensions_ReplicationControllerDummy,
deepCopy_extensions_ResourceConsumption, deepCopy_extensions_ResourceConsumption,
deepCopy_extensions_RollingUpdateDeployment, deepCopy_extensions_RollingUpdateDeployment,
......
/*
Copyright 2015 The Kubernetes Authors All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package extensions
import (
"fmt"
"sort"
"k8s.io/kubernetes/pkg/labels"
"k8s.io/kubernetes/pkg/util/sets"
)
// PodSelectorAsSelector converts the PodSelector api type into a struct that implements
// labels.Selector
func PodSelectorAsSelector(ps *PodSelector) (labels.Selector, error) {
if ps == nil {
return labels.Nothing(), nil
}
if len(ps.MatchLabels)+len(ps.MatchExpressions) == 0 {
return labels.Everything(), nil
}
selector := labels.LabelSelector{}
for k, v := range ps.MatchLabels {
req, err := labels.NewRequirement(k, labels.InOperator, sets.NewString(v))
if err != nil {
return nil, err
}
selector = append(selector, *req)
}
for _, expr := range ps.MatchExpressions {
var op labels.Operator
switch expr.Operator {
case PodSelectorOpIn:
op = labels.InOperator
case PodSelectorOpNotIn:
op = labels.NotInOperator
case PodSelectorOpExists:
op = labels.ExistsOperator
case PodSelectorOpDoesNotExist:
op = labels.DoesNotExistOperator
default:
return nil, fmt.Errorf("%q is not a valid pod selector operator", expr.Operator)
}
req, err := labels.NewRequirement(expr.Key, op, sets.NewString(expr.Values...))
if err != nil {
return nil, err
}
selector = append(selector, *req)
}
sort.Sort(labels.ByKey(selector))
return selector, nil
}
/*
Copyright 2015 The Kubernetes Authors All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package extensions
import (
"reflect"
"testing"
"k8s.io/kubernetes/pkg/labels"
)
func TestPodSelectorAsSelector(t *testing.T) {
matchLabels := map[string]string{"foo": "bar"}
matchExpressions := []PodSelectorRequirement{{
Key: "baz",
Operator: PodSelectorOpIn,
Values: []string{"qux", "norf"},
}}
mustParse := func(s string) labels.Selector {
out, e := labels.Parse(s)
if e != nil {
panic(e)
}
return out
}
tc := []struct {
in *PodSelector
out labels.Selector
expectErr bool
}{
{in: nil, out: labels.Nothing()},
{in: &PodSelector{}, out: labels.Everything()},
{
in: &PodSelector{MatchLabels: matchLabels},
out: mustParse("foo in (bar)"),
},
{
in: &PodSelector{MatchExpressions: matchExpressions},
out: mustParse("baz in (norf,qux)"),
},
{
in: &PodSelector{MatchLabels: matchLabels, MatchExpressions: matchExpressions},
out: mustParse("foo in (bar),baz in (norf,qux)"),
},
{
in: &PodSelector{
MatchExpressions: []PodSelectorRequirement{{
Key: "baz",
Operator: PodSelectorOpExists,
Values: []string{"qux", "norf"},
}},
},
expectErr: true,
},
}
for i, tc := range tc {
out, err := PodSelectorAsSelector(tc.in)
if err == nil && tc.expectErr {
t.Errorf("[%v]expected error but got none.", i)
}
if err != nil && !tc.expectErr {
t.Errorf("[%v]did not expect error but got: %v", i, err)
}
if !reflect.DeepEqual(out, tc.out) {
t.Errorf("[%v]expected:\n\t%+v\nbut got:\n\t%+v", i, tc.out, out)
}
}
}
...@@ -405,7 +405,7 @@ type JobSpec struct { ...@@ -405,7 +405,7 @@ type JobSpec struct {
Completions *int `json:"completions,omitempty"` Completions *int `json:"completions,omitempty"`
// Selector is a label query over pods that should match the pod count. // Selector is a label query over pods that should match the pod count.
Selector map[string]string `json:"selector"` Selector *PodSelector `json:"selector,omitempty"`
// Template is the object that describes the pod that will be created when // Template is the object that describes the pod that will be created when
// executing a job. // executing a job.
...@@ -661,7 +661,7 @@ type PodSelector struct { ...@@ -661,7 +661,7 @@ type PodSelector struct {
MatchExpressions []PodSelectorRequirement `json:"matchExpressions,omitempty"` MatchExpressions []PodSelectorRequirement `json:"matchExpressions,omitempty"`
} }
// A pod selector requirement is a selector that contains values, a key and an operator that // A pod selector requirement is a selector that contains values, a key, and an operator that
// relates the key and values. // relates the key and values.
type PodSelectorRequirement struct { type PodSelectorRequirement struct {
// key is the label key that the selector applies to. // key is the label key that the selector applies to.
...@@ -669,10 +669,11 @@ type PodSelectorRequirement struct { ...@@ -669,10 +669,11 @@ type PodSelectorRequirement struct {
// operator represents a key's relationship to a set of values. // operator represents a key's relationship to a set of values.
// Valid operators ard In, NotIn, Exists and DoesNotExist. // Valid operators ard In, NotIn, Exists and DoesNotExist.
Operator PodSelectorOperator `json:"operator"` Operator PodSelectorOperator `json:"operator"`
// values is a set of string values. If the operator is In or NotIn, // values is an array of string values. If the operator is In or NotIn,
// the values set must be non-empty. This array is replaced during a // the values array must be non-empty. If the operator is Exists or DoesNotExist,
// strategic merge patch. // the values array must be empty. This array is replaced during a strategic
Values []string `json:"stringValues,omitempty"` // merge patch.
Values []string `json:"values,omitempty"`
} }
// A pod selector operator is the set of operators that can be used in a selector requirement. // A pod selector operator is the set of operators that can be used in a selector requirement.
......
...@@ -2785,9 +2785,9 @@ func autoconvert_extensions_JobSpec_To_v1beta1_JobSpec(in *extensions.JobSpec, o ...@@ -2785,9 +2785,9 @@ func autoconvert_extensions_JobSpec_To_v1beta1_JobSpec(in *extensions.JobSpec, o
out.Completions = nil out.Completions = nil
} }
if in.Selector != nil { if in.Selector != nil {
out.Selector = make(map[string]string) out.Selector = new(PodSelector)
for key, val := range in.Selector { if err := convert_extensions_PodSelector_To_v1beta1_PodSelector(in.Selector, out.Selector, s); err != nil {
out.Selector[key] = val return err
} }
} else { } else {
out.Selector = nil out.Selector = nil
...@@ -2853,6 +2853,56 @@ func convert_extensions_NodeUtilization_To_v1beta1_NodeUtilization(in *extension ...@@ -2853,6 +2853,56 @@ func convert_extensions_NodeUtilization_To_v1beta1_NodeUtilization(in *extension
return autoconvert_extensions_NodeUtilization_To_v1beta1_NodeUtilization(in, out, s) return autoconvert_extensions_NodeUtilization_To_v1beta1_NodeUtilization(in, out, s)
} }
func autoconvert_extensions_PodSelector_To_v1beta1_PodSelector(in *extensions.PodSelector, out *PodSelector, s conversion.Scope) error {
if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found {
defaulting.(func(*extensions.PodSelector))(in)
}
if in.MatchLabels != nil {
out.MatchLabels = make(map[string]string)
for key, val := range in.MatchLabels {
out.MatchLabels[key] = val
}
} else {
out.MatchLabels = nil
}
if in.MatchExpressions != nil {
out.MatchExpressions = make([]PodSelectorRequirement, len(in.MatchExpressions))
for i := range in.MatchExpressions {
if err := convert_extensions_PodSelectorRequirement_To_v1beta1_PodSelectorRequirement(&in.MatchExpressions[i], &out.MatchExpressions[i], s); err != nil {
return err
}
}
} else {
out.MatchExpressions = nil
}
return nil
}
func convert_extensions_PodSelector_To_v1beta1_PodSelector(in *extensions.PodSelector, out *PodSelector, s conversion.Scope) error {
return autoconvert_extensions_PodSelector_To_v1beta1_PodSelector(in, out, s)
}
func autoconvert_extensions_PodSelectorRequirement_To_v1beta1_PodSelectorRequirement(in *extensions.PodSelectorRequirement, out *PodSelectorRequirement, s conversion.Scope) error {
if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found {
defaulting.(func(*extensions.PodSelectorRequirement))(in)
}
out.Key = in.Key
out.Operator = PodSelectorOperator(in.Operator)
if in.Values != nil {
out.Values = make([]string, len(in.Values))
for i := range in.Values {
out.Values[i] = in.Values[i]
}
} else {
out.Values = nil
}
return nil
}
func convert_extensions_PodSelectorRequirement_To_v1beta1_PodSelectorRequirement(in *extensions.PodSelectorRequirement, out *PodSelectorRequirement, s conversion.Scope) error {
return autoconvert_extensions_PodSelectorRequirement_To_v1beta1_PodSelectorRequirement(in, out, s)
}
func autoconvert_extensions_ReplicationControllerDummy_To_v1beta1_ReplicationControllerDummy(in *extensions.ReplicationControllerDummy, out *ReplicationControllerDummy, s conversion.Scope) error { func autoconvert_extensions_ReplicationControllerDummy_To_v1beta1_ReplicationControllerDummy(in *extensions.ReplicationControllerDummy, out *ReplicationControllerDummy, s conversion.Scope) error {
if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found {
defaulting.(func(*extensions.ReplicationControllerDummy))(in) defaulting.(func(*extensions.ReplicationControllerDummy))(in)
...@@ -3702,9 +3752,9 @@ func autoconvert_v1beta1_JobSpec_To_extensions_JobSpec(in *JobSpec, out *extensi ...@@ -3702,9 +3752,9 @@ func autoconvert_v1beta1_JobSpec_To_extensions_JobSpec(in *JobSpec, out *extensi
out.Completions = nil out.Completions = nil
} }
if in.Selector != nil { if in.Selector != nil {
out.Selector = make(map[string]string) out.Selector = new(extensions.PodSelector)
for key, val := range in.Selector { if err := convert_v1beta1_PodSelector_To_extensions_PodSelector(in.Selector, out.Selector, s); err != nil {
out.Selector[key] = val return err
} }
} else { } else {
out.Selector = nil out.Selector = nil
...@@ -3770,6 +3820,56 @@ func convert_v1beta1_NodeUtilization_To_extensions_NodeUtilization(in *NodeUtili ...@@ -3770,6 +3820,56 @@ func convert_v1beta1_NodeUtilization_To_extensions_NodeUtilization(in *NodeUtili
return autoconvert_v1beta1_NodeUtilization_To_extensions_NodeUtilization(in, out, s) return autoconvert_v1beta1_NodeUtilization_To_extensions_NodeUtilization(in, out, s)
} }
func autoconvert_v1beta1_PodSelector_To_extensions_PodSelector(in *PodSelector, out *extensions.PodSelector, s conversion.Scope) error {
if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found {
defaulting.(func(*PodSelector))(in)
}
if in.MatchLabels != nil {
out.MatchLabels = make(map[string]string)
for key, val := range in.MatchLabels {
out.MatchLabels[key] = val
}
} else {
out.MatchLabels = nil
}
if in.MatchExpressions != nil {
out.MatchExpressions = make([]extensions.PodSelectorRequirement, len(in.MatchExpressions))
for i := range in.MatchExpressions {
if err := convert_v1beta1_PodSelectorRequirement_To_extensions_PodSelectorRequirement(&in.MatchExpressions[i], &out.MatchExpressions[i], s); err != nil {
return err
}
}
} else {
out.MatchExpressions = nil
}
return nil
}
func convert_v1beta1_PodSelector_To_extensions_PodSelector(in *PodSelector, out *extensions.PodSelector, s conversion.Scope) error {
return autoconvert_v1beta1_PodSelector_To_extensions_PodSelector(in, out, s)
}
func autoconvert_v1beta1_PodSelectorRequirement_To_extensions_PodSelectorRequirement(in *PodSelectorRequirement, out *extensions.PodSelectorRequirement, s conversion.Scope) error {
if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found {
defaulting.(func(*PodSelectorRequirement))(in)
}
out.Key = in.Key
out.Operator = extensions.PodSelectorOperator(in.Operator)
if in.Values != nil {
out.Values = make([]string, len(in.Values))
for i := range in.Values {
out.Values[i] = in.Values[i]
}
} else {
out.Values = nil
}
return nil
}
func convert_v1beta1_PodSelectorRequirement_To_extensions_PodSelectorRequirement(in *PodSelectorRequirement, out *extensions.PodSelectorRequirement, s conversion.Scope) error {
return autoconvert_v1beta1_PodSelectorRequirement_To_extensions_PodSelectorRequirement(in, out, s)
}
func autoconvert_v1beta1_ReplicationControllerDummy_To_extensions_ReplicationControllerDummy(in *ReplicationControllerDummy, out *extensions.ReplicationControllerDummy, s conversion.Scope) error { func autoconvert_v1beta1_ReplicationControllerDummy_To_extensions_ReplicationControllerDummy(in *ReplicationControllerDummy, out *extensions.ReplicationControllerDummy, s conversion.Scope) error {
if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found {
defaulting.(func(*ReplicationControllerDummy))(in) defaulting.(func(*ReplicationControllerDummy))(in)
...@@ -4057,6 +4157,8 @@ func init() { ...@@ -4057,6 +4157,8 @@ func init() {
autoconvert_extensions_JobStatus_To_v1beta1_JobStatus, autoconvert_extensions_JobStatus_To_v1beta1_JobStatus,
autoconvert_extensions_Job_To_v1beta1_Job, autoconvert_extensions_Job_To_v1beta1_Job,
autoconvert_extensions_NodeUtilization_To_v1beta1_NodeUtilization, autoconvert_extensions_NodeUtilization_To_v1beta1_NodeUtilization,
autoconvert_extensions_PodSelectorRequirement_To_v1beta1_PodSelectorRequirement,
autoconvert_extensions_PodSelector_To_v1beta1_PodSelector,
autoconvert_extensions_ReplicationControllerDummy_To_v1beta1_ReplicationControllerDummy, autoconvert_extensions_ReplicationControllerDummy_To_v1beta1_ReplicationControllerDummy,
autoconvert_extensions_ResourceConsumption_To_v1beta1_ResourceConsumption, autoconvert_extensions_ResourceConsumption_To_v1beta1_ResourceConsumption,
autoconvert_extensions_RollingUpdateDeployment_To_v1beta1_RollingUpdateDeployment, autoconvert_extensions_RollingUpdateDeployment_To_v1beta1_RollingUpdateDeployment,
...@@ -4140,6 +4242,8 @@ func init() { ...@@ -4140,6 +4242,8 @@ func init() {
autoconvert_v1beta1_JobStatus_To_extensions_JobStatus, autoconvert_v1beta1_JobStatus_To_extensions_JobStatus,
autoconvert_v1beta1_Job_To_extensions_Job, autoconvert_v1beta1_Job_To_extensions_Job,
autoconvert_v1beta1_NodeUtilization_To_extensions_NodeUtilization, autoconvert_v1beta1_NodeUtilization_To_extensions_NodeUtilization,
autoconvert_v1beta1_PodSelectorRequirement_To_extensions_PodSelectorRequirement,
autoconvert_v1beta1_PodSelector_To_extensions_PodSelector,
autoconvert_v1beta1_ReplicationControllerDummy_To_extensions_ReplicationControllerDummy, autoconvert_v1beta1_ReplicationControllerDummy_To_extensions_ReplicationControllerDummy,
autoconvert_v1beta1_ResourceConsumption_To_extensions_ResourceConsumption, autoconvert_v1beta1_ResourceConsumption_To_extensions_ResourceConsumption,
autoconvert_v1beta1_RollingUpdateDeployment_To_extensions_RollingUpdateDeployment, autoconvert_v1beta1_RollingUpdateDeployment_To_extensions_RollingUpdateDeployment,
......
...@@ -1306,9 +1306,9 @@ func deepCopy_v1beta1_JobSpec(in JobSpec, out *JobSpec, c *conversion.Cloner) er ...@@ -1306,9 +1306,9 @@ func deepCopy_v1beta1_JobSpec(in JobSpec, out *JobSpec, c *conversion.Cloner) er
out.Completions = nil out.Completions = nil
} }
if in.Selector != nil { if in.Selector != nil {
out.Selector = make(map[string]string) out.Selector = new(PodSelector)
for key, val := range in.Selector { if err := deepCopy_v1beta1_PodSelector(*in.Selector, out.Selector, c); err != nil {
out.Selector[key] = val return err
} }
} else { } else {
out.Selector = nil out.Selector = nil
...@@ -1358,6 +1358,42 @@ func deepCopy_v1beta1_NodeUtilization(in NodeUtilization, out *NodeUtilization, ...@@ -1358,6 +1358,42 @@ func deepCopy_v1beta1_NodeUtilization(in NodeUtilization, out *NodeUtilization,
return nil return nil
} }
func deepCopy_v1beta1_PodSelector(in PodSelector, out *PodSelector, c *conversion.Cloner) error {
if in.MatchLabels != nil {
out.MatchLabels = make(map[string]string)
for key, val := range in.MatchLabels {
out.MatchLabels[key] = val
}
} else {
out.MatchLabels = nil
}
if in.MatchExpressions != nil {
out.MatchExpressions = make([]PodSelectorRequirement, len(in.MatchExpressions))
for i := range in.MatchExpressions {
if err := deepCopy_v1beta1_PodSelectorRequirement(in.MatchExpressions[i], &out.MatchExpressions[i], c); err != nil {
return err
}
}
} else {
out.MatchExpressions = nil
}
return nil
}
func deepCopy_v1beta1_PodSelectorRequirement(in PodSelectorRequirement, out *PodSelectorRequirement, c *conversion.Cloner) error {
out.Key = in.Key
out.Operator = in.Operator
if in.Values != nil {
out.Values = make([]string, len(in.Values))
for i := range in.Values {
out.Values[i] = in.Values[i]
}
} else {
out.Values = nil
}
return nil
}
func deepCopy_v1beta1_ReplicationControllerDummy(in ReplicationControllerDummy, out *ReplicationControllerDummy, c *conversion.Cloner) error { func deepCopy_v1beta1_ReplicationControllerDummy(in ReplicationControllerDummy, out *ReplicationControllerDummy, c *conversion.Cloner) error {
if err := deepCopy_unversioned_TypeMeta(in.TypeMeta, &out.TypeMeta, c); err != nil { if err := deepCopy_unversioned_TypeMeta(in.TypeMeta, &out.TypeMeta, c); err != nil {
return err return err
...@@ -1603,6 +1639,8 @@ func init() { ...@@ -1603,6 +1639,8 @@ func init() {
deepCopy_v1beta1_JobSpec, deepCopy_v1beta1_JobSpec,
deepCopy_v1beta1_JobStatus, deepCopy_v1beta1_JobStatus,
deepCopy_v1beta1_NodeUtilization, deepCopy_v1beta1_NodeUtilization,
deepCopy_v1beta1_PodSelector,
deepCopy_v1beta1_PodSelectorRequirement,
deepCopy_v1beta1_ReplicationControllerDummy, deepCopy_v1beta1_ReplicationControllerDummy,
deepCopy_v1beta1_ResourceConsumption, deepCopy_v1beta1_ResourceConsumption,
deepCopy_v1beta1_RollingUpdateDeployment, deepCopy_v1beta1_RollingUpdateDeployment,
......
...@@ -92,8 +92,10 @@ func addDefaultingFuncs() { ...@@ -92,8 +92,10 @@ func addDefaultingFuncs() {
labels := obj.Spec.Template.Labels labels := obj.Spec.Template.Labels
// TODO: support templates defined elsewhere when we support them in the API // TODO: support templates defined elsewhere when we support them in the API
if labels != nil { if labels != nil {
if len(obj.Spec.Selector) == 0 { if obj.Spec.Selector == nil {
obj.Spec.Selector = labels obj.Spec.Selector = &PodSelector{
MatchLabels: labels,
}
} }
if len(obj.Labels) == 0 { if len(obj.Labels) == 0 {
obj.Labels = labels obj.Labels = labels
......
...@@ -192,7 +192,9 @@ func TestSetDefaultDeployment(t *testing.T) { ...@@ -192,7 +192,9 @@ func TestSetDefaultDeployment(t *testing.T) {
func TestSetDefaultJob(t *testing.T) { func TestSetDefaultJob(t *testing.T) {
expected := &Job{ expected := &Job{
Spec: JobSpec{ Spec: JobSpec{
Selector: map[string]string{"job": "selector"}, Selector: &PodSelector{
MatchLabels: map[string]string{"job": "selector"},
},
Completions: newInt(1), Completions: newInt(1),
Parallelism: newInt(1), Parallelism: newInt(1),
}, },
...@@ -201,7 +203,9 @@ func TestSetDefaultJob(t *testing.T) { ...@@ -201,7 +203,9 @@ func TestSetDefaultJob(t *testing.T) {
// selector set explicitly, completions and parallelism - default // selector set explicitly, completions and parallelism - default
{ {
Spec: JobSpec{ Spec: JobSpec{
Selector: map[string]string{"job": "selector"}, Selector: &PodSelector{
MatchLabels: map[string]string{"job": "selector"},
},
}, },
}, },
// selector from template labels, completions and parallelism - default // selector from template labels, completions and parallelism - default
......
...@@ -412,7 +412,7 @@ type JobSpec struct { ...@@ -412,7 +412,7 @@ type JobSpec struct {
// Selector is a label query over pods that should match the pod count. // Selector is a label query over pods that should match the pod count.
// More info: http://releases.k8s.io/HEAD/docs/user-guide/labels.md#label-selectors // More info: http://releases.k8s.io/HEAD/docs/user-guide/labels.md#label-selectors
Selector map[string]string `json:"selector,omitempty"` Selector *PodSelector `json:"selector,omitempty"`
// Template is the object that describes the pod that will be created when // Template is the object that describes the pod that will be created when
// executing a job. // executing a job.
...@@ -657,3 +657,40 @@ type ClusterAutoscalerList struct { ...@@ -657,3 +657,40 @@ type ClusterAutoscalerList struct {
Items []ClusterAutoscaler `json:"items"` Items []ClusterAutoscaler `json:"items"`
} }
// A pod selector is a label query over a set of pods. The result of matchLabels and
// matchExpressions are ANDed. An empty pod selector matches all objects. A null
// pod selector matches no objects.
type PodSelector struct {
// matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels
// map is equivalent to an element of matchExpressions, whose key field is "key", the
// operator is "In", and the values array contains only "value". The requirements are ANDed.
MatchLabels map[string]string `json:"matchLabels,omitempty"`
// matchExpressions is a list of pod selector requirements. The requirements are ANDed.
MatchExpressions []PodSelectorRequirement `json:"matchExpressions,omitempty"`
}
// A pod selector requirement is a selector that contains values, a key, and an operator that
// relates the key and values.
type PodSelectorRequirement struct {
// key is the label key that the selector applies to.
Key string `json:"key" patchStrategy:"merge" patchMergeKey:"key"`
// operator represents a key's relationship to a set of values.
// Valid operators ard In, NotIn, Exists and DoesNotExist.
Operator PodSelectorOperator `json:"operator"`
// values is an array of string values. If the operator is In or NotIn,
// the values array must be non-empty. If the operator is Exists or DoesNotExist,
// the values array must be empty. This array is replaced during a strategic
// merge patch.
Values []string `json:"values,omitempty"`
}
// A pod selector operator is the set of operators that can be used in a selector requirement.
type PodSelectorOperator string
const (
PodSelectorOpIn PodSelectorOperator = "In"
PodSelectorOpNotIn PodSelectorOperator = "NotIn"
PodSelectorOpExists PodSelectorOperator = "Exists"
PodSelectorOpDoesNotExist PodSelectorOperator = "DoesNotExist"
)
...@@ -363,6 +363,27 @@ func (NodeUtilization) SwaggerDoc() map[string]string { ...@@ -363,6 +363,27 @@ func (NodeUtilization) SwaggerDoc() map[string]string {
return map_NodeUtilization return map_NodeUtilization
} }
var map_PodSelector = map[string]string{
"": "A pod selector is a label query over a set of pods. The result of matchLabels and matchExpressions are ANDed. An empty pod selector matches all objects. A null pod selector matches no objects.",
"matchLabels": "matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is \"key\", the operator is \"In\", and the values array contains only \"value\". The requirements are ANDed.",
"matchExpressions": "matchExpressions is a list of pod selector requirements. The requirements are ANDed.",
}
func (PodSelector) SwaggerDoc() map[string]string {
return map_PodSelector
}
var map_PodSelectorRequirement = map[string]string{
"": "A pod selector requirement is a selector that contains values, a key, and an operator that relates the key and values.",
"key": "key is the label key that the selector applies to.",
"operator": "operator represents a key's relationship to a set of values. Valid operators ard In, NotIn, Exists and DoesNotExist.",
"values": "values is an array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. This array is replaced during a strategic merge patch.",
}
func (PodSelectorRequirement) SwaggerDoc() map[string]string {
return map_PodSelectorRequirement
}
var map_ReplicationControllerDummy = map[string]string{ var map_ReplicationControllerDummy = map[string]string{
"": "Dummy definition", "": "Dummy definition",
} }
......
...@@ -345,16 +345,19 @@ func ValidateJobSpec(spec *extensions.JobSpec) errs.ValidationErrorList { ...@@ -345,16 +345,19 @@ func ValidateJobSpec(spec *extensions.JobSpec) errs.ValidationErrorList {
if spec.Completions != nil && *spec.Completions < 0 { if spec.Completions != nil && *spec.Completions < 0 {
allErrs = append(allErrs, errs.NewFieldInvalid("completions", spec.Completions, isNegativeErrorMsg)) allErrs = append(allErrs, errs.NewFieldInvalid("completions", spec.Completions, isNegativeErrorMsg))
} }
if spec.Selector == nil {
selector := labels.Set(spec.Selector).AsSelector()
if selector.Empty() {
allErrs = append(allErrs, errs.NewFieldRequired("selector")) allErrs = append(allErrs, errs.NewFieldRequired("selector"))
} else {
allErrs = append(allErrs, ValidatePodSelector(spec.Selector).Prefix("selector")...)
} }
labels := labels.Set(spec.Template.Labels) if selector, err := extensions.PodSelectorAsSelector(spec.Selector); err == nil {
if !selector.Matches(labels) { labels := labels.Set(spec.Template.Labels)
allErrs = append(allErrs, errs.NewFieldInvalid("template.labels", spec.Template.Labels, "selector does not match template")) if !selector.Matches(labels) {
allErrs = append(allErrs, errs.NewFieldInvalid("template.metadata.labels", spec.Template.Labels, "selector does not match template"))
}
} }
allErrs = append(allErrs, apivalidation.ValidatePodTemplateSpec(&spec.Template).Prefix("template")...) allErrs = append(allErrs, apivalidation.ValidatePodTemplateSpec(&spec.Template).Prefix("template")...)
if spec.Template.Spec.RestartPolicy != api.RestartPolicyOnFailure && if spec.Template.Spec.RestartPolicy != api.RestartPolicyOnFailure &&
spec.Template.Spec.RestartPolicy != api.RestartPolicyNever { spec.Template.Spec.RestartPolicy != api.RestartPolicyNever {
...@@ -568,3 +571,33 @@ func ValidateClusterAutoscaler(autoscaler *extensions.ClusterAutoscaler) errs.Va ...@@ -568,3 +571,33 @@ func ValidateClusterAutoscaler(autoscaler *extensions.ClusterAutoscaler) errs.Va
allErrs = append(allErrs, validateClusterAutoscalerSpec(autoscaler.Spec)...) allErrs = append(allErrs, validateClusterAutoscalerSpec(autoscaler.Spec)...)
return allErrs return allErrs
} }
func ValidatePodSelector(ps *extensions.PodSelector) errs.ValidationErrorList {
allErrs := errs.ValidationErrorList{}
if ps == nil {
return allErrs
}
allErrs = append(allErrs, apivalidation.ValidateLabels(ps.MatchLabels, "matchLabels")...)
for i, expr := range ps.MatchExpressions {
allErrs = append(allErrs, ValidatePodSelectorRequirement(expr).Prefix(fmt.Sprintf("matchExpressions.[%v]", i))...)
}
return allErrs
}
func ValidatePodSelectorRequirement(sr extensions.PodSelectorRequirement) errs.ValidationErrorList {
allErrs := errs.ValidationErrorList{}
switch sr.Operator {
case extensions.PodSelectorOpIn, extensions.PodSelectorOpNotIn:
if len(sr.Values) == 0 {
allErrs = append(allErrs, errs.NewFieldInvalid("values", sr.Values, "must be non-empty when operator is In or NotIn"))
}
case extensions.PodSelectorOpExists, extensions.PodSelectorOpDoesNotExist:
if len(sr.Values) > 0 {
allErrs = append(allErrs, errs.NewFieldInvalid("values", sr.Values, "must be empty when operator is Exists or DoesNotExist"))
}
default:
allErrs = append(allErrs, errs.NewFieldInvalid("operator", sr.Operator, "not a valid pod selector operator"))
}
allErrs = append(allErrs, apivalidation.ValidateLabelName(sr.Key, "key")...)
return allErrs
}
...@@ -192,7 +192,6 @@ func TestValidateDaemonSetStatusUpdate(t *testing.T) { ...@@ -192,7 +192,6 @@ func TestValidateDaemonSetStatusUpdate(t *testing.T) {
t.Errorf("expected failure: %s", testName) t.Errorf("expected failure: %s", testName)
} }
} }
} }
func TestValidateDaemonSetUpdate(t *testing.T) { func TestValidateDaemonSetUpdate(t *testing.T) {
...@@ -725,10 +724,12 @@ func TestValidateDeployment(t *testing.T) { ...@@ -725,10 +724,12 @@ func TestValidateDeployment(t *testing.T) {
} }
func TestValidateJob(t *testing.T) { func TestValidateJob(t *testing.T) {
validSelector := map[string]string{"a": "b"} validSelector := &extensions.PodSelector{
MatchLabels: map[string]string{"a": "b"},
}
validPodTemplateSpec := api.PodTemplateSpec{ validPodTemplateSpec := api.PodTemplateSpec{
ObjectMeta: api.ObjectMeta{ ObjectMeta: api.ObjectMeta{
Labels: validSelector, Labels: validSelector.MatchLabels,
}, },
Spec: api.PodSpec{ Spec: api.PodSpec{
RestartPolicy: api.RestartPolicyOnFailure, RestartPolicy: api.RestartPolicyOnFailure,
...@@ -783,11 +784,10 @@ func TestValidateJob(t *testing.T) { ...@@ -783,11 +784,10 @@ func TestValidateJob(t *testing.T) {
Namespace: api.NamespaceDefault, Namespace: api.NamespaceDefault,
}, },
Spec: extensions.JobSpec{ Spec: extensions.JobSpec{
Selector: map[string]string{},
Template: validPodTemplateSpec, Template: validPodTemplateSpec,
}, },
}, },
"spec.template.labels:selector does not match template": { "spec.template.metadata.labels: invalid value 'map[y:z]', Details: selector does not match template": {
ObjectMeta: api.ObjectMeta{ ObjectMeta: api.ObjectMeta{
Name: "myjob", Name: "myjob",
Namespace: api.NamespaceDefault, Namespace: api.NamespaceDefault,
...@@ -815,7 +815,7 @@ func TestValidateJob(t *testing.T) { ...@@ -815,7 +815,7 @@ func TestValidateJob(t *testing.T) {
Selector: validSelector, Selector: validSelector,
Template: api.PodTemplateSpec{ Template: api.PodTemplateSpec{
ObjectMeta: api.ObjectMeta{ ObjectMeta: api.ObjectMeta{
Labels: validSelector, Labels: validSelector.MatchLabels,
}, },
Spec: api.PodSpec{ Spec: api.PodSpec{
RestartPolicy: api.RestartPolicyAlways, RestartPolicy: api.RestartPolicyAlways,
......
...@@ -382,11 +382,9 @@ func (s *StoreToJobLister) GetPodJobs(pod *api.Pod) (jobs []extensions.Job, err ...@@ -382,11 +382,9 @@ func (s *StoreToJobLister) GetPodJobs(pod *api.Pod) (jobs []extensions.Job, err
if job.Namespace != pod.Namespace { if job.Namespace != pod.Namespace {
continue continue
} }
labelSet := labels.Set(job.Spec.Selector)
selector = labels.Set(job.Spec.Selector).AsSelector()
// Job with a nil or empty selector match nothing selector, _ = extensions.PodSelectorAsSelector(job.Spec.Selector)
if labelSet.AsSelector().Empty() || !selector.Matches(labels.Set(pod.Labels)) { if !selector.Matches(labels.Set(pod.Labels)) {
continue continue
} }
jobs = append(jobs, job) jobs = append(jobs, job)
......
...@@ -313,7 +313,8 @@ func (jm *JobController) syncJob(key string) error { ...@@ -313,7 +313,8 @@ func (jm *JobController) syncJob(key string) error {
return err return err
} }
jobNeedsSync := jm.expectations.SatisfiedExpectations(jobKey) jobNeedsSync := jm.expectations.SatisfiedExpectations(jobKey)
podList, err := jm.podStore.Pods(job.Namespace).List(labels.Set(job.Spec.Selector).AsSelector()) selector, _ := extensions.PodSelectorAsSelector(job.Spec.Selector)
podList, err := jm.podStore.Pods(job.Namespace).List(selector)
if err != nil { if err != nil {
glog.Errorf("Error getting pods for job %q: %v", key, err) glog.Errorf("Error getting pods for job %q: %v", key, err)
jm.queue.Add(key) jm.queue.Add(key)
......
...@@ -43,7 +43,9 @@ func newJob(parallelism, completions int) *extensions.Job { ...@@ -43,7 +43,9 @@ func newJob(parallelism, completions int) *extensions.Job {
Spec: extensions.JobSpec{ Spec: extensions.JobSpec{
Parallelism: &parallelism, Parallelism: &parallelism,
Completions: &completions, Completions: &completions,
Selector: map[string]string{"foo": "bar"}, Selector: &extensions.PodSelector{
MatchLabels: map[string]string{"foo": "bar"},
},
Template: api.PodTemplateSpec{ Template: api.PodTemplateSpec{
ObjectMeta: api.ObjectMeta{ ObjectMeta: api.ObjectMeta{
Labels: map[string]string{ Labels: map[string]string{
...@@ -76,7 +78,7 @@ func newPodList(count int, status api.PodPhase, job *extensions.Job) []api.Pod { ...@@ -76,7 +78,7 @@ func newPodList(count int, status api.PodPhase, job *extensions.Job) []api.Pod {
newPod := api.Pod{ newPod := api.Pod{
ObjectMeta: api.ObjectMeta{ ObjectMeta: api.ObjectMeta{
Name: fmt.Sprintf("pod-%v", unversioned.Now().UnixNano()), Name: fmt.Sprintf("pod-%v", unversioned.Now().UnixNano()),
Labels: job.Spec.Selector, Labels: job.Spec.Selector.MatchLabels,
Namespace: job.Namespace, Namespace: job.Namespace,
}, },
Status: api.PodStatus{Phase: status}, Status: api.PodStatus{Phase: status},
...@@ -289,7 +291,9 @@ func TestJobPodLookup(t *testing.T) { ...@@ -289,7 +291,9 @@ func TestJobPodLookup(t *testing.T) {
job: &extensions.Job{ job: &extensions.Job{
ObjectMeta: api.ObjectMeta{Name: "foo"}, ObjectMeta: api.ObjectMeta{Name: "foo"},
Spec: extensions.JobSpec{ Spec: extensions.JobSpec{
Selector: map[string]string{"foo": "bar"}, Selector: &extensions.PodSelector{
MatchLabels: map[string]string{"foo": "bar"},
},
}, },
}, },
pod: &api.Pod{ pod: &api.Pod{
...@@ -306,7 +310,15 @@ func TestJobPodLookup(t *testing.T) { ...@@ -306,7 +310,15 @@ func TestJobPodLookup(t *testing.T) {
job: &extensions.Job{ job: &extensions.Job{
ObjectMeta: api.ObjectMeta{Name: "bar", Namespace: "ns"}, ObjectMeta: api.ObjectMeta{Name: "bar", Namespace: "ns"},
Spec: extensions.JobSpec{ Spec: extensions.JobSpec{
Selector: map[string]string{"foo": "bar"}, Selector: &extensions.PodSelector{
MatchExpressions: []extensions.PodSelectorRequirement{
{
Key: "foo",
Operator: extensions.PodSelectorOpIn,
Values: []string{"bar"},
},
},
},
}, },
}, },
pod: &api.Pod{ pod: &api.Pod{
......
...@@ -885,7 +885,8 @@ func describeJob(job *extensions.Job, events *api.EventList) (string, error) { ...@@ -885,7 +885,8 @@ func describeJob(job *extensions.Job, events *api.EventList) (string, error) {
fmt.Fprintf(out, "Name:\t%s\n", job.Name) fmt.Fprintf(out, "Name:\t%s\n", job.Name)
fmt.Fprintf(out, "Namespace:\t%s\n", job.Namespace) fmt.Fprintf(out, "Namespace:\t%s\n", job.Namespace)
fmt.Fprintf(out, "Image(s):\t%s\n", makeImageList(&job.Spec.Template.Spec)) fmt.Fprintf(out, "Image(s):\t%s\n", makeImageList(&job.Spec.Template.Spec))
fmt.Fprintf(out, "Selector:\t%s\n", labels.FormatLabels(job.Spec.Selector)) selector, _ := extensions.PodSelectorAsSelector(job.Spec.Selector)
fmt.Fprintf(out, "Selector:\t%s\n", selector)
fmt.Fprintf(out, "Parallelism:\t%d\n", *job.Spec.Parallelism) fmt.Fprintf(out, "Parallelism:\t%d\n", *job.Spec.Parallelism)
fmt.Fprintf(out, "Completions:\t%d\n", *job.Spec.Completions) fmt.Fprintf(out, "Completions:\t%d\n", *job.Spec.Completions)
fmt.Fprintf(out, "Labels:\t%s\n", labels.FormatLabels(job.Labels)) fmt.Fprintf(out, "Labels:\t%s\n", labels.FormatLabels(job.Labels))
......
...@@ -736,11 +736,13 @@ func printJob(job *extensions.Job, w io.Writer, withNamespace bool, wide bool, s ...@@ -736,11 +736,13 @@ func printJob(job *extensions.Job, w io.Writer, withNamespace bool, wide bool, s
return err return err
} }
} }
selector, _ := extensions.PodSelectorAsSelector(job.Spec.Selector)
_, err := fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%d\n", _, err := fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%d\n",
name, name,
firstContainer.Name, firstContainer.Name,
firstContainer.Image, firstContainer.Image,
labels.FormatLabels(job.Spec.Selector), selector.String(),
job.Status.Succeeded) job.Status.Succeeded)
if err != nil { if err != nil {
return err return err
......
...@@ -293,7 +293,10 @@ func TestJobStop(t *testing.T) { ...@@ -293,7 +293,10 @@ func TestJobStop(t *testing.T) {
}, },
Spec: extensions.JobSpec{ Spec: extensions.JobSpec{
Parallelism: &zero, Parallelism: &zero,
Selector: map[string]string{"k1": "v1"}}, Selector: &extensions.PodSelector{
MatchLabels: map[string]string{"k1": "v1"},
},
},
}, },
&extensions.JobList{ // LIST &extensions.JobList{ // LIST
Items: []extensions.Job{ Items: []extensions.Job{
...@@ -304,7 +307,10 @@ func TestJobStop(t *testing.T) { ...@@ -304,7 +307,10 @@ func TestJobStop(t *testing.T) {
}, },
Spec: extensions.JobSpec{ Spec: extensions.JobSpec{
Parallelism: &zero, Parallelism: &zero,
Selector: map[string]string{"k1": "v1"}}, Selector: &extensions.PodSelector{
MatchLabels: map[string]string{"k1": "v1"},
},
},
}, },
}, },
}, },
......
...@@ -47,6 +47,18 @@ func Everything() Selector { ...@@ -47,6 +47,18 @@ func Everything() Selector {
return LabelSelector{} return LabelSelector{}
} }
type nothingSelector struct{}
func (n nothingSelector) Matches(_ Labels) bool { return false }
func (n nothingSelector) Empty() bool { return false }
func (n nothingSelector) String() string { return "<null>" }
func (n nothingSelector) Add(_ string, _ Operator, _ []string) Selector { return n }
// Nothing returns a selector that matches no labels
func Nothing() Selector {
return nothingSelector{}
}
// Operator represents a key's relationship // Operator represents a key's relationship
// to a set of values in a Requirement. // to a set of values in a Requirement.
type Operator string type Operator string
......
...@@ -47,7 +47,9 @@ func validNewJob() *extensions.Job { ...@@ -47,7 +47,9 @@ func validNewJob() *extensions.Job {
Spec: extensions.JobSpec{ Spec: extensions.JobSpec{
Completions: &completions, Completions: &completions,
Parallelism: &parallelism, Parallelism: &parallelism,
Selector: map[string]string{"a": "b"}, Selector: &extensions.PodSelector{
MatchLabels: map[string]string{"a": "b"},
},
Template: api.PodTemplateSpec{ Template: api.PodTemplateSpec{
ObjectMeta: api.ObjectMeta{ ObjectMeta: api.ObjectMeta{
Labels: map[string]string{"a": "b"}, Labels: map[string]string{"a": "b"},
...@@ -80,7 +82,7 @@ func TestCreate(t *testing.T) { ...@@ -80,7 +82,7 @@ func TestCreate(t *testing.T) {
&extensions.Job{ &extensions.Job{
Spec: extensions.JobSpec{ Spec: extensions.JobSpec{
Completions: validJob.Spec.Completions, Completions: validJob.Spec.Completions,
Selector: map[string]string{}, Selector: &extensions.PodSelector{},
Template: validJob.Spec.Template, Template: validJob.Spec.Template,
}, },
}, },
...@@ -103,7 +105,7 @@ func TestUpdate(t *testing.T) { ...@@ -103,7 +105,7 @@ func TestUpdate(t *testing.T) {
// invalid updateFunc // invalid updateFunc
func(obj runtime.Object) runtime.Object { func(obj runtime.Object) runtime.Object {
object := obj.(*extensions.Job) object := obj.(*extensions.Job)
object.Spec.Selector = map[string]string{} object.Spec.Selector = &extensions.PodSelector{}
return object return object
}, },
func(obj runtime.Object) runtime.Object { func(obj runtime.Object) runtime.Object {
......
...@@ -32,10 +32,12 @@ func TestJobStrategy(t *testing.T) { ...@@ -32,10 +32,12 @@ func TestJobStrategy(t *testing.T) {
t.Errorf("Job should not allow create on update") t.Errorf("Job should not allow create on update")
} }
validSelector := map[string]string{"a": "b"} validSelector := &extensions.PodSelector{
MatchLabels: map[string]string{"a": "b"},
}
validPodTemplateSpec := api.PodTemplateSpec{ validPodTemplateSpec := api.PodTemplateSpec{
ObjectMeta: api.ObjectMeta{ ObjectMeta: api.ObjectMeta{
Labels: validSelector, Labels: validSelector.MatchLabels,
}, },
Spec: api.PodSpec{ Spec: api.PodSpec{
RestartPolicy: api.RestartPolicyOnFailure, RestartPolicy: api.RestartPolicyOnFailure,
...@@ -95,10 +97,12 @@ func TestJobStatusStrategy(t *testing.T) { ...@@ -95,10 +97,12 @@ func TestJobStatusStrategy(t *testing.T) {
if StatusStrategy.AllowCreateOnUpdate() { if StatusStrategy.AllowCreateOnUpdate() {
t.Errorf("Job should not allow create on update") t.Errorf("Job should not allow create on update")
} }
validSelector := map[string]string{"a": "b"} validSelector := &extensions.PodSelector{
MatchLabels: map[string]string{"a": "b"},
}
validPodTemplateSpec := api.PodTemplateSpec{ validPodTemplateSpec := api.PodTemplateSpec{
ObjectMeta: api.ObjectMeta{ ObjectMeta: api.ObjectMeta{
Labels: validSelector, Labels: validSelector.MatchLabels,
}, },
Spec: api.PodSpec{ Spec: api.PodSpec{
RestartPolicy: api.RestartPolicyOnFailure, RestartPolicy: api.RestartPolicyOnFailure,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment