diff --git a/pkg/api/deep_copy_generated.go b/pkg/api/deep_copy_generated.go index 2a4d4d80ca9..5044b286574 100644 --- a/pkg/api/deep_copy_generated.go +++ b/pkg/api/deep_copy_generated.go @@ -289,6 +289,69 @@ func deepCopy_api_ContainerStatus(in ContainerStatus, out *ContainerStatus, c *c return nil } +func deepCopy_api_Daemon(in Daemon, out *Daemon, c *conversion.Cloner) error { + if err := deepCopy_api_TypeMeta(in.TypeMeta, &out.TypeMeta, c); err != nil { + return err + } + if err := deepCopy_api_ObjectMeta(in.ObjectMeta, &out.ObjectMeta, c); err != nil { + return err + } + if err := deepCopy_api_DaemonSpec(in.Spec, &out.Spec, c); err != nil { + return err + } + if err := deepCopy_api_DaemonStatus(in.Status, &out.Status, c); err != nil { + return err + } + return nil +} + +func deepCopy_api_DaemonList(in DaemonList, out *DaemonList, c *conversion.Cloner) error { + if err := deepCopy_api_TypeMeta(in.TypeMeta, &out.TypeMeta, c); err != nil { + return err + } + if err := deepCopy_api_ListMeta(in.ListMeta, &out.ListMeta, c); err != nil { + return err + } + if in.Items != nil { + out.Items = make([]Daemon, len(in.Items)) + for i := range in.Items { + if err := deepCopy_api_Daemon(in.Items[i], &out.Items[i], c); err != nil { + return err + } + } + } else { + out.Items = nil + } + return nil +} + +func deepCopy_api_DaemonSpec(in DaemonSpec, out *DaemonSpec, c *conversion.Cloner) error { + if in.Selector != nil { + out.Selector = make(map[string]string) + for key, val := range in.Selector { + out.Selector[key] = val + } + } else { + out.Selector = nil + } + if in.Template != nil { + out.Template = new(PodTemplateSpec) + if err := deepCopy_api_PodTemplateSpec(*in.Template, out.Template, c); err != nil { + return err + } + } else { + out.Template = nil + } + return nil +} + +func deepCopy_api_DaemonStatus(in DaemonStatus, out *DaemonStatus, c *conversion.Cloner) error { + out.CurrentNumberScheduled = in.CurrentNumberScheduled + out.NumberMisscheduled = in.NumberMisscheduled + out.DesiredNumberScheduled = in.DesiredNumberScheduled + return nil +} + func deepCopy_api_DeleteOptions(in DeleteOptions, out *DeleteOptions, c *conversion.Cloner) error { if err := deepCopy_api_TypeMeta(in.TypeMeta, &out.TypeMeta, c); err != nil { return err @@ -2101,6 +2164,10 @@ func init() { deepCopy_api_ContainerStateTerminated, deepCopy_api_ContainerStateWaiting, deepCopy_api_ContainerStatus, + deepCopy_api_Daemon, + deepCopy_api_DaemonList, + deepCopy_api_DaemonSpec, + deepCopy_api_DaemonStatus, deepCopy_api_DeleteOptions, deepCopy_api_EmptyDirVolumeSource, deepCopy_api_EndpointAddress, diff --git a/pkg/api/helpers.go b/pkg/api/helpers.go index d9e83156d63..e3100e46b4e 100644 --- a/pkg/api/helpers.go +++ b/pkg/api/helpers.go @@ -83,6 +83,7 @@ var standardResources = util.NewStringSet( string(ResourceQuotas), string(ResourceServices), string(ResourceReplicationControllers), + string(ResourceDaemon), string(ResourceSecrets), string(ResourcePersistentVolumeClaims), string(ResourceStorage)) diff --git a/pkg/api/latest/latest.go b/pkg/api/latest/latest.go index 6bbcc210b8c..81428fec3f8 100644 --- a/pkg/api/latest/latest.go +++ b/pkg/api/latest/latest.go @@ -94,7 +94,8 @@ func init() { "PodLogOptions", "PodExecOptions", "PodAttachOptions", - "PodProxyOptions") + "PodProxyOptions", + "Daemon") mapper := api.NewDefaultRESTMapper(versions, InterfacesFor, importPrefix, ignoredKinds, rootScoped) // setup aliases for groups of resources diff --git a/pkg/api/register.go b/pkg/api/register.go index d53f35b963c..8029ec64075 100644 --- a/pkg/api/register.go +++ b/pkg/api/register.go @@ -32,6 +32,8 @@ func init() { &PodTemplateList{}, &ReplicationControllerList{}, &ReplicationController{}, + &DaemonList{}, + &Daemon{}, &ServiceList{}, &Service{}, &NodeList{}, @@ -80,6 +82,8 @@ func (*PodTemplate) IsAnAPIObject() {} func (*PodTemplateList) IsAnAPIObject() {} func (*ReplicationController) IsAnAPIObject() {} func (*ReplicationControllerList) IsAnAPIObject() {} +func (*Daemon) IsAnAPIObject() {} +func (*DaemonList) IsAnAPIObject() {} func (*Service) IsAnAPIObject() {} func (*ServiceList) IsAnAPIObject() {} func (*Endpoints) IsAnAPIObject() {} diff --git a/pkg/api/testing/fuzzer.go b/pkg/api/testing/fuzzer.go index 9c39a51c488..f99eb0490c8 100644 --- a/pkg/api/testing/fuzzer.go +++ b/pkg/api/testing/fuzzer.go @@ -110,9 +110,8 @@ func FuzzerFor(t *testing.T, version string, src rand.Source) *fuzz.Fuzzer { c.FuzzNoCustom(j) // fuzz self without calling this function again //j.TemplateRef = nil // this is required for round trip }, - func(j *api.ReplicationControllerStatus, c fuzz.Continue) { - // only replicas round trips - j.Replicas = int(c.RandUint64()) + func(j *api.DaemonSpec, c fuzz.Continue) { + c.FuzzNoCustom(j) // fuzz self without calling this function again }, func(j *api.List, c fuzz.Continue) { c.FuzzNoCustom(j) // fuzz self without calling this function again diff --git a/pkg/api/types.go b/pkg/api/types.go index ba19a23a88f..48a71579958 100644 --- a/pkg/api/types.go +++ b/pkg/api/types.go @@ -1054,6 +1054,54 @@ type ReplicationControllerList struct { Items []ReplicationController `json:"items"` } +// DaemonSpec is the specification of a daemon. +type DaemonSpec struct { + // Selector is a label query over pods that are managed by the daemon. + Selector map[string]string `json:"selector"` + + // Template is the object that describes the pod that will be created. + // The Daemon will create exactly one copy of this pod on every node + // that matches the template's node selector (or on every node if no node + // selector is specified). + Template *PodTemplateSpec `json:"template,omitempty"` +} + +// DaemonStatus represents the current status of a daemon. +type DaemonStatus struct { + // CurrentNumberScheduled is the number of nodes that are running exactly 1 copy of the + // daemon and are supposed to run the daemon. + CurrentNumberScheduled int `json:"currentNumberScheduled"` + + // NumberMisscheduled is the number of nodes that are running the daemon, but are + // not supposed to run the daemon. + NumberMisscheduled int `json:"numberMisscheduled"` + + // DesiredNumberScheduled is the total number of nodes that should be running the daemon + // (including nodes correctly running the daemon). + DesiredNumberScheduled int `json:"desiredNumberScheduled"` +} + +// Daemon represents the configuration of a daemon. +type Daemon struct { + TypeMeta `json:",inline"` + ObjectMeta `json:"metadata,omitempty"` + + // Spec defines the desired behavior of this daemon. + Spec DaemonSpec `json:"spec,omitempty"` + + // Status is the current status of this daemon. This data may be + // out of date by some window of time. + Status DaemonStatus `json:"status,omitempty"` +} + +// DaemonList is a collection of daemon. +type DaemonList struct { + TypeMeta `json:",inline"` + ListMeta `json:"metadata,omitempty"` + + Items []Daemon `json:"items"` +} + const ( // ClusterIPNone - do not assign a cluster IP // no proxying required and no environment variables should be created for pods @@ -1927,6 +1975,8 @@ const ( ResourceServices ResourceName = "services" // ReplicationControllers, number ResourceReplicationControllers ResourceName = "replicationcontrollers" + // Daemon, number + ResourceDaemon ResourceName = "daemon" // ResourceQuotas, number ResourceQuotas ResourceName = "resourcequotas" // ResourceSecrets, number diff --git a/pkg/api/v1/conversion_generated.go b/pkg/api/v1/conversion_generated.go index 397c65c3cf3..dc78306a632 100644 --- a/pkg/api/v1/conversion_generated.go +++ b/pkg/api/v1/conversion_generated.go @@ -324,6 +324,81 @@ func convert_api_ContainerStatus_To_v1_ContainerStatus(in *api.ContainerStatus, return nil } +func convert_api_Daemon_To_v1_Daemon(in *api.Daemon, out *Daemon, s conversion.Scope) error { + if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { + defaulting.(func(*api.Daemon))(in) + } + if err := convert_api_TypeMeta_To_v1_TypeMeta(&in.TypeMeta, &out.TypeMeta, s); err != nil { + return err + } + if err := convert_api_ObjectMeta_To_v1_ObjectMeta(&in.ObjectMeta, &out.ObjectMeta, s); err != nil { + return err + } + if err := convert_api_DaemonSpec_To_v1_DaemonSpec(&in.Spec, &out.Spec, s); err != nil { + return err + } + if err := convert_api_DaemonStatus_To_v1_DaemonStatus(&in.Status, &out.Status, s); err != nil { + return err + } + return nil +} + +func convert_api_DaemonList_To_v1_DaemonList(in *api.DaemonList, out *DaemonList, s conversion.Scope) error { + if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { + defaulting.(func(*api.DaemonList))(in) + } + if err := convert_api_TypeMeta_To_v1_TypeMeta(&in.TypeMeta, &out.TypeMeta, s); err != nil { + return err + } + if err := convert_api_ListMeta_To_v1_ListMeta(&in.ListMeta, &out.ListMeta, s); err != nil { + return err + } + if in.Items != nil { + out.Items = make([]Daemon, len(in.Items)) + for i := range in.Items { + if err := convert_api_Daemon_To_v1_Daemon(&in.Items[i], &out.Items[i], s); err != nil { + return err + } + } + } else { + out.Items = nil + } + return nil +} + +func convert_api_DaemonSpec_To_v1_DaemonSpec(in *api.DaemonSpec, out *DaemonSpec, s conversion.Scope) error { + if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { + defaulting.(func(*api.DaemonSpec))(in) + } + if in.Selector != nil { + out.Selector = make(map[string]string) + for key, val := range in.Selector { + out.Selector[key] = val + } + } else { + out.Selector = nil + } + if in.Template != nil { + out.Template = new(PodTemplateSpec) + if err := convert_api_PodTemplateSpec_To_v1_PodTemplateSpec(in.Template, out.Template, s); err != nil { + return err + } + } else { + out.Template = nil + } + return nil +} + +func convert_api_DaemonStatus_To_v1_DaemonStatus(in *api.DaemonStatus, out *DaemonStatus, s conversion.Scope) error { + if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { + defaulting.(func(*api.DaemonStatus))(in) + } + out.CurrentNumberScheduled = in.CurrentNumberScheduled + out.NumberMisscheduled = in.NumberMisscheduled + out.DesiredNumberScheduled = in.DesiredNumberScheduled + return nil +} + func convert_api_DeleteOptions_To_v1_DeleteOptions(in *api.DeleteOptions, out *DeleteOptions, s conversion.Scope) error { if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { defaulting.(func(*api.DeleteOptions))(in) @@ -2591,6 +2666,81 @@ func convert_v1_ContainerStatus_To_api_ContainerStatus(in *ContainerStatus, out return nil } +func convert_v1_Daemon_To_api_Daemon(in *Daemon, out *api.Daemon, s conversion.Scope) error { + if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { + defaulting.(func(*Daemon))(in) + } + if err := convert_v1_TypeMeta_To_api_TypeMeta(&in.TypeMeta, &out.TypeMeta, s); err != nil { + return err + } + if err := convert_v1_ObjectMeta_To_api_ObjectMeta(&in.ObjectMeta, &out.ObjectMeta, s); err != nil { + return err + } + if err := convert_v1_DaemonSpec_To_api_DaemonSpec(&in.Spec, &out.Spec, s); err != nil { + return err + } + if err := convert_v1_DaemonStatus_To_api_DaemonStatus(&in.Status, &out.Status, s); err != nil { + return err + } + return nil +} + +func convert_v1_DaemonList_To_api_DaemonList(in *DaemonList, out *api.DaemonList, s conversion.Scope) error { + if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { + defaulting.(func(*DaemonList))(in) + } + if err := convert_v1_TypeMeta_To_api_TypeMeta(&in.TypeMeta, &out.TypeMeta, s); err != nil { + return err + } + if err := convert_v1_ListMeta_To_api_ListMeta(&in.ListMeta, &out.ListMeta, s); err != nil { + return err + } + if in.Items != nil { + out.Items = make([]api.Daemon, len(in.Items)) + for i := range in.Items { + if err := convert_v1_Daemon_To_api_Daemon(&in.Items[i], &out.Items[i], s); err != nil { + return err + } + } + } else { + out.Items = nil + } + return nil +} + +func convert_v1_DaemonSpec_To_api_DaemonSpec(in *DaemonSpec, out *api.DaemonSpec, s conversion.Scope) error { + if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { + defaulting.(func(*DaemonSpec))(in) + } + if in.Selector != nil { + out.Selector = make(map[string]string) + for key, val := range in.Selector { + out.Selector[key] = val + } + } else { + out.Selector = nil + } + if in.Template != nil { + out.Template = new(api.PodTemplateSpec) + if err := convert_v1_PodTemplateSpec_To_api_PodTemplateSpec(in.Template, out.Template, s); err != nil { + return err + } + } else { + out.Template = nil + } + return nil +} + +func convert_v1_DaemonStatus_To_api_DaemonStatus(in *DaemonStatus, out *api.DaemonStatus, s conversion.Scope) error { + if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { + defaulting.(func(*DaemonStatus))(in) + } + out.CurrentNumberScheduled = in.CurrentNumberScheduled + out.NumberMisscheduled = in.NumberMisscheduled + out.DesiredNumberScheduled = in.DesiredNumberScheduled + return nil +} + func convert_v1_DeleteOptions_To_api_DeleteOptions(in *DeleteOptions, out *api.DeleteOptions, s conversion.Scope) error { if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { defaulting.(func(*DeleteOptions))(in) @@ -4573,6 +4723,10 @@ func init() { convert_api_ContainerState_To_v1_ContainerState, convert_api_ContainerStatus_To_v1_ContainerStatus, convert_api_Container_To_v1_Container, + convert_api_DaemonList_To_v1_DaemonList, + convert_api_DaemonSpec_To_v1_DaemonSpec, + convert_api_DaemonStatus_To_v1_DaemonStatus, + convert_api_Daemon_To_v1_Daemon, convert_api_DeleteOptions_To_v1_DeleteOptions, convert_api_EmptyDirVolumeSource_To_v1_EmptyDirVolumeSource, convert_api_EndpointAddress_To_v1_EndpointAddress, @@ -4686,6 +4840,10 @@ func init() { convert_v1_ContainerState_To_api_ContainerState, convert_v1_ContainerStatus_To_api_ContainerStatus, convert_v1_Container_To_api_Container, + convert_v1_DaemonList_To_api_DaemonList, + convert_v1_DaemonSpec_To_api_DaemonSpec, + convert_v1_DaemonStatus_To_api_DaemonStatus, + convert_v1_Daemon_To_api_Daemon, convert_v1_DeleteOptions_To_api_DeleteOptions, convert_v1_EmptyDirVolumeSource_To_api_EmptyDirVolumeSource, convert_v1_EndpointAddress_To_api_EndpointAddress, diff --git a/pkg/api/v1/deep_copy_generated.go b/pkg/api/v1/deep_copy_generated.go index 44a4d685210..bac49bba915 100644 --- a/pkg/api/v1/deep_copy_generated.go +++ b/pkg/api/v1/deep_copy_generated.go @@ -302,6 +302,69 @@ func deepCopy_v1_ContainerStatus(in ContainerStatus, out *ContainerStatus, c *co return nil } +func deepCopy_v1_Daemon(in Daemon, out *Daemon, c *conversion.Cloner) error { + if err := deepCopy_v1_TypeMeta(in.TypeMeta, &out.TypeMeta, c); err != nil { + return err + } + if err := deepCopy_v1_ObjectMeta(in.ObjectMeta, &out.ObjectMeta, c); err != nil { + return err + } + if err := deepCopy_v1_DaemonSpec(in.Spec, &out.Spec, c); err != nil { + return err + } + if err := deepCopy_v1_DaemonStatus(in.Status, &out.Status, c); err != nil { + return err + } + return nil +} + +func deepCopy_v1_DaemonList(in DaemonList, out *DaemonList, c *conversion.Cloner) error { + if err := deepCopy_v1_TypeMeta(in.TypeMeta, &out.TypeMeta, c); err != nil { + return err + } + if err := deepCopy_v1_ListMeta(in.ListMeta, &out.ListMeta, c); err != nil { + return err + } + if in.Items != nil { + out.Items = make([]Daemon, len(in.Items)) + for i := range in.Items { + if err := deepCopy_v1_Daemon(in.Items[i], &out.Items[i], c); err != nil { + return err + } + } + } else { + out.Items = nil + } + return nil +} + +func deepCopy_v1_DaemonSpec(in DaemonSpec, out *DaemonSpec, c *conversion.Cloner) error { + if in.Selector != nil { + out.Selector = make(map[string]string) + for key, val := range in.Selector { + out.Selector[key] = val + } + } else { + out.Selector = nil + } + if in.Template != nil { + out.Template = new(PodTemplateSpec) + if err := deepCopy_v1_PodTemplateSpec(*in.Template, out.Template, c); err != nil { + return err + } + } else { + out.Template = nil + } + return nil +} + +func deepCopy_v1_DaemonStatus(in DaemonStatus, out *DaemonStatus, c *conversion.Cloner) error { + out.CurrentNumberScheduled = in.CurrentNumberScheduled + out.NumberMisscheduled = in.NumberMisscheduled + out.DesiredNumberScheduled = in.DesiredNumberScheduled + return nil +} + func deepCopy_v1_DeleteOptions(in DeleteOptions, out *DeleteOptions, c *conversion.Cloner) error { if err := deepCopy_v1_TypeMeta(in.TypeMeta, &out.TypeMeta, c); err != nil { return err @@ -2109,6 +2172,10 @@ func init() { deepCopy_v1_ContainerStateTerminated, deepCopy_v1_ContainerStateWaiting, deepCopy_v1_ContainerStatus, + deepCopy_v1_Daemon, + deepCopy_v1_DaemonList, + deepCopy_v1_DaemonSpec, + deepCopy_v1_DaemonStatus, deepCopy_v1_DeleteOptions, deepCopy_v1_EmptyDirVolumeSource, deepCopy_v1_EndpointAddress, diff --git a/pkg/api/v1/defaults.go b/pkg/api/v1/defaults.go index f9e47a2135f..d64f7c49dd0 100644 --- a/pkg/api/v1/defaults.go +++ b/pkg/api/v1/defaults.go @@ -44,6 +44,21 @@ func addDefaultingFuncs() { *obj.Spec.Replicas = 1 } }, + func(obj *Daemon) { + var labels map[string]string + if obj.Spec.Template != nil { + labels = obj.Spec.Template.Labels + } + // TODO: support templates defined elsewhere when we support them in the API + if labels != nil { + if len(obj.Spec.Selector) == 0 { + obj.Spec.Selector = labels + } + if len(obj.Labels) == 0 { + obj.Labels = labels + } + } + }, func(obj *Volume) { if util.AllPtrFieldsNil(&obj.VolumeSource) { obj.VolumeSource = VolumeSource{ diff --git a/pkg/api/v1/defaults_test.go b/pkg/api/v1/defaults_test.go index 9c4a6275f41..f09e52f7204 100644 --- a/pkg/api/v1/defaults_test.go +++ b/pkg/api/v1/defaults_test.go @@ -155,6 +155,64 @@ func TestSetDefaultReplicationController(t *testing.T) { } } +func TestSetDefaultDaemon(t *testing.T) { + tests := []struct { + dc *versioned.Daemon + expectLabelsChange bool + }{ + { + dc: &versioned.Daemon{ + Spec: versioned.DaemonSpec{ + Template: &versioned.PodTemplateSpec{ + ObjectMeta: versioned.ObjectMeta{ + Labels: map[string]string{ + "foo": "bar", + }, + }, + }, + }, + }, + expectLabelsChange: true, + }, + { + dc: &versioned.Daemon{ + ObjectMeta: versioned.ObjectMeta{ + Labels: map[string]string{ + "bar": "foo", + }, + }, + Spec: versioned.DaemonSpec{ + Template: &versioned.PodTemplateSpec{ + ObjectMeta: versioned.ObjectMeta{ + Labels: map[string]string{ + "foo": "bar", + }, + }, + }, + }, + }, + expectLabelsChange: false, + }, + } + + for _, test := range tests { + dc := test.dc + obj2 := roundTrip(t, runtime.Object(dc)) + dc2, ok := obj2.(*versioned.Daemon) + if !ok { + t.Errorf("unexpected object: %v", dc2) + t.FailNow() + } + if test.expectLabelsChange != reflect.DeepEqual(dc2.Labels, dc2.Spec.Template.Labels) { + if test.expectLabelsChange { + t.Errorf("expected: %v, got: %v", dc2.Spec.Template.Labels, dc2.Labels) + } else { + t.Errorf("unexpected equality: %v", dc.Labels) + } + } + } +} + func newInt(val int) *int { p := new(int) *p = val diff --git a/pkg/api/v1/register.go b/pkg/api/v1/register.go index bb5b142c0cc..3c0a52331c4 100644 --- a/pkg/api/v1/register.go +++ b/pkg/api/v1/register.go @@ -47,6 +47,8 @@ func addKnownTypes() { &PodTemplateList{}, &ReplicationController{}, &ReplicationControllerList{}, + &DaemonList{}, + &Daemon{}, &Service{}, &ServiceList{}, &Endpoints{}, @@ -95,6 +97,8 @@ func (*PodTemplate) IsAnAPIObject() {} func (*PodTemplateList) IsAnAPIObject() {} func (*ReplicationController) IsAnAPIObject() {} func (*ReplicationControllerList) IsAnAPIObject() {} +func (*Daemon) IsAnAPIObject() {} +func (*DaemonList) IsAnAPIObject() {} func (*Service) IsAnAPIObject() {} func (*ServiceList) IsAnAPIObject() {} func (*Endpoints) IsAnAPIObject() {} diff --git a/pkg/api/v1/types.go b/pkg/api/v1/types.go index 3185ece53dc..b4744a1ddba 100644 --- a/pkg/api/v1/types.go +++ b/pkg/api/v1/types.go @@ -1028,6 +1028,54 @@ type ReplicationControllerList struct { Items []ReplicationController `json:"items" description:"list of replication controllers; see http://releases.k8s.io/HEAD/docs/user-guide/replication-controller.md"` } +// DaemonSpec is the specification of a daemon. +type DaemonSpec struct { + // Selector is a label query over pods that are managed by the daemon. + Selector map[string]string `json:"selector,omitempty" description:"label keys and values that must match in order to be controlled by this daemon, if empty defaulted to labels on Pod template; see http://releases.k8s.io/HEAD/docs/user-guide/labels.md#label-selectors"` + + // Template is the object that describes the pod that will be created. + // The Daemon will create exactly one copy of this pod on every node + // that matches the template's node selector (or on every node if no node + // selector is specified). + Template *PodTemplateSpec `json:"template,omitempty" description:"object that describes the pod that will be created by this daemon; see http://releases.k8s.io/HEAD/docs/user-guide/replication-controller.md#pod-template"` +} + +// DaemonStatus represents the current status of a daemon. +type DaemonStatus struct { + // CurrentNumberScheduled is the number of nodes that are running exactly 1 copy of the + // daemon and are supposed to run the daemon. + CurrentNumberScheduled int `json:"currentNumberScheduled" description:"number of nodes that are running exactly 1 copy of the daemon and are supposed to run the daemon"` + + // NumberMisscheduled is the number of nodes that are running the daemon, but are + // not supposed to run the daemon. + NumberMisscheduled int `json:"numberMisscheduled" description:"number of nodes that are running the Daemon, but are not supposed to run the daemon"` + + // DesiredNumberScheduled is the total number of nodes that should be running the daemon + // (including nodes correctly running the daemon). + DesiredNumberScheduled int `json:"desiredNumberScheduled" description:"total number of nodes that should be running the Daemon (including nodes correctly running the daemon)"` +} + +// Daemon represents the configuration of a daemon. +type Daemon struct { + TypeMeta `json:",inline"` + ObjectMeta `json:"metadata,omitempty" description:"standard object metadata; see http://releases.k8s.io/HEAD/docs/devel/api-conventions.md#metadata"` + + // Spec defines the desired behavior of this daemon. + Spec DaemonSpec `json:"spec,omitempty" description:"specification of the desired behavior of the daemon; http://releases.k8s.io/HEAD/docs/devel/api-conventions.md#spec-and-status"` + + // Status is the current status of this daemon. This data may be + // out of date by some window of time. + Status DaemonStatus `json:"status,omitempty" description:"most recently observed status of the daemon; populated by the system, read-only; http://releases.k8s.io/HEAD/docs/devel/api-conventions.md#spec-and-status"` +} + +// DaemonList is a collection of daemon. +type DaemonList struct { + TypeMeta `json:",inline"` + ListMeta `json:"metadata,omitempty" description:"standard list metadata; see http://releases.k8s.io/HEAD/docs/devel/api-conventions.md#metadata"` + + Items []Daemon `json:"items" description:"list of daemons"` +} + // Session Affinity Type string type ServiceAffinity string @@ -1829,6 +1877,8 @@ const ( ResourceServices ResourceName = "services" // ReplicationControllers, number ResourceReplicationControllers ResourceName = "replicationcontrollers" + // Daemon, number + ResourceDaemon ResourceName = "daemon" // ResourceQuotas, number ResourceQuotas ResourceName = "resourcequotas" // ResourceSecrets, number diff --git a/pkg/api/validation/validation.go b/pkg/api/validation/validation.go index c54362a4f10..d8e991a8f5f 100644 --- a/pkg/api/validation/validation.go +++ b/pkg/api/validation/validation.go @@ -112,6 +112,13 @@ func ValidateReplicationControllerName(name string, prefix bool) (bool, string) return NameIsDNSSubdomain(name, prefix) } +// ValidateDaemonName can be used to check whether the given daemon name is valid. +// Prefix indicates this name will be used as part of generation, in which case +// trailing dashes are allowed. +func ValidateDaemonName(name string, prefix bool) (bool, string) { + return NameIsDNSSubdomain(name, prefix) +} + // ValidateServiceName can be used to check whether the given service name is valid. // Prefix indicates this name will be used as part of generation, in which case // trailing dashes are allowed. @@ -1027,7 +1034,7 @@ func ValidatePodStatusUpdate(newPod, oldPod *api.Pod) errs.ValidationErrorList { func ValidatePodTemplate(pod *api.PodTemplate) errs.ValidationErrorList { allErrs := errs.ValidationErrorList{} allErrs = append(allErrs, ValidateObjectMeta(&pod.ObjectMeta, true, ValidatePodName).Prefix("metadata")...) - allErrs = append(allErrs, ValidatePodTemplateSpec(&pod.Template, 0).Prefix("template")...) + allErrs = append(allErrs, ValidatePodTemplateSpec(&pod.Template).Prefix("template")...) return allErrs } @@ -1035,9 +1042,8 @@ func ValidatePodTemplate(pod *api.PodTemplate) errs.ValidationErrorList { // that cannot be changed. func ValidatePodTemplateUpdate(newPod, oldPod *api.PodTemplate) errs.ValidationErrorList { allErrs := errs.ValidationErrorList{} - - allErrs = append(allErrs, ValidateObjectMetaUpdate(&newPod.ObjectMeta, &oldPod.ObjectMeta).Prefix("metadata")...) - allErrs = append(allErrs, ValidatePodTemplateSpec(&newPod.Template, 0).Prefix("template")...) + allErrs = append(allErrs, ValidateObjectMetaUpdate(&oldPod.ObjectMeta, &newPod.ObjectMeta).Prefix("metadata")...) + allErrs = append(allErrs, ValidatePodTemplateSpec(&newPod.Template).Prefix("template")...) return allErrs } @@ -1185,7 +1191,6 @@ func ValidateReplicationController(controller *api.ReplicationController) errs.V allErrs := errs.ValidationErrorList{} allErrs = append(allErrs, ValidateObjectMeta(&controller.ObjectMeta, true, ValidateReplicationControllerName).Prefix("metadata")...) allErrs = append(allErrs, ValidateReplicationControllerSpec(&controller.Spec).Prefix("spec")...) - return allErrs } @@ -1214,9 +1219,71 @@ func ValidateReplicationControllerSpec(spec *api.ReplicationControllerSpec) errs } else { labels := labels.Set(spec.Template.Labels) if !selector.Matches(labels) { - allErrs = append(allErrs, errs.NewFieldInvalid("template.labels", spec.Template.Labels, "selector does not match template")) + allErrs = append(allErrs, errs.NewFieldInvalid("template.metadata.labels", spec.Template.Labels, "selector does not match template")) } - allErrs = append(allErrs, ValidatePodTemplateSpec(spec.Template, spec.Replicas).Prefix("template")...) + allErrs = append(allErrs, ValidatePodTemplateSpec(spec.Template).Prefix("template")...) + if spec.Replicas > 1 { + allErrs = append(allErrs, ValidateReadOnlyPersistentDisks(spec.Template.Spec.Volumes).Prefix("template.spec.volumes")...) + } + // RestartPolicy has already been first-order validated as per ValidatePodTemplateSpec(). + if spec.Template.Spec.RestartPolicy != api.RestartPolicyAlways { + allErrs = append(allErrs, errs.NewFieldValueNotSupported("template.spec.restartPolicy", spec.Template.Spec.RestartPolicy, []string{string(api.RestartPolicyAlways)})) + } + } + return allErrs +} + +// ValidateDaemon tests if required fields in the daemon are set. +func ValidateDaemon(controller *api.Daemon) errs.ValidationErrorList { + allErrs := errs.ValidationErrorList{} + allErrs = append(allErrs, ValidateObjectMeta(&controller.ObjectMeta, true, ValidateReplicationControllerName).Prefix("metadata")...) + allErrs = append(allErrs, ValidateDaemonSpec(&controller.Spec).Prefix("spec")...) + return allErrs +} + +// ValidateDaemonUpdate tests if required fields in the daemon are set. +func ValidateDaemonUpdate(oldController, controller *api.Daemon) errs.ValidationErrorList { + allErrs := errs.ValidationErrorList{} + allErrs = append(allErrs, ValidateObjectMetaUpdate(&controller.ObjectMeta, &oldController.ObjectMeta).Prefix("metadata")...) + allErrs = append(allErrs, ValidateDaemonSpec(&controller.Spec).Prefix("spec")...) + allErrs = append(allErrs, ValidateDaemonTemplateUpdate(oldController.Spec.Template, controller.Spec.Template).Prefix("spec.template")...) + return allErrs +} + +// ValidateDaemonTemplateUpdate tests that certain fields in the daemon's pod template are not updated. +func ValidateDaemonTemplateUpdate(oldPodTemplate, podTemplate *api.PodTemplateSpec) errs.ValidationErrorList { + allErrs := errs.ValidationErrorList{} + podSpec := podTemplate.Spec + // podTemplate.Spec is not a pointer, so we can modify NodeSelector and NodeName directly. + podSpec.NodeSelector = oldPodTemplate.Spec.NodeSelector + podSpec.NodeName = oldPodTemplate.Spec.NodeName + // In particular, we do not allow updates to container images at this point. + if !api.Semantic.DeepEqual(oldPodTemplate.Spec, podSpec) { + // TODO: Pinpoint the specific field that causes the invalid error after we have strategic merge diff + allErrs = append(allErrs, errs.NewFieldInvalid("spec", "content of spec is not printed out, please refer to the \"details\"", "may not update fields other than spec.nodeSelector")) + } + return allErrs +} + +// ValidateDaemonSpec tests if required fields in the daemon spec are set. +func ValidateDaemonSpec(spec *api.DaemonSpec) errs.ValidationErrorList { + allErrs := errs.ValidationErrorList{} + + selector := labels.Set(spec.Selector).AsSelector() + if selector.Empty() { + allErrs = append(allErrs, errs.NewFieldRequired("selector")) + } + + if spec.Template == nil { + allErrs = append(allErrs, errs.NewFieldRequired("template")) + } else { + labels := labels.Set(spec.Template.Labels) + if !selector.Matches(labels) { + allErrs = append(allErrs, errs.NewFieldInvalid("template.metadata.labels", spec.Template.Labels, "selector does not match template")) + } + allErrs = append(allErrs, ValidatePodTemplateSpec(spec.Template).Prefix("template")...) + // Daemons typically run on more than one node, so mark Read-Write persistent disks as invalid. + allErrs = append(allErrs, ValidateReadOnlyPersistentDisks(spec.Template.Spec.Volumes).Prefix("template.spec.volumes")...) // RestartPolicy has already been first-order validated as per ValidatePodTemplateSpec(). if spec.Template.Spec.RestartPolicy != api.RestartPolicyAlways { allErrs = append(allErrs, errs.NewFieldValueNotSupported("template.spec.restartPolicy", spec.Template.Spec.RestartPolicy, []string{string(api.RestartPolicyAlways)})) @@ -1226,14 +1293,11 @@ func ValidateReplicationControllerSpec(spec *api.ReplicationControllerSpec) errs } // ValidatePodTemplateSpec validates the spec of a pod template -func ValidatePodTemplateSpec(spec *api.PodTemplateSpec, replicas int) errs.ValidationErrorList { +func ValidatePodTemplateSpec(spec *api.PodTemplateSpec) errs.ValidationErrorList { allErrs := errs.ValidationErrorList{} allErrs = append(allErrs, ValidateLabels(spec.Labels, "labels")...) allErrs = append(allErrs, ValidateAnnotations(spec.Annotations, "annotations")...) allErrs = append(allErrs, ValidatePodSpec(&spec.Spec).Prefix("spec")...) - if replicas > 1 { - allErrs = append(allErrs, ValidateReadOnlyPersistentDisks(spec.Spec.Volumes).Prefix("spec.volumes")...) - } return allErrs } diff --git a/pkg/api/validation/validation_test.go b/pkg/api/validation/validation_test.go index 05739a23b61..dd0d050370c 100644 --- a/pkg/api/validation/validation_test.go +++ b/pkg/api/validation/validation_test.go @@ -2260,6 +2260,418 @@ func TestValidateReplicationController(t *testing.T) { } } +func TestValidateDaemonUpdate(t *testing.T) { + validSelector := map[string]string{"a": "b"} + validSelector2 := map[string]string{"c": "d"} + invalidSelector := map[string]string{"NoUppercaseOrSpecialCharsLike=Equals": "b"} + + validPodSpecAbc := api.PodSpec{ + RestartPolicy: api.RestartPolicyAlways, + DNSPolicy: api.DNSClusterFirst, + Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent"}}, + } + validPodSpecDef := api.PodSpec{ + RestartPolicy: api.RestartPolicyAlways, + DNSPolicy: api.DNSClusterFirst, + Containers: []api.Container{{Name: "def", Image: "image", ImagePullPolicy: "IfNotPresent"}}, + } + validPodSpecNodeSelector := api.PodSpec{ + NodeSelector: validSelector, + NodeName: "xyz", + RestartPolicy: api.RestartPolicyAlways, + DNSPolicy: api.DNSClusterFirst, + Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent"}}, + } + validPodSpecVolume := api.PodSpec{ + Volumes: []api.Volume{{Name: "gcepd", VolumeSource: api.VolumeSource{GCEPersistentDisk: &api.GCEPersistentDiskVolumeSource{"my-PD", "ext4", 1, false}}}}, + RestartPolicy: api.RestartPolicyAlways, + DNSPolicy: api.DNSClusterFirst, + Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent"}}, + } + + validPodTemplateAbc := api.PodTemplate{ + Template: api.PodTemplateSpec{ + ObjectMeta: api.ObjectMeta{ + Labels: validSelector, + }, + Spec: validPodSpecAbc, + }, + } + validPodTemplateNodeSelector := api.PodTemplate{ + Template: api.PodTemplateSpec{ + ObjectMeta: api.ObjectMeta{ + Labels: validSelector, + }, + Spec: validPodSpecNodeSelector, + }, + } + validPodTemplateAbc2 := api.PodTemplate{ + Template: api.PodTemplateSpec{ + ObjectMeta: api.ObjectMeta{ + Labels: validSelector2, + }, + Spec: validPodSpecAbc, + }, + } + validPodTemplateDef := api.PodTemplate{ + Template: api.PodTemplateSpec{ + ObjectMeta: api.ObjectMeta{ + Labels: validSelector2, + }, + Spec: validPodSpecDef, + }, + } + invalidPodTemplate := api.PodTemplate{ + Template: api.PodTemplateSpec{ + Spec: api.PodSpec{ + RestartPolicy: api.RestartPolicyAlways, + DNSPolicy: api.DNSClusterFirst, + }, + ObjectMeta: api.ObjectMeta{ + Labels: invalidSelector, + }, + }, + } + readWriteVolumePodTemplate := api.PodTemplate{ + Template: api.PodTemplateSpec{ + ObjectMeta: api.ObjectMeta{ + Labels: validSelector, + }, + Spec: validPodSpecVolume, + }, + } + + type dcUpdateTest struct { + old api.Daemon + update api.Daemon + } + successCases := []dcUpdateTest{ + { + old: api.Daemon{ + ObjectMeta: api.ObjectMeta{Name: "abc", Namespace: api.NamespaceDefault}, + Spec: api.DaemonSpec{ + Selector: validSelector, + Template: &validPodTemplateAbc.Template, + }, + }, + update: api.Daemon{ + ObjectMeta: api.ObjectMeta{Name: "abc", Namespace: api.NamespaceDefault}, + Spec: api.DaemonSpec{ + Selector: validSelector, + Template: &validPodTemplateAbc.Template, + }, + }, + }, + { + old: api.Daemon{ + ObjectMeta: api.ObjectMeta{Name: "abc", Namespace: api.NamespaceDefault}, + Spec: api.DaemonSpec{ + Selector: validSelector, + Template: &validPodTemplateAbc.Template, + }, + }, + update: api.Daemon{ + ObjectMeta: api.ObjectMeta{Name: "abc", Namespace: api.NamespaceDefault}, + Spec: api.DaemonSpec{ + Selector: validSelector2, + Template: &validPodTemplateAbc2.Template, + }, + }, + }, + { + old: api.Daemon{ + ObjectMeta: api.ObjectMeta{Name: "abc", Namespace: api.NamespaceDefault}, + Spec: api.DaemonSpec{ + Selector: validSelector, + Template: &validPodTemplateAbc.Template, + }, + }, + update: api.Daemon{ + ObjectMeta: api.ObjectMeta{Name: "abc", Namespace: api.NamespaceDefault}, + Spec: api.DaemonSpec{ + Selector: validSelector, + Template: &validPodTemplateNodeSelector.Template, + }, + }, + }, + } + for _, successCase := range successCases { + successCase.old.ObjectMeta.ResourceVersion = "1" + successCase.update.ObjectMeta.ResourceVersion = "1" + if errs := ValidateDaemonUpdate(&successCase.old, &successCase.update); len(errs) != 0 { + t.Errorf("expected success: %v", errs) + } + } + errorCases := map[string]dcUpdateTest{ + "change daemon name": { + old: api.Daemon{ + ObjectMeta: api.ObjectMeta{Name: "", Namespace: api.NamespaceDefault}, + Spec: api.DaemonSpec{ + Selector: validSelector, + Template: &validPodTemplateAbc.Template, + }, + }, + update: api.Daemon{ + ObjectMeta: api.ObjectMeta{Name: "abc", Namespace: api.NamespaceDefault}, + Spec: api.DaemonSpec{ + Selector: validSelector, + Template: &validPodTemplateAbc.Template, + }, + }, + }, + "invalid selector": { + old: api.Daemon{ + ObjectMeta: api.ObjectMeta{Name: "", Namespace: api.NamespaceDefault}, + Spec: api.DaemonSpec{ + Selector: validSelector, + Template: &validPodTemplateAbc.Template, + }, + }, + update: api.Daemon{ + ObjectMeta: api.ObjectMeta{Name: "abc", Namespace: api.NamespaceDefault}, + Spec: api.DaemonSpec{ + Selector: invalidSelector, + Template: &validPodTemplateAbc.Template, + }, + }, + }, + "invalid pod": { + old: api.Daemon{ + ObjectMeta: api.ObjectMeta{Name: "", Namespace: api.NamespaceDefault}, + Spec: api.DaemonSpec{ + Selector: validSelector, + Template: &validPodTemplateAbc.Template, + }, + }, + update: api.Daemon{ + ObjectMeta: api.ObjectMeta{Name: "abc", Namespace: api.NamespaceDefault}, + Spec: api.DaemonSpec{ + Selector: validSelector, + Template: &invalidPodTemplate.Template, + }, + }, + }, + "change container image": { + old: api.Daemon{ + ObjectMeta: api.ObjectMeta{Name: "", Namespace: api.NamespaceDefault}, + Spec: api.DaemonSpec{ + Selector: validSelector, + Template: &validPodTemplateAbc.Template, + }, + }, + update: api.Daemon{ + ObjectMeta: api.ObjectMeta{Name: "abc", Namespace: api.NamespaceDefault}, + Spec: api.DaemonSpec{ + Selector: validSelector, + Template: &validPodTemplateDef.Template, + }, + }, + }, + "read-write volume": { + old: api.Daemon{ + ObjectMeta: api.ObjectMeta{Name: "", Namespace: api.NamespaceDefault}, + Spec: api.DaemonSpec{ + Selector: validSelector, + Template: &validPodTemplateAbc.Template, + }, + }, + update: api.Daemon{ + ObjectMeta: api.ObjectMeta{Name: "abc", Namespace: api.NamespaceDefault}, + Spec: api.DaemonSpec{ + Selector: validSelector, + Template: &readWriteVolumePodTemplate.Template, + }, + }, + }, + } + for testName, errorCase := range errorCases { + if errs := ValidateDaemonUpdate(&errorCase.old, &errorCase.update); len(errs) == 0 { + t.Errorf("expected failure: %s", testName) + } + } +} + +func TestValidateDaemon(t *testing.T) { + validSelector := map[string]string{"a": "b"} + validPodTemplate := api.PodTemplate{ + Template: api.PodTemplateSpec{ + ObjectMeta: api.ObjectMeta{ + Labels: validSelector, + }, + Spec: api.PodSpec{ + RestartPolicy: api.RestartPolicyAlways, + DNSPolicy: api.DNSClusterFirst, + Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent"}}, + }, + }, + } + invalidSelector := map[string]string{"NoUppercaseOrSpecialCharsLike=Equals": "b"} + invalidPodTemplate := api.PodTemplate{ + Template: api.PodTemplateSpec{ + Spec: api.PodSpec{ + RestartPolicy: api.RestartPolicyAlways, + DNSPolicy: api.DNSClusterFirst, + }, + ObjectMeta: api.ObjectMeta{ + Labels: invalidSelector, + }, + }, + } + successCases := []api.Daemon{ + { + ObjectMeta: api.ObjectMeta{Name: "abc", Namespace: api.NamespaceDefault}, + Spec: api.DaemonSpec{ + Selector: validSelector, + Template: &validPodTemplate.Template, + }, + }, + { + ObjectMeta: api.ObjectMeta{Name: "abc-123", Namespace: api.NamespaceDefault}, + Spec: api.DaemonSpec{ + Selector: validSelector, + Template: &validPodTemplate.Template, + }, + }, + } + for _, successCase := range successCases { + if errs := ValidateDaemon(&successCase); len(errs) != 0 { + t.Errorf("expected success: %v", errs) + } + } + + errorCases := map[string]api.Daemon{ + "zero-length ID": { + ObjectMeta: api.ObjectMeta{Name: "", Namespace: api.NamespaceDefault}, + Spec: api.DaemonSpec{ + Selector: validSelector, + Template: &validPodTemplate.Template, + }, + }, + "missing-namespace": { + ObjectMeta: api.ObjectMeta{Name: "abc-123"}, + Spec: api.DaemonSpec{ + Selector: validSelector, + Template: &validPodTemplate.Template, + }, + }, + "empty selector": { + ObjectMeta: api.ObjectMeta{Name: "abc", Namespace: api.NamespaceDefault}, + Spec: api.DaemonSpec{ + Template: &validPodTemplate.Template, + }, + }, + "selector_doesnt_match": { + ObjectMeta: api.ObjectMeta{Name: "abc", Namespace: api.NamespaceDefault}, + Spec: api.DaemonSpec{ + Selector: map[string]string{"foo": "bar"}, + Template: &validPodTemplate.Template, + }, + }, + "invalid manifest": { + ObjectMeta: api.ObjectMeta{Name: "abc", Namespace: api.NamespaceDefault}, + Spec: api.DaemonSpec{ + Selector: validSelector, + }, + }, + "invalid_label": { + ObjectMeta: api.ObjectMeta{ + Name: "abc-123", + Namespace: api.NamespaceDefault, + Labels: map[string]string{ + "NoUppercaseOrSpecialCharsLike=Equals": "bar", + }, + }, + Spec: api.DaemonSpec{ + Selector: validSelector, + Template: &validPodTemplate.Template, + }, + }, + "invalid_label 2": { + ObjectMeta: api.ObjectMeta{ + Name: "abc-123", + Namespace: api.NamespaceDefault, + Labels: map[string]string{ + "NoUppercaseOrSpecialCharsLike=Equals": "bar", + }, + }, + Spec: api.DaemonSpec{ + Template: &invalidPodTemplate.Template, + }, + }, + "invalid_annotation": { + ObjectMeta: api.ObjectMeta{ + Name: "abc-123", + Namespace: api.NamespaceDefault, + Annotations: map[string]string{ + "NoUppercaseOrSpecialCharsLike=Equals": "bar", + }, + }, + Spec: api.DaemonSpec{ + Selector: validSelector, + Template: &validPodTemplate.Template, + }, + }, + "invalid restart policy 1": { + ObjectMeta: api.ObjectMeta{ + Name: "abc-123", + Namespace: api.NamespaceDefault, + }, + Spec: api.DaemonSpec{ + Selector: validSelector, + Template: &api.PodTemplateSpec{ + Spec: api.PodSpec{ + RestartPolicy: api.RestartPolicyOnFailure, + DNSPolicy: api.DNSClusterFirst, + Containers: []api.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent"}}, + }, + ObjectMeta: api.ObjectMeta{ + Labels: validSelector, + }, + }, + }, + }, + "invalid restart policy 2": { + ObjectMeta: api.ObjectMeta{ + Name: "abc-123", + Namespace: api.NamespaceDefault, + }, + Spec: api.DaemonSpec{ + Selector: validSelector, + Template: &api.PodTemplateSpec{ + Spec: api.PodSpec{ + RestartPolicy: api.RestartPolicyNever, + DNSPolicy: api.DNSClusterFirst, + Containers: []api.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent"}}, + }, + ObjectMeta: api.ObjectMeta{ + Labels: validSelector, + }, + }, + }, + }, + } + for k, v := range errorCases { + errs := ValidateDaemon(&v) + if len(errs) == 0 { + t.Errorf("expected failure for %s", k) + } + for i := range errs { + field := errs[i].(*errors.ValidationError).Field + if !strings.HasPrefix(field, "spec.template.") && + field != "metadata.name or metadata.generateName" && + field != "metadata.namespace" && + field != "spec.selector" && + field != "spec.template" && + field != "GCEPersistentDisk.ReadOnly" && + field != "spec.template.labels" && + field != "metadata.annotations" && + field != "metadata.labels" { + t.Errorf("%s: missing prefix for: %v", k, errs[i]) + } + } + } +} + func TestValidateNode(t *testing.T) { validSelector := map[string]string{"a": "b"} invalidSelector := map[string]string{"NoUppercaseOrSpecialCharsLike=Equals": "b"}