Add Ephemeral Containers to the Kubernetes core API

This commit is contained in:
Lee Verberne
2018-08-09 15:24:23 +02:00
parent c7ffc1cd8c
commit 013f049ce0
20 changed files with 1492 additions and 64 deletions

View File

@@ -5527,6 +5527,243 @@ func getResourceLimits(cpu, memory string) core.ResourceList {
return res
}
func TestValidateEphemeralContainers(t *testing.T) {
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.EphemeralContainers, true)()
containers := []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}
initContainers := []core.Container{{Name: "ictr", Image: "iimage", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}
vols := map[string]core.VolumeSource{"vol": {EmptyDir: &core.EmptyDirVolumeSource{}}}
// Success Cases
for title, ephemeralContainers := range map[string][]core.EphemeralContainer{
"Empty Ephemeral Containers": {},
"Single Container": {
{EphemeralContainerCommon: core.EphemeralContainerCommon{Name: "debug", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}},
},
"Multiple Containers": {
{EphemeralContainerCommon: core.EphemeralContainerCommon{Name: "debug1", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}},
{EphemeralContainerCommon: core.EphemeralContainerCommon{Name: "debug2", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}},
},
"Single Container with Target": {
{
EphemeralContainerCommon: core.EphemeralContainerCommon{Name: "debug", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"},
TargetContainerName: "ctr",
},
},
"All Whitelisted Fields": {
{
EphemeralContainerCommon: core.EphemeralContainerCommon{
Name: "debug",
Image: "image",
Command: []string{"bash"},
Args: []string{"bash"},
WorkingDir: "/",
EnvFrom: []core.EnvFromSource{
{
ConfigMapRef: &core.ConfigMapEnvSource{
LocalObjectReference: core.LocalObjectReference{Name: "dummy"},
Optional: &[]bool{true}[0],
},
},
},
Env: []core.EnvVar{
{Name: "TEST", Value: "TRUE"},
},
VolumeMounts: []core.VolumeMount{
{Name: "vol", MountPath: "/vol"},
},
TerminationMessagePath: "/dev/termination-log",
TerminationMessagePolicy: "File",
ImagePullPolicy: "IfNotPresent",
Stdin: true,
StdinOnce: true,
TTY: true,
},
},
},
} {
if errs := validateEphemeralContainers(ephemeralContainers, containers, initContainers, vols, field.NewPath("ephemeralContainers")); len(errs) != 0 {
t.Errorf("expected success for '%s' but got errors: %v", title, errs)
}
}
// Failure Cases
tcs := []struct {
title string
ephemeralContainers []core.EphemeralContainer
expectedError field.Error
}{
{
"Name Collision with Container.Containers",
[]core.EphemeralContainer{
{EphemeralContainerCommon: core.EphemeralContainerCommon{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}},
{EphemeralContainerCommon: core.EphemeralContainerCommon{Name: "debug1", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}},
},
field.Error{Type: field.ErrorTypeDuplicate, Field: "ephemeralContainers[0].name"},
},
{
"Name Collision with Container.InitContainers",
[]core.EphemeralContainer{
{EphemeralContainerCommon: core.EphemeralContainerCommon{Name: "ictr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}},
{EphemeralContainerCommon: core.EphemeralContainerCommon{Name: "debug1", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}},
},
field.Error{Type: field.ErrorTypeDuplicate, Field: "ephemeralContainers[0].name"},
},
{
"Name Collision with EphemeralContainers",
[]core.EphemeralContainer{
{EphemeralContainerCommon: core.EphemeralContainerCommon{Name: "debug1", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}},
{EphemeralContainerCommon: core.EphemeralContainerCommon{Name: "debug1", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}},
},
field.Error{Type: field.ErrorTypeDuplicate, Field: "ephemeralContainers[1].name"},
},
{
"empty Container Container",
[]core.EphemeralContainer{
{EphemeralContainerCommon: core.EphemeralContainerCommon{}},
},
field.Error{Type: field.ErrorTypeRequired, Field: "ephemeralContainers[0]"},
},
{
"empty Container Name",
[]core.EphemeralContainer{
{EphemeralContainerCommon: core.EphemeralContainerCommon{Name: "", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}},
},
field.Error{Type: field.ErrorTypeRequired, Field: "ephemeralContainers[0]"},
},
{
"whitespace padded image name",
[]core.EphemeralContainer{
{EphemeralContainerCommon: core.EphemeralContainerCommon{Name: "debug", Image: " image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}},
},
field.Error{Type: field.ErrorTypeInvalid, Field: "ephemeralContainers[0][0].image"},
},
{
"TargetContainerName doesn't exist",
[]core.EphemeralContainer{
{
EphemeralContainerCommon: core.EphemeralContainerCommon{Name: "debug", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"},
TargetContainerName: "bogus",
},
},
field.Error{Type: field.ErrorTypeNotFound, Field: "ephemeralContainers[0].targetContainerName"},
},
{
"Container uses non-whitelisted field: Lifecycle",
[]core.EphemeralContainer{
{
EphemeralContainerCommon: core.EphemeralContainerCommon{
Name: "debug",
Image: "image",
ImagePullPolicy: "IfNotPresent",
TerminationMessagePolicy: "File",
Lifecycle: &core.Lifecycle{
PreStop: &core.Handler{
Exec: &core.ExecAction{Command: []string{"ls", "-l"}},
},
},
},
},
},
field.Error{Type: field.ErrorTypeForbidden, Field: "ephemeralContainers[0].lifecycle"},
},
{
"Container uses non-whitelisted field: LivenessProbe",
[]core.EphemeralContainer{
{
EphemeralContainerCommon: core.EphemeralContainerCommon{
Name: "debug",
Image: "image",
ImagePullPolicy: "IfNotPresent",
TerminationMessagePolicy: "File",
LivenessProbe: &core.Probe{
Handler: core.Handler{
TCPSocket: &core.TCPSocketAction{Port: intstr.FromInt(80)},
},
SuccessThreshold: 1,
},
},
},
},
field.Error{Type: field.ErrorTypeForbidden, Field: "ephemeralContainers[0].livenessProbe"},
},
{
"Container uses non-whitelisted field: Ports",
[]core.EphemeralContainer{
{
EphemeralContainerCommon: core.EphemeralContainerCommon{
Name: "debug",
Image: "image",
ImagePullPolicy: "IfNotPresent",
TerminationMessagePolicy: "File",
Ports: []core.ContainerPort{
{Protocol: "TCP", ContainerPort: 80},
},
},
},
},
field.Error{Type: field.ErrorTypeForbidden, Field: "ephemeralContainers[0].ports"},
},
{
"Container uses non-whitelisted field: ReadinessProbe",
[]core.EphemeralContainer{
{
EphemeralContainerCommon: core.EphemeralContainerCommon{
Name: "debug",
Image: "image",
ImagePullPolicy: "IfNotPresent",
TerminationMessagePolicy: "File",
ReadinessProbe: &core.Probe{
Handler: core.Handler{
TCPSocket: &core.TCPSocketAction{Port: intstr.FromInt(80)},
},
},
},
},
},
field.Error{Type: field.ErrorTypeForbidden, Field: "ephemeralContainers[0].readinessProbe"},
},
{
"Container uses non-whitelisted field: Resources",
[]core.EphemeralContainer{
{
EphemeralContainerCommon: core.EphemeralContainerCommon{
Name: "debug",
Image: "image",
ImagePullPolicy: "IfNotPresent",
TerminationMessagePolicy: "File",
Resources: core.ResourceRequirements{
Limits: core.ResourceList{
core.ResourceName(core.ResourceCPU): resource.MustParse("10"),
},
},
},
},
},
field.Error{Type: field.ErrorTypeForbidden, Field: "ephemeralContainers[0].resources"},
},
}
for _, tc := range tcs {
errs := validateEphemeralContainers(tc.ephemeralContainers, containers, initContainers, vols, field.NewPath("ephemeralContainers"))
if len(errs) == 0 {
t.Errorf("for test %q, expected error but received none", tc.title)
} else if len(errs) > 1 {
t.Errorf("for test %q, expected 1 error but received %d: %q", tc.title, len(errs), errs)
} else {
if errs[0].Type != tc.expectedError.Type {
t.Errorf("for test %q, expected error type %q but received %q: %q", tc.title, string(tc.expectedError.Type), string(errs[0].Type), errs)
}
if errs[0].Field != tc.expectedError.Field {
t.Errorf("for test %q, expected error for field %q but received error for field %q: %q", tc.title, tc.expectedError.Field, errs[0].Field, errs)
}
}
}
}
func TestValidateContainers(t *testing.T) {
volumeDevices := make(map[string]core.VolumeSource)
capabilities.SetForTests(capabilities.Capabilities{
@@ -6330,6 +6567,7 @@ func TestValidatePodSpec(t *testing.T) {
minGroupID := int64(0)
maxGroupID := int64(2147483647)
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.EphemeralContainers, true)()
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.RuntimeClass, true)()
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.PodOverhead, true)()
@@ -6672,6 +6910,34 @@ func TestValidatePodSpec(t *testing.T) {
t.Errorf("expected failure for %q", k)
}
}
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.EphemeralContainers, false)()
featuregatedCases := map[string]core.PodSpec{
"disabled by EphemeralContainers feature-gate": {
Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}},
EphemeralContainers: []core.EphemeralContainer{
{
EphemeralContainerCommon: core.EphemeralContainerCommon{
Name: "debug",
Image: "image",
ImagePullPolicy: "IfNotPresent",
TerminationMessagePolicy: "File",
},
},
},
RestartPolicy: core.RestartPolicyAlways,
DNSPolicy: core.DNSClusterFirst,
},
}
for expectedErr, spec := range featuregatedCases {
errs := ValidatePodSpec(&spec, field.NewPath("field"))
if len(errs) == 0 {
t.Errorf("expected failure due to gated feature: %s\n%+v", expectedErr, spec)
} else if actualErr := errs.ToAggregate().Error(); !strings.Contains(actualErr, expectedErr) {
t.Errorf("unexpected error message for gated feature. Expected error: %s\nActual error: %s", expectedErr, actualErr)
}
}
}
func extendPodSpecwithTolerations(in core.PodSpec, tolerations []core.Toleration) core.PodSpec {
@@ -8321,6 +8587,27 @@ func TestValidatePodUpdate(t *testing.T) {
"spec.initContainers[0].image",
"init container image change to empty",
},
{
core.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "foo"},
Spec: core.PodSpec{
EphemeralContainers: []core.EphemeralContainer{
{
EphemeralContainerCommon: core.EphemeralContainerCommon{
Name: "ephemeral",
Image: "busybox",
},
},
},
},
},
core.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "foo"},
Spec: core.PodSpec{},
},
"Forbidden: pod updates may not change fields other than",
"ephemeralContainer changes are not allowed via normal pod update",
},
{
core.Pod{
Spec: core.PodSpec{},
@@ -8902,6 +9189,272 @@ func makeValidService() core.Service {
}
}
func TestValidatePodEphemeralContainersUpdate(t *testing.T) {
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.EphemeralContainers, true)()
tests := []struct {
new []core.EphemeralContainer
old []core.EphemeralContainer
err string
test string
}{
{[]core.EphemeralContainer{}, []core.EphemeralContainer{}, "", "nothing"},
{
[]core.EphemeralContainer{{
EphemeralContainerCommon: core.EphemeralContainerCommon{
Name: "debugger",
Image: "busybox",
ImagePullPolicy: "IfNotPresent",
TerminationMessagePolicy: "File",
},
}, {
EphemeralContainerCommon: core.EphemeralContainerCommon{
Name: "debugger2",
Image: "busybox",
ImagePullPolicy: "IfNotPresent",
TerminationMessagePolicy: "File",
},
}},
[]core.EphemeralContainer{{
EphemeralContainerCommon: core.EphemeralContainerCommon{
Name: "debugger",
Image: "busybox",
ImagePullPolicy: "IfNotPresent",
TerminationMessagePolicy: "File",
},
}, {
EphemeralContainerCommon: core.EphemeralContainerCommon{
Name: "debugger2",
Image: "busybox",
ImagePullPolicy: "IfNotPresent",
TerminationMessagePolicy: "File",
},
}},
"",
"No change in Ephemeral Containers",
},
{
[]core.EphemeralContainer{{
EphemeralContainerCommon: core.EphemeralContainerCommon{
Name: "debugger",
Image: "busybox",
ImagePullPolicy: "IfNotPresent",
TerminationMessagePolicy: "File",
},
}, {
EphemeralContainerCommon: core.EphemeralContainerCommon{
Name: "debugger2",
Image: "busybox",
ImagePullPolicy: "IfNotPresent",
TerminationMessagePolicy: "File",
},
}},
[]core.EphemeralContainer{{
EphemeralContainerCommon: core.EphemeralContainerCommon{
Name: "debugger2",
Image: "busybox",
ImagePullPolicy: "IfNotPresent",
TerminationMessagePolicy: "File",
},
}, {
EphemeralContainerCommon: core.EphemeralContainerCommon{
Name: "debugger",
Image: "busybox",
ImagePullPolicy: "IfNotPresent",
TerminationMessagePolicy: "File",
},
}},
"",
"Ephemeral Container list order changes",
},
{
[]core.EphemeralContainer{{
EphemeralContainerCommon: core.EphemeralContainerCommon{
Name: "debugger",
Image: "busybox",
ImagePullPolicy: "IfNotPresent",
TerminationMessagePolicy: "File",
},
}},
[]core.EphemeralContainer{},
"",
"Add an Ephemeral Container",
},
{
[]core.EphemeralContainer{{
EphemeralContainerCommon: core.EphemeralContainerCommon{
Name: "debugger1",
Image: "busybox",
ImagePullPolicy: "IfNotPresent",
TerminationMessagePolicy: "File",
},
}, {
EphemeralContainerCommon: core.EphemeralContainerCommon{
Name: "debugger2",
Image: "busybox",
ImagePullPolicy: "IfNotPresent",
TerminationMessagePolicy: "File",
},
}},
[]core.EphemeralContainer{},
"",
"Add two Ephemeral Containers",
},
{
[]core.EphemeralContainer{{
EphemeralContainerCommon: core.EphemeralContainerCommon{
Name: "debugger",
Image: "busybox",
ImagePullPolicy: "IfNotPresent",
TerminationMessagePolicy: "File",
},
}, {
EphemeralContainerCommon: core.EphemeralContainerCommon{
Name: "debugger2",
Image: "busybox",
ImagePullPolicy: "IfNotPresent",
TerminationMessagePolicy: "File",
},
}},
[]core.EphemeralContainer{{
EphemeralContainerCommon: core.EphemeralContainerCommon{
Name: "debugger",
Image: "busybox",
ImagePullPolicy: "IfNotPresent",
TerminationMessagePolicy: "File",
},
}},
"",
"Add to an existing Ephemeral Containers",
},
{
[]core.EphemeralContainer{{
EphemeralContainerCommon: core.EphemeralContainerCommon{
Name: "debugger3",
Image: "busybox",
ImagePullPolicy: "IfNotPresent",
TerminationMessagePolicy: "File",
},
}, {
EphemeralContainerCommon: core.EphemeralContainerCommon{
Name: "debugger2",
Image: "busybox",
ImagePullPolicy: "IfNotPresent",
TerminationMessagePolicy: "File",
},
}, {
EphemeralContainerCommon: core.EphemeralContainerCommon{
Name: "debugger",
Image: "busybox",
ImagePullPolicy: "IfNotPresent",
TerminationMessagePolicy: "File",
},
}},
[]core.EphemeralContainer{{
EphemeralContainerCommon: core.EphemeralContainerCommon{
Name: "debugger",
Image: "busybox",
ImagePullPolicy: "IfNotPresent",
TerminationMessagePolicy: "File",
},
}, {
EphemeralContainerCommon: core.EphemeralContainerCommon{
Name: "debugger2",
Image: "busybox",
ImagePullPolicy: "IfNotPresent",
TerminationMessagePolicy: "File",
},
}},
"",
"Add to an existing Ephemeral Containers, list order changes",
},
{
[]core.EphemeralContainer{},
[]core.EphemeralContainer{{
EphemeralContainerCommon: core.EphemeralContainerCommon{
Name: "debugger",
Image: "busybox",
ImagePullPolicy: "IfNotPresent",
TerminationMessagePolicy: "File",
},
}},
"may not be removed",
"Remove an Ephemeral Container",
},
{
[]core.EphemeralContainer{{
EphemeralContainerCommon: core.EphemeralContainerCommon{
Name: "firstone",
Image: "busybox",
ImagePullPolicy: "IfNotPresent",
TerminationMessagePolicy: "File",
},
}},
[]core.EphemeralContainer{{
EphemeralContainerCommon: core.EphemeralContainerCommon{
Name: "thentheother",
Image: "busybox",
ImagePullPolicy: "IfNotPresent",
TerminationMessagePolicy: "File",
},
}},
"may not be removed",
"Replace an Ephemeral Container",
},
{
[]core.EphemeralContainer{{
EphemeralContainerCommon: core.EphemeralContainerCommon{
Name: "debugger1",
Image: "busybox",
ImagePullPolicy: "IfNotPresent",
TerminationMessagePolicy: "File",
},
}, {
EphemeralContainerCommon: core.EphemeralContainerCommon{
Name: "debugger2",
Image: "busybox",
ImagePullPolicy: "IfNotPresent",
TerminationMessagePolicy: "File",
},
}},
[]core.EphemeralContainer{{
EphemeralContainerCommon: core.EphemeralContainerCommon{
Name: "debugger1",
Image: "debian",
ImagePullPolicy: "IfNotPresent",
TerminationMessagePolicy: "File",
},
}, {
EphemeralContainerCommon: core.EphemeralContainerCommon{
Name: "debugger2",
Image: "busybox",
ImagePullPolicy: "IfNotPresent",
TerminationMessagePolicy: "File",
},
}},
"may not be changed",
"Change an Ephemeral Containers",
},
}
for _, test := range tests {
new := core.Pod{Spec: core.PodSpec{EphemeralContainers: test.new}}
old := core.Pod{Spec: core.PodSpec{EphemeralContainers: test.old}}
errs := ValidatePodEphemeralContainersUpdate(&new, &old)
if test.err == "" {
if len(errs) != 0 {
t.Errorf("unexpected invalid: %s (%+v)\nA: %+v\nB: %+v", test.test, errs, test.new, test.old)
}
} else {
if len(errs) == 0 {
t.Errorf("unexpected valid: %s\nA: %+v\nB: %+v", test.test, test.new, test.old)
} else if actualErr := errs.ToAggregate().Error(); !strings.Contains(actualErr, test.err) {
t.Errorf("unexpected error message: %s\nExpected error: %s\nActual error: %s", test.test, test.err, actualErr)
}
}
}
}
func TestValidateService(t *testing.T) {
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.SCTPSupport, true)()
@@ -10006,6 +10559,8 @@ func TestValidateReplicationControllerUpdate(t *testing.T) {
}
func TestValidateReplicationController(t *testing.T) {
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.EphemeralContainers, true)()
validSelector := map[string]string{"a": "b"}
validPodTemplate := core.PodTemplate{
Template: core.PodTemplateSpec{
@@ -10199,6 +10754,24 @@ func TestValidateReplicationController(t *testing.T) {
},
},
},
"template may not contain ephemeral containers": {
ObjectMeta: metav1.ObjectMeta{Name: "abc-123", Namespace: metav1.NamespaceDefault},
Spec: core.ReplicationControllerSpec{
Replicas: 1,
Selector: validSelector,
Template: &core.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: validSelector,
},
Spec: core.PodSpec{
RestartPolicy: core.RestartPolicyAlways,
DNSPolicy: core.DNSClusterFirst,
Containers: []core.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}},
EphemeralContainers: []core.EphemeralContainer{{EphemeralContainerCommon: core.EphemeralContainerCommon{Name: "debug", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}},
},
},
},
},
}
for k, v := range errorCases {
errs := ValidateReplicationController(&v)