fix pod template spec validation missing in sts

Signed-off-by: joey <zchengjoey@gmail.com>
This commit is contained in:
joey
2025-05-15 18:57:04 +08:00
parent ec80dcaf38
commit dfd34a5b18
5 changed files with 346 additions and 29 deletions

View File

@@ -38,6 +38,8 @@ import (
type StatefulSetValidationOptions struct {
// Allow invalid DNS1123 ServiceName
AllowInvalidServiceName bool
// Skip validating pod template spec, which is used for StatefulSet update
SkipValidatePodTemplateSpec bool
}
// ValidateStatefulSetName can be used to check whether the given StatefulSet name is valid.
@@ -51,7 +53,8 @@ func ValidateStatefulSetName(name string, prefix bool) []string {
}
// ValidatePodTemplateSpecForStatefulSet validates the given template and ensures that it is in accordance with the desired selector.
func ValidatePodTemplateSpecForStatefulSet(template *api.PodTemplateSpec, selector labels.Selector, fldPath *field.Path, opts apivalidation.PodValidationOptions) field.ErrorList {
func ValidatePodTemplateSpecForStatefulSet(template *api.PodTemplateSpec, selector labels.Selector, fldPath *field.Path, opts apivalidation.PodValidationOptions,
setOpts StatefulSetValidationOptions) field.ErrorList {
allErrs := field.ErrorList{}
if template == nil {
allErrs = append(allErrs, field.Required(fldPath, ""))
@@ -63,10 +66,9 @@ func ValidatePodTemplateSpecForStatefulSet(template *api.PodTemplateSpec, select
allErrs = append(allErrs, field.Invalid(fldPath.Child("metadata", "labels"), template.Labels, "`selector` does not match template `labels`"))
}
}
// TODO: Add validation for PodSpec, currently this will check volumes, which we know will
// fail. We should really check that the union of the given volumes and volumeClaims match
// volume mounts in the containers.
// allErrs = append(allErrs, apivalidation.ValidatePodTemplateSpec(template, fldPath)...)
if !setOpts.SkipValidatePodTemplateSpec {
allErrs = append(allErrs, apivalidation.ValidatePodTemplateSpec(template, fldPath, opts)...)
}
allErrs = append(allErrs, unversionedvalidation.ValidateLabels(template.Labels, fldPath.Child("labels"))...)
allErrs = append(allErrs, apivalidation.ValidateAnnotations(template.Annotations, fldPath.Child("annotations"))...)
allErrs = append(allErrs, apivalidation.ValidatePodSpecificAnnotations(template.Annotations, &template.Spec, fldPath.Child("annotations"), opts)...)
@@ -94,6 +96,22 @@ func ValidatePersistentVolumeClaimRetentionPolicy(policy *apps.StatefulSetPersis
return allErrs
}
func volumesToAddForTemplates(spec *apps.StatefulSetSpec) map[string]api.Volume {
volumes := make(map[string]api.Volume)
templates := spec.VolumeClaimTemplates
for i := range templates {
volumes[templates[i].Name] = api.Volume{
Name: templates[i].Name,
VolumeSource: api.VolumeSource{
PersistentVolumeClaim: &api.PersistentVolumeClaimVolumeSource{
ClaimName: templates[i].Name,
},
},
}
}
return volumes
}
// ValidateStatefulSetSpec tests if required fields in the StatefulSet spec are set.
func ValidateStatefulSetSpec(spec *apps.StatefulSetSpec, fldPath *field.Path, opts apivalidation.PodValidationOptions, setOpts StatefulSetValidationOptions) field.ErrorList {
allErrs := field.ErrorList{}
@@ -158,7 +176,28 @@ func ValidateStatefulSetSpec(spec *apps.StatefulSetSpec, fldPath *field.Path, op
if err != nil {
allErrs = append(allErrs, field.Invalid(fldPath.Child("selector"), spec.Selector, ""))
} else {
allErrs = append(allErrs, ValidatePodTemplateSpecForStatefulSet(&spec.Template, selector, fldPath.Child("template"), opts)...)
templateToValidate := &spec.Template
if len(spec.Template.Spec.Subdomain) > 0 || len(spec.Template.Spec.Hostname) > 0 || len(spec.VolumeClaimTemplates) > 0 {
templateToValidate = templateToValidate.DeepCopy()
// overwritten by controller to spec.ServiceName in initIdentity, don't validate
templateToValidate.Spec.Subdomain = ""
// overwritten by controller to pod.Name in initIdentity, don't validate
templateToValidate.Spec.Hostname = ""
if len(spec.VolumeClaimTemplates) > 0 {
templateVolumes := volumesToAddForTemplates(spec)
newVolumes := make([]api.Volume, 0, len(templateVolumes))
for _, v := range templateVolumes {
newVolumes = append(newVolumes, v)
}
for _, v := range templateToValidate.Spec.Volumes {
if _, ok := templateVolumes[v.Name]; !ok {
newVolumes = append(newVolumes, v)
}
}
templateToValidate.Spec.Volumes = newVolumes
}
}
allErrs = append(allErrs, ValidatePodTemplateSpecForStatefulSet(templateToValidate, selector, fldPath.Child("template"), opts, setOpts)...)
}
if spec.Template.Spec.RestartPolicy != api.RestartPolicyAlways {
@@ -193,6 +232,10 @@ func ValidateStatefulSetUpdate(statefulSet, oldStatefulSet *apps.StatefulSet, op
setOpts := StatefulSetValidationOptions{
AllowInvalidServiceName: true, // serviceName is immutable, tolerate existing invalid names on update
}
// In order to tolerate the existing sts, we choose to skip the validation error of old sts podTemplateSpec.
if len(ValidateStatefulSetSpec(&oldStatefulSet.Spec, nil, opts, setOpts)) > 0 {
setOpts.SkipValidatePodTemplateSpec = true
}
allErrs = append(allErrs, ValidateStatefulSetSpec(&statefulSet.Spec, field.NewPath("spec"), opts, setOpts)...)
// statefulset updates aren't super common and general updates are likely to be touching spec, so we'll do this

View File

@@ -141,6 +141,12 @@ func tweakPVCTemplate(pvc ...api.PersistentVolumeClaim) statefulSetTweak {
}
}
func tweakVolumes(volumes ...api.Volume) statefulSetTweak {
return func(ss *apps.StatefulSet) {
ss.Spec.Template.Spec.Volumes = volumes
}
}
func tweakUpdateStrategyType(t apps.StatefulSetUpdateStrategyType) statefulSetTweak {
return func(ss *apps.StatefulSet) {
ss.Spec.UpdateStrategy.Type = t
@@ -201,6 +207,18 @@ func tweakServiceName(name string) statefulSetTweak {
}
}
func tweakSubdomain(name string) statefulSetTweak {
return func(ss *apps.StatefulSet) {
ss.Spec.Template.Spec.Subdomain = name
}
}
func tweakHostname(name string) statefulSetTweak {
return func(ss *apps.StatefulSet) {
ss.Spec.Template.Spec.Hostname = name
}
}
func TestValidateStatefulSet(t *testing.T) {
validLabels := map[string]string{"a": "b"}
validPodTemplate := api.PodTemplate{
@@ -209,9 +227,10 @@ func TestValidateStatefulSet(t *testing.T) {
Labels: validLabels,
},
Spec: api.PodSpec{
RestartPolicy: api.RestartPolicyAlways,
DNSPolicy: api.DNSClusterFirst,
Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent"}},
RestartPolicy: api.RestartPolicyAlways,
DNSPolicy: api.DNSClusterFirst,
Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}},
TerminationGracePeriodSeconds: ptr.To[int64](30),
},
},
}
@@ -235,7 +254,9 @@ func TestValidateStatefulSet(t *testing.T) {
ContainerPort: 12345,
Protocol: api.ProtocolTCP,
}},
TerminationMessagePolicy: api.TerminationMessageReadFile,
}},
TerminationGracePeriodSeconds: ptr.To[int64](30),
},
},
}
@@ -253,6 +274,33 @@ func TestValidateStatefulSet(t *testing.T) {
},
}
podTemplateWithVolumeMounts := api.PodTemplate{
Template: api.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: validLabels,
},
Spec: api.PodSpec{
RestartPolicy: api.RestartPolicyAlways,
DNSPolicy: api.DNSClusterFirst,
Containers: []api.Container{{
Name: "abc",
Image: "image",
ImagePullPolicy: "IfNotPresent",
Ports: []api.ContainerPort{{
ContainerPort: 12345,
Protocol: api.ProtocolTCP,
}},
VolumeMounts: []api.VolumeMount{{
Name: "test",
MountPath: "/test",
}},
TerminationMessagePolicy: api.TerminationMessageReadFile,
}},
TerminationGracePeriodSeconds: ptr.To[int64](30),
},
},
}
invalidTime := int64(60)
invalidPodTemplate2 := api.PodTemplate{
Template: api.PodTemplateSpec{
@@ -260,9 +308,10 @@ func TestValidateStatefulSet(t *testing.T) {
Labels: validLabels,
},
Spec: api.PodSpec{
RestartPolicy: api.RestartPolicyAlways,
DNSPolicy: api.DNSClusterFirst,
ActiveDeadlineSeconds: &invalidTime,
RestartPolicy: api.RestartPolicyAlways,
DNSPolicy: api.DNSClusterFirst,
ActiveDeadlineSeconds: &invalidTime,
TerminationGracePeriodSeconds: ptr.To[int64](30),
},
},
}
@@ -317,6 +366,104 @@ func TestValidateStatefulSet(t *testing.T) {
tweakReplicas(3),
tweakOrdinalsStart(2),
),
}, {
name: "invalid subdomain",
set: mkStatefulSet(&validPodTemplate,
tweakSubdomain("NoUppercaseOrSpecialCharsLike=Equals"),
),
}, {
name: "invalid hostname",
set: mkStatefulSet(&validPodTemplate,
tweakHostname("NoUppercaseOrSpecialCharsLike=Equals"),
),
}, {
name: "both invalid subdomain and hostname",
set: mkStatefulSet(&validPodTemplate,
tweakSubdomain("NoUppercaseOrSpecialCharsLike=Equals"),
tweakHostname("NoUppercaseOrSpecialCharsLike=Equals"),
),
}, {
name: "volume template in pod volumeMounts",
set: mkStatefulSet(&podTemplateWithVolumeMounts,
tweakPVCTemplate(api.PersistentVolumeClaim{
ObjectMeta: metav1.ObjectMeta{Name: "test"},
Spec: api.PersistentVolumeClaimSpec{
Resources: api.VolumeResourceRequirements{
Requests: api.ResourceList{
api.ResourceStorage: resource.MustParse("1Gi"),
},
},
},
}),
),
}, {
name: "volume templates already present in volumes",
set: mkStatefulSet(&podTemplateWithVolumeMounts,
tweakVolumes([]api.Volume{
{
Name: "test1",
VolumeSource: api.VolumeSource{
EmptyDir: &api.EmptyDirVolumeSource{},
},
},
{
Name: "test2",
VolumeSource: api.VolumeSource{
EmptyDir: &api.EmptyDirVolumeSource{},
},
},
}...),
tweakPVCTemplate([]api.PersistentVolumeClaim{
{
ObjectMeta: metav1.ObjectMeta{Name: "test"},
Spec: api.PersistentVolumeClaimSpec{
Resources: api.VolumeResourceRequirements{
Requests: api.ResourceList{
api.ResourceStorage: resource.MustParse("1Gi"),
},
},
},
}, {
ObjectMeta: metav1.ObjectMeta{Name: "test1"},
Spec: api.PersistentVolumeClaimSpec{
Resources: api.VolumeResourceRequirements{
Requests: api.ResourceList{
api.ResourceStorage: resource.MustParse("1Gi"),
},
},
},
}, {
ObjectMeta: metav1.ObjectMeta{Name: "test2"},
Spec: api.PersistentVolumeClaimSpec{
Resources: api.VolumeResourceRequirements{
Requests: api.ResourceList{
api.ResourceStorage: resource.MustParse("1Gi"),
},
},
},
}}...),
),
}, {
name: "volume templates present in volumes",
set: mkStatefulSet(&podTemplateWithVolumeMounts,
tweakVolumes([]api.Volume{
{
Name: "test",
VolumeSource: api.VolumeSource{
EmptyDir: &api.EmptyDirVolumeSource{},
},
}}...),
tweakPVCTemplate(api.PersistentVolumeClaim{
ObjectMeta: metav1.ObjectMeta{Name: "test"},
Spec: api.PersistentVolumeClaimSpec{
Resources: api.VolumeResourceRequirements{
Requests: api.ResourceList{
api.ResourceStorage: resource.MustParse("1Gi"),
},
},
},
}),
),
},
}
@@ -407,6 +554,7 @@ func TestValidateStatefulSet(t *testing.T) {
set: mkStatefulSet(&validPodTemplate, tweakTemplateRestartPolicy("")),
errs: field.ErrorList{
field.NotSupported[string](field.NewPath("spec", "template", "spec", "restartPolicy"), nil, nil),
field.Required(field.NewPath("spec", "template", "spec", "restartPolicy"), ""),
},
}, {
name: "invalid update strategy",
@@ -465,6 +613,7 @@ func TestValidateStatefulSet(t *testing.T) {
set: mkStatefulSet(&invalidPodTemplate2, tweakReplicas(3)),
errs: field.ErrorList{
field.Forbidden(field.NewPath("spec", "template", "spec", "activeDeadlineSeconds"), ""),
field.Required(field.NewPath("spec", "template", "spec", "containers"), ""),
},
}, {
name: "empty PersistentVolumeClaimRetentionPolicy",
@@ -534,6 +683,48 @@ func TestValidateStatefulSet(t *testing.T) {
errs: field.ErrorList{
field.Invalid(field.NewPath("spec", "serviceName"), "Invalid.Name", ""),
},
}, {
name: "missing volume in pod volumeMounts",
set: mkStatefulSet(&podTemplateWithVolumeMounts),
errs: field.ErrorList{
field.NotFound(field.NewPath("spec", "template", "spec", "containers[0].volumeMounts[0].name"), "test"),
},
}, {
name: "want test volumeMount but have test1 volumeTemplate",
set: mkStatefulSet(&podTemplateWithVolumeMounts,
tweakPVCTemplate(api.PersistentVolumeClaim{
ObjectMeta: metav1.ObjectMeta{Name: "test1"},
Spec: api.PersistentVolumeClaimSpec{
Resources: api.VolumeResourceRequirements{
Requests: api.ResourceList{
api.ResourceStorage: resource.MustParse("1Gi"),
},
},
},
})),
errs: field.ErrorList{
field.NotFound(field.NewPath("spec", "template", "spec", "containers[0].volumeMounts[0].name"), "test"),
},
}, {
name: "duplicate volumes",
set: mkStatefulSet(&validPodTemplate,
tweakVolumes([]api.Volume{
{
Name: "test1",
VolumeSource: api.VolumeSource{
EmptyDir: &api.EmptyDirVolumeSource{},
},
},
{
Name: "test1",
VolumeSource: api.VolumeSource{
EmptyDir: &api.EmptyDirVolumeSource{},
},
},
}...)),
errs: field.ErrorList{
field.Duplicate(field.NewPath("spec", "template", "spec", "volumes[1]", "name"), "test1"),
},
},
}
@@ -566,9 +757,10 @@ func generateStatefulSetSpec(minSeconds int32) *apps.StatefulSetSpec {
Labels: labels,
},
Spec: api.PodSpec{
RestartPolicy: api.RestartPolicyAlways,
DNSPolicy: api.DNSClusterFirst,
Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent"}},
RestartPolicy: api.RestartPolicyAlways,
DNSPolicy: api.DNSClusterFirst,
Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}},
TerminationGracePeriodSeconds: ptr.To[int64](30),
},
},
}
@@ -758,9 +950,10 @@ func TestValidateStatefulSetUpdate(t *testing.T) {
Labels: validLabels,
},
Spec: api.PodSpec{
RestartPolicy: api.RestartPolicyAlways,
DNSPolicy: api.DNSClusterFirst,
Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent"}},
RestartPolicy: api.RestartPolicyAlways,
DNSPolicy: api.DNSClusterFirst,
Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}},
TerminationGracePeriodSeconds: ptr.To[int64](30),
},
},
}
@@ -769,10 +962,42 @@ func TestValidateStatefulSetUpdate(t *testing.T) {
ObjectMeta: metav1.ObjectMeta{
Labels: validLabels2,
},
Spec: api.PodSpec{
RestartPolicy: api.RestartPolicyAlways,
DNSPolicy: api.DNSClusterFirst,
Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}},
TerminationGracePeriodSeconds: ptr.To[int64](30),
},
},
}
validPodTemplateWithVolumeMounts := api.PodTemplate{
Template: api.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: validLabels,
},
Spec: api.PodSpec{
RestartPolicy: api.RestartPolicyAlways,
DNSPolicy: api.DNSClusterFirst,
Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent"}},
Containers: []api.Container{{
Name: "abc",
Image: "image",
ImagePullPolicy: "IfNotPresent",
TerminationMessagePolicy: api.TerminationMessageReadFile,
VolumeMounts: []api.VolumeMount{{
Name: "pvc-abc",
MountPath: "/test",
}},
}},
TerminationGracePeriodSeconds: ptr.To[int64](30),
},
},
}
invalidLabels := map[string]string{"NoUppercaseOrSpecialCharsLike=Equals": "b"}
invalidPodTemplate := api.PodTemplate{
Template: api.PodTemplateSpec{
Spec: podtest.MakePodSpec(podtest.SetContainers()),
ObjectMeta: metav1.ObjectMeta{
Labels: invalidLabels,
},
},
}
@@ -815,7 +1040,7 @@ func TestValidateStatefulSetUpdate(t *testing.T) {
addContainersValidTemplate := validPodTemplate.DeepCopy()
addContainersValidTemplate.Template.Spec.Containers = append(addContainersValidTemplate.Template.Spec.Containers,
api.Container{Name: "def", Image: "image2", ImagePullPolicy: "IfNotPresent"})
api.Container{Name: "def", Image: "image2", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile})
if len(addContainersValidTemplate.Template.Spec.Containers) != len(validPodTemplate.Template.Spec.Containers)+1 {
t.Errorf("failure during test setup: template %v should have more containers than template %v", addContainersValidTemplate, validPodTemplate)
}
@@ -882,6 +1107,38 @@ func TestValidateStatefulSetUpdate(t *testing.T) {
name: "update with invalid .spec.serviceName",
old: mkStatefulSet(&validPodTemplate, tweakServiceName("Invalid.Name")),
update: mkStatefulSet(&validPodTemplate, tweakServiceName("Invalid.Name"), tweakReplicas(3)),
}, {
name: "invalid old spec (missing volume template) should skip validation",
old: mkStatefulSet(&validPodTemplateWithVolumeMounts),
update: mkStatefulSet(&validPodTemplateWithVolumeMounts, tweakReplicas(3)),
}, {
name: "volume mount ",
old: mkStatefulSet(&validPodTemplate, tweakPVCTemplate(validPVCTemplate)),
update: mkStatefulSet(&validPodTemplateWithVolumeMounts, tweakPVCTemplate(validPVCTemplate), tweakReplicas(3)),
}, {
name: "valid old spec, invalid subdomain in new spec",
old: mkStatefulSet(&validPodTemplate),
update: mkStatefulSet(&validPodTemplate,
tweakSubdomain("NoUppercaseOrSpecialCharsLike=Equals"),
),
}, {
name: "valid old spec, invalid hostname in new spec",
old: mkStatefulSet(&validPodTemplate),
update: mkStatefulSet(&validPodTemplate,
tweakHostname("NoUppercaseOrSpecialCharsLike=Equals"),
),
}, {
name: "both invalid subdomain",
old: mkStatefulSet(&validPodTemplate, tweakSubdomain("NoUppercaseOrSpecialCharsLike=Equals")),
update: mkStatefulSet(&validPodTemplate,
tweakSubdomain("NoUppercaseOrSpecialCharsLike=Equals"),
),
}, {
name: "both invalid hostname",
old: mkStatefulSet(&validPodTemplate, tweakHostname("NoUppercaseOrSpecialCharsLike=Equals")),
update: mkStatefulSet(&validPodTemplate,
tweakHostname("NoUppercaseOrSpecialCharsLike=Equals"),
),
},
}
@@ -950,6 +1207,19 @@ func TestValidateStatefulSetUpdate(t *testing.T) {
errs: field.ErrorList{
field.Forbidden(field.NewPath("spec"), ""),
},
}, {
name: "valid old spec but invalid new spec",
old: mkStatefulSet(&validPodTemplate),
update: mkStatefulSet(&invalidPodTemplate),
errs: field.ErrorList{
field.Invalid(field.NewPath("spec", "template", "labels"),
"NoUppercaseOrSpecialCharsLike=Equals", ""),
field.Invalid(field.NewPath("spec", "template", "labels"),
"map[string]string{\\\"NoUppercaseOrSpecialCharsLike=Equals\\\":\\\"b\\\"}", ""),
field.Invalid(field.NewPath("spec", "template", "metadata", "labels"),
"map[string]string{\\\"NoUppercaseOrSpecialCharsLike=Equals\\\":\\\"b\\\"}", ""),
field.Required(field.NewPath("spec", "template", "spec", "containers"), ""),
},
},
}

View File

@@ -38,6 +38,7 @@ import (
"k8s.io/kubernetes/pkg/apis/autoscaling"
api "k8s.io/kubernetes/pkg/apis/core"
"k8s.io/kubernetes/pkg/registry/registrytest"
"k8s.io/utils/ptr"
)
// TODO: allow for global factory override
@@ -68,13 +69,15 @@ func validNewStatefulSet() *apps.StatefulSet {
Spec: api.PodSpec{
Containers: []api.Container{
{
Name: "test",
Image: "test_image",
ImagePullPolicy: api.PullIfNotPresent,
Name: "test",
Image: "test_image",
ImagePullPolicy: api.PullIfNotPresent,
TerminationMessagePolicy: api.TerminationMessageReadFile,
},
},
RestartPolicy: api.RestartPolicyAlways,
DNSPolicy: api.DNSClusterFirst,
RestartPolicy: api.RestartPolicyAlways,
DNSPolicy: api.DNSClusterFirst,
TerminationGracePeriodSeconds: ptr.To[int64](30),
},
},
Replicas: 7,

View File

@@ -48,9 +48,10 @@ func TestStatefulSetStrategy(t *testing.T) {
Labels: validSelector,
},
Spec: api.PodSpec{
RestartPolicy: api.RestartPolicyAlways,
DNSPolicy: api.DNSClusterFirst,
Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent"}},
RestartPolicy: api.RestartPolicyAlways,
DNSPolicy: api.DNSClusterFirst,
Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}},
TerminationGracePeriodSeconds: ptr.To[int64](30),
},
},
}

View File

@@ -162,7 +162,7 @@ func GetEtcdStorageDataForNamespaceServedAt(namespace string, v string, isEmulat
IntroducedVersion: "1.9",
},
gvr("apps", "v1", "statefulsets"): {
Stub: `{"metadata": {"name": "ss3"}, "spec": {"selector": {"matchLabels": {"a": "b"}}, "template": {"metadata": {"labels": {"a": "b"}}}}}`,
Stub: `{"metadata": {"name": "ss3"}, "spec": {"selector": {"matchLabels": {"a": "b"}}, "template": {"metadata": {"labels": {"a": "b"}}, "spec": {"restartPolicy": "Always", "terminationGracePeriodSeconds": 30, "containers": [{"image": "` + image + `", "name": "container6", "terminationMessagePolicy": "File"}]}}}}`,
ExpectedEtcdPath: "/registry/statefulsets/" + namespace + "/ss3",
IntroducedVersion: "1.9",
},