Merge pull request #86377 from wojtek-t/immutable_secrets_api

API for immutable Secrets and ConfigMaps
This commit is contained in:
Kubernetes Prow Robot 2020-01-18 05:25:35 -08:00 committed by GitHub
commit 37d9c22abe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 1419 additions and 912 deletions

View File

@ -5354,6 +5354,10 @@
"description": "Data contains the configuration data. Each key must consist of alphanumeric characters, '-', '_' or '.'. Values with non-UTF-8 byte sequences must use the BinaryData field. The keys stored in Data must not overlap with the keys in the BinaryData field, this is enforced during validation process.",
"type": "object"
},
"immutable": {
"description": "Immutable, if set to true, ensures that data stored in the ConfigMap cannot be updated (only object metadata can be modified). If not set to true, the field can be modified at any time. Defaulted to nil. This is an alpha field enabled by ImmutableEphemeralVolumes feature gate.",
"type": "boolean"
},
"kind": {
"description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
"type": "string"
@ -9455,6 +9459,10 @@
"description": "Data contains the secret data. Each key must consist of alphanumeric characters, '-', '_' or '.'. The serialized form of the secret data is a base64 encoded string, representing the arbitrary (possibly non-string) data value here. Described in https://tools.ietf.org/html/rfc4648#section-4",
"type": "object"
},
"immutable": {
"description": "Immutable, if set to true, ensures that data stored in the Secret cannot be updated (only object metadata can be modified). If not set to true, the field can be modified at any time. Defaulted to nil. This is an alpha field enabled by ImmutableEphemeralVolumes feature gate.",
"type": "boolean"
},
"kind": {
"description": "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds",
"type": "string"

View File

@ -4735,6 +4735,12 @@ type Secret struct {
// +optional
metav1.ObjectMeta
// Immutable field, if set, ensures that data stored in the Secret cannot
// be updated (only object metadata can be modified).
// This is an alpha field enabled by ImmutableEphemeralVolumes feature gate.
// +optional
Immutable *bool
// Data contains the secret data. Each key must consist of alphanumeric
// characters, '-', '_' or '.'. The serialized form of the secret data is a
// base64 encoded string, representing the arbitrary (possibly non-string)
@ -4857,6 +4863,12 @@ type ConfigMap struct {
// +optional
metav1.ObjectMeta
// Immutable field, if set, ensures that data stored in the ConfigMap cannot
// be updated (only object metadata can be modified).
// This is an alpha field enabled by ImmutableEphemeralVolumes feature gate.
// +optional
Immutable *bool
// Data contains the configuration data.
// Each key must consist of alphanumeric characters, '-', '_' or '.'.
// Values with non-UTF-8 byte sequences must use the BinaryData field.

View File

@ -2642,6 +2642,7 @@ func Convert_core_ComponentStatusList_To_v1_ComponentStatusList(in *core.Compone
func autoConvert_v1_ConfigMap_To_core_ConfigMap(in *v1.ConfigMap, out *core.ConfigMap, s conversion.Scope) error {
out.ObjectMeta = in.ObjectMeta
out.Immutable = (*bool)(unsafe.Pointer(in.Immutable))
out.Data = *(*map[string]string)(unsafe.Pointer(&in.Data))
out.BinaryData = *(*map[string][]byte)(unsafe.Pointer(&in.BinaryData))
return nil
@ -2654,6 +2655,7 @@ func Convert_v1_ConfigMap_To_core_ConfigMap(in *v1.ConfigMap, out *core.ConfigMa
func autoConvert_core_ConfigMap_To_v1_ConfigMap(in *core.ConfigMap, out *v1.ConfigMap, s conversion.Scope) error {
out.ObjectMeta = in.ObjectMeta
out.Immutable = (*bool)(unsafe.Pointer(in.Immutable))
out.Data = *(*map[string]string)(unsafe.Pointer(&in.Data))
out.BinaryData = *(*map[string][]byte)(unsafe.Pointer(&in.BinaryData))
return nil
@ -7006,6 +7008,7 @@ func Convert_core_ScopedResourceSelectorRequirement_To_v1_ScopedResourceSelector
func autoConvert_v1_Secret_To_core_Secret(in *v1.Secret, out *core.Secret, s conversion.Scope) error {
out.ObjectMeta = in.ObjectMeta
out.Immutable = (*bool)(unsafe.Pointer(in.Immutable))
out.Data = *(*map[string][]byte)(unsafe.Pointer(&in.Data))
// INFO: in.StringData opted out of conversion generation
out.Type = core.SecretType(in.Type)
@ -7014,6 +7017,7 @@ func autoConvert_v1_Secret_To_core_Secret(in *v1.Secret, out *core.Secret, s con
func autoConvert_core_Secret_To_v1_Secret(in *core.Secret, out *v1.Secret, s conversion.Scope) error {
out.ObjectMeta = in.ObjectMeta
out.Immutable = (*bool)(unsafe.Pointer(in.Immutable))
out.Data = *(*map[string][]byte)(unsafe.Pointer(&in.Data))
out.Type = v1.SecretType(in.Type)
return nil

View File

@ -5005,6 +5005,16 @@ func ValidateSecretUpdate(newSecret, oldSecret *core.Secret) field.ErrorList {
}
allErrs = append(allErrs, ValidateImmutableField(newSecret.Type, oldSecret.Type, field.NewPath("type"))...)
if oldSecret.Immutable != nil && *oldSecret.Immutable {
if !reflect.DeepEqual(newSecret.Immutable, oldSecret.Immutable) {
allErrs = append(allErrs, field.Forbidden(field.NewPath("immutable"), "field is immutable when `immutable` is set"))
}
if !reflect.DeepEqual(newSecret.Data, oldSecret.Data) {
allErrs = append(allErrs, field.Forbidden(field.NewPath("data"), "field is immutable when `immutable` is set"))
}
// We don't validate StringData, as it was already converted back to Data
// before validation is happening.
}
allErrs = append(allErrs, ValidateSecret(newSecret)...)
return allErrs
@ -5051,8 +5061,20 @@ func ValidateConfigMap(cfg *core.ConfigMap) field.ErrorList {
func ValidateConfigMapUpdate(newCfg, oldCfg *core.ConfigMap) field.ErrorList {
allErrs := field.ErrorList{}
allErrs = append(allErrs, ValidateObjectMetaUpdate(&newCfg.ObjectMeta, &oldCfg.ObjectMeta, field.NewPath("metadata"))...)
allErrs = append(allErrs, ValidateConfigMap(newCfg)...)
if oldCfg.Immutable != nil && *oldCfg.Immutable {
if !reflect.DeepEqual(newCfg.Immutable, oldCfg.Immutable) {
allErrs = append(allErrs, field.Forbidden(field.NewPath("immutable"), "field is immutable when `immutable` is set"))
}
if !reflect.DeepEqual(newCfg.Data, oldCfg.Data) {
allErrs = append(allErrs, field.Forbidden(field.NewPath("data"), "field is immutable when `immutable` is set"))
}
if !reflect.DeepEqual(newCfg.BinaryData, oldCfg.BinaryData) {
allErrs = append(allErrs, field.Forbidden(field.NewPath("binaryData"), "field is immutable when `immutable` is set"))
}
}
allErrs = append(allErrs, ValidateConfigMap(newCfg)...)
return allErrs
}

View File

@ -13117,6 +13117,104 @@ func TestValidateSecret(t *testing.T) {
}
}
func TestValidateSecretUpdate(t *testing.T) {
validSecret := func() core.Secret {
return core.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
Namespace: "bar",
ResourceVersion: "20",
},
Data: map[string][]byte{
"data-1": []byte("bar"),
},
}
}
falseVal := false
trueVal := true
secret := validSecret()
immutableSecret := validSecret()
immutableSecret.Immutable = &trueVal
mutableSecret := validSecret()
mutableSecret.Immutable = &falseVal
secretWithData := validSecret()
secretWithData.Data["data-2"] = []byte("baz")
immutableSecretWithData := validSecret()
immutableSecretWithData.Immutable = &trueVal
immutableSecretWithData.Data["data-2"] = []byte("baz")
secretWithChangedData := validSecret()
secretWithChangedData.Data["data-1"] = []byte("foo")
immutableSecretWithChangedData := validSecret()
immutableSecretWithChangedData.Immutable = &trueVal
immutableSecretWithChangedData.Data["data-1"] = []byte("foo")
tests := []struct {
name string
oldSecret core.Secret
newSecret core.Secret
valid bool
}{
{
name: "mark secret immutable",
oldSecret: secret,
newSecret: immutableSecret,
valid: true,
},
{
name: "revert immutable secret",
oldSecret: immutableSecret,
newSecret: secret,
valid: false,
},
{
name: "makr immutable secret mutable",
oldSecret: immutableSecret,
newSecret: mutableSecret,
valid: false,
},
{
name: "add data in secret",
oldSecret: secret,
newSecret: secretWithData,
valid: true,
},
{
name: "add data in immutable secret",
oldSecret: immutableSecret,
newSecret: immutableSecretWithData,
valid: false,
},
{
name: "change data in secret",
oldSecret: secret,
newSecret: secretWithChangedData,
valid: true,
},
{
name: "change data in immutable secret",
oldSecret: immutableSecret,
newSecret: immutableSecretWithChangedData,
valid: false,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
errs := ValidateSecretUpdate(&tc.newSecret, &tc.oldSecret)
if tc.valid && len(errs) > 0 {
t.Errorf("Unexpected error: %v", errs)
}
if !tc.valid && len(errs) == 0 {
t.Errorf("Unexpected lack of error")
}
})
}
}
func TestValidateDockerConfigSecret(t *testing.T) {
validDockerSecret := func() core.Secret {
return core.Secret{
@ -13731,40 +13829,105 @@ func TestValidateConfigMapUpdate(t *testing.T) {
Data: data,
}
}
validConfigMap := func() core.ConfigMap {
return newConfigMap("1", "validname", "validdns", map[string]string{"key": "value"})
}
var (
validConfigMap = newConfigMap("1", "validname", "validns", map[string]string{"key": "value"})
noVersion = newConfigMap("", "validname", "validns", map[string]string{"key": "value"})
)
falseVal := false
trueVal := true
configMap := validConfigMap()
immutableConfigMap := validConfigMap()
immutableConfigMap.Immutable = &trueVal
mutableConfigMap := validConfigMap()
mutableConfigMap.Immutable = &falseVal
configMapWithData := validConfigMap()
configMapWithData.Data["key-2"] = "value-2"
immutableConfigMapWithData := validConfigMap()
immutableConfigMapWithData.Immutable = &trueVal
immutableConfigMapWithData.Data["key-2"] = "value-2"
configMapWithChangedData := validConfigMap()
configMapWithChangedData.Data["key"] = "foo"
immutableConfigMapWithChangedData := validConfigMap()
immutableConfigMapWithChangedData.Immutable = &trueVal
immutableConfigMapWithChangedData.Data["key"] = "foo"
noVersion := newConfigMap("", "validname", "validns", map[string]string{"key": "value"})
cases := []struct {
name string
newCfg core.ConfigMap
oldCfg core.ConfigMap
isValid bool
name string
newCfg core.ConfigMap
oldCfg core.ConfigMap
valid bool
}{
{
name: "valid",
newCfg: validConfigMap,
oldCfg: validConfigMap,
isValid: true,
name: "valid",
newCfg: configMap,
oldCfg: configMap,
valid: true,
},
{
name: "invalid",
newCfg: noVersion,
oldCfg: validConfigMap,
isValid: false,
name: "invalid",
newCfg: noVersion,
oldCfg: configMap,
valid: false,
},
{
name: "mark configmap immutable",
oldCfg: configMap,
newCfg: immutableConfigMap,
valid: true,
},
{
name: "revert immutable configmap",
oldCfg: immutableConfigMap,
newCfg: configMap,
valid: false,
},
{
name: "mark immutable configmap mutable",
oldCfg: immutableConfigMap,
newCfg: mutableConfigMap,
valid: false,
},
{
name: "add data in configmap",
oldCfg: configMap,
newCfg: configMapWithData,
valid: true,
},
{
name: "add data in immutable configmap",
oldCfg: immutableConfigMap,
newCfg: immutableConfigMapWithData,
valid: false,
},
{
name: "change data in configmap",
oldCfg: configMap,
newCfg: configMapWithChangedData,
valid: true,
},
{
name: "change data in immutable configmap",
oldCfg: immutableConfigMap,
newCfg: immutableConfigMapWithChangedData,
valid: false,
},
}
for _, tc := range cases {
errs := ValidateConfigMapUpdate(&tc.newCfg, &tc.oldCfg)
if tc.isValid && len(errs) > 0 {
t.Errorf("%v: unexpected error: %v", tc.name, errs)
}
if !tc.isValid && len(errs) == 0 {
t.Errorf("%v: unexpected non-error", tc.name)
}
t.Run(tc.name, func(t *testing.T) {
errs := ValidateConfigMapUpdate(&tc.newCfg, &tc.oldCfg)
if tc.valid && len(errs) > 0 {
t.Errorf("Unexpected error: %v", errs)
}
if !tc.valid && len(errs) == 0 {
t.Errorf("Unexpected lack of error")
}
})
}
}

View File

@ -519,6 +519,11 @@ func (in *ConfigMap) DeepCopyInto(out *ConfigMap) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
if in.Immutable != nil {
in, out := &in.Immutable, &out.Immutable
*out = new(bool)
**out = **in
}
if in.Data != nil {
in, out := &in.Data, &out.Data
*out = make(map[string]string, len(*in))
@ -4660,6 +4665,11 @@ func (in *Secret) DeepCopyInto(out *Secret) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
if in.Immutable != nil {
in, out := &in.Immutable, &out.Immutable
*out = new(bool)
**out = **in
}
if in.Data != nil {
in, out := &in.Data, &out.Data
*out = make(map[string][]byte, len(*in))

View File

@ -524,6 +524,12 @@ const (
//
// Enables topology aware service routing
ServiceTopology featuregate.Feature = "ServiceTopology"
// owner: @wojtek-t
// alpha: v1.18
//
// Enables a feature to make secrets and configmaps data immutable.
ImmutableEphemeralVolumes featuregate.Feature = "ImmutableEphemeralVolumes"
)
func init() {
@ -607,6 +613,7 @@ var defaultKubernetesFeatureGates = map[featuregate.Feature]featuregate.FeatureS
AllowInsecureBackendProxy: {Default: true, PreRelease: featuregate.Beta},
PodDisruptionBudget: {Default: true, PreRelease: featuregate.Beta},
ServiceTopology: {Default: false, PreRelease: featuregate.Alpha},
ImmutableEphemeralVolumes: {Default: false, PreRelease: featuregate.Alpha},
// inherited features from generic apiserver, relisted here to get a conflict if it is changed
// unintentionally on either side:

View File

@ -17,6 +17,7 @@ go_library(
"//pkg/api/legacyscheme:go_default_library",
"//pkg/apis/core:go_default_library",
"//pkg/apis/core/validation:go_default_library",
"//pkg/features:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/fields:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/labels:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
@ -25,6 +26,7 @@ go_library(
"//staging/src/k8s.io/apiserver/pkg/registry/rest:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/storage:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/storage/names:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/util/feature:go_default_library",
],
)

View File

@ -28,9 +28,11 @@ import (
"k8s.io/apiserver/pkg/registry/rest"
pkgstorage "k8s.io/apiserver/pkg/storage"
"k8s.io/apiserver/pkg/storage/names"
utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/kubernetes/pkg/api/legacyscheme"
api "k8s.io/kubernetes/pkg/apis/core"
"k8s.io/kubernetes/pkg/apis/core/validation"
"k8s.io/kubernetes/pkg/features"
)
// strategy implements behavior for ConfigMap objects
@ -54,7 +56,8 @@ func (strategy) NamespaceScoped() bool {
}
func (strategy) PrepareForCreate(ctx context.Context, obj runtime.Object) {
_ = obj.(*api.ConfigMap)
configMap := obj.(*api.ConfigMap)
dropDisabledFields(configMap, nil)
}
func (strategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList {
@ -72,12 +75,9 @@ func (strategy) AllowCreateOnUpdate() bool {
}
func (strategy) PrepareForUpdate(ctx context.Context, newObj, oldObj runtime.Object) {
_ = oldObj.(*api.ConfigMap)
_ = newObj.(*api.ConfigMap)
}
func (strategy) AllowUnconditionalUpdate() bool {
return true
oldConfigMap := oldObj.(*api.ConfigMap)
newConfigMap := newObj.(*api.ConfigMap)
dropDisabledFields(newConfigMap, oldConfigMap)
}
func (strategy) ValidateUpdate(ctx context.Context, newObj, oldObj runtime.Object) field.ErrorList {
@ -86,6 +86,20 @@ func (strategy) ValidateUpdate(ctx context.Context, newObj, oldObj runtime.Objec
return validation.ValidateConfigMapUpdate(newCfg, oldCfg)
}
func isImmutableInUse(configMap *api.ConfigMap) bool {
return configMap != nil && configMap.Immutable != nil
}
func dropDisabledFields(configMap *api.ConfigMap, oldConfigMap *api.ConfigMap) {
if !utilfeature.DefaultFeatureGate.Enabled(features.ImmutableEphemeralVolumes) && !isImmutableInUse(oldConfigMap) {
configMap.Immutable = nil
}
}
func (strategy) AllowUnconditionalUpdate() bool {
return true
}
// GetAttrs returns labels and fields of a given object for filtering purposes.
func GetAttrs(obj runtime.Object) (labels.Set, fields.Set, error) {
configMap, ok := obj.(*api.ConfigMap)

View File

@ -17,6 +17,7 @@ go_library(
"//pkg/api/legacyscheme:go_default_library",
"//pkg/apis/core:go_default_library",
"//pkg/apis/core/validation:go_default_library",
"//pkg/features:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/api/errors:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/fields:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/labels:go_default_library",
@ -26,6 +27,7 @@ go_library(
"//staging/src/k8s.io/apiserver/pkg/registry/rest:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/storage:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/storage/names:go_default_library",
"//staging/src/k8s.io/apiserver/pkg/util/feature:go_default_library",
],
)

View File

@ -29,9 +29,11 @@ import (
"k8s.io/apiserver/pkg/registry/rest"
pkgstorage "k8s.io/apiserver/pkg/storage"
"k8s.io/apiserver/pkg/storage/names"
utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/kubernetes/pkg/api/legacyscheme"
api "k8s.io/kubernetes/pkg/apis/core"
"k8s.io/kubernetes/pkg/apis/core/validation"
"k8s.io/kubernetes/pkg/features"
)
// strategy implements behavior for Secret objects
@ -53,6 +55,8 @@ func (strategy) NamespaceScoped() bool {
}
func (strategy) PrepareForCreate(ctx context.Context, obj runtime.Object) {
secret := obj.(*api.Secret)
dropDisabledFields(secret, nil)
}
func (strategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList {
@ -67,12 +71,25 @@ func (strategy) AllowCreateOnUpdate() bool {
}
func (strategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) {
newSecret := obj.(*api.Secret)
oldSecret := old.(*api.Secret)
dropDisabledFields(newSecret, oldSecret)
}
func (strategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList {
return validation.ValidateSecretUpdate(obj.(*api.Secret), old.(*api.Secret))
}
func isImmutableInUse(secret *api.Secret) bool {
return secret != nil && secret.Immutable != nil
}
func dropDisabledFields(secret *api.Secret, oldSecret *api.Secret) {
if !utilfeature.DefaultFeatureGate.Enabled(features.ImmutableEphemeralVolumes) && !isImmutableInUse(oldSecret) {
secret.Immutable = nil
}
}
func (strategy) AllowUnconditionalUpdate() bool {
return true
}

File diff suppressed because it is too large Load Diff

View File

@ -455,6 +455,14 @@ message ConfigMap {
// +optional
optional k8s.io.apimachinery.pkg.apis.meta.v1.ObjectMeta metadata = 1;
// Immutable, if set to true, ensures that data stored in the ConfigMap cannot
// be updated (only object metadata can be modified).
// If not set to true, the field can be modified at any time.
// Defaulted to nil.
// This is an alpha field enabled by ImmutableEphemeralVolumes feature gate.
// +optional
optional bool immutable = 4;
// Data contains the configuration data.
// Each key must consist of alphanumeric characters, '-', '_' or '.'.
// Values with non-UTF-8 byte sequences must use the BinaryData field.
@ -4256,6 +4264,14 @@ message Secret {
// +optional
optional k8s.io.apimachinery.pkg.apis.meta.v1.ObjectMeta metadata = 1;
// Immutable, if set to true, ensures that data stored in the Secret cannot
// be updated (only object metadata can be modified).
// If not set to true, the field can be modified at any time.
// Defaulted to nil.
// This is an alpha field enabled by ImmutableEphemeralVolumes feature gate.
// +optional
optional bool immutable = 5;
// Data contains the secret data. Each key must consist of alphanumeric
// characters, '-', '_' or '.'. The serialized form of the secret data is a
// base64 encoded string, representing the arbitrary (possibly non-string)

View File

@ -5424,6 +5424,14 @@ type Secret struct {
// +optional
metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`
// Immutable, if set to true, ensures that data stored in the Secret cannot
// be updated (only object metadata can be modified).
// If not set to true, the field can be modified at any time.
// Defaulted to nil.
// This is an alpha field enabled by ImmutableEphemeralVolumes feature gate.
// +optional
Immutable *bool `json:"immutable,omitempty" protobuf:"varint,5,opt,name=immutable"`
// Data contains the secret data. Each key must consist of alphanumeric
// characters, '-', '_' or '.'. The serialized form of the secret data is a
// base64 encoded string, representing the arbitrary (possibly non-string)
@ -5557,6 +5565,14 @@ type ConfigMap struct {
// +optional
metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`
// Immutable, if set to true, ensures that data stored in the ConfigMap cannot
// be updated (only object metadata can be modified).
// If not set to true, the field can be modified at any time.
// Defaulted to nil.
// This is an alpha field enabled by ImmutableEphemeralVolumes feature gate.
// +optional
Immutable *bool `json:"immutable,omitempty" protobuf:"varint,4,opt,name=immutable"`
// Data contains the configuration data.
// Each key must consist of alphanumeric characters, '-', '_' or '.'.
// Values with non-UTF-8 byte sequences must use the BinaryData field.

View File

@ -252,6 +252,7 @@ func (ComponentStatusList) SwaggerDoc() map[string]string {
var map_ConfigMap = map[string]string{
"": "ConfigMap holds configuration data for pods to consume.",
"metadata": "Standard object's metadata. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata",
"immutable": "Immutable, if set to true, ensures that data stored in the ConfigMap cannot be updated (only object metadata can be modified). If not set to true, the field can be modified at any time. Defaulted to nil. This is an alpha field enabled by ImmutableEphemeralVolumes feature gate.",
"data": "Data contains the configuration data. Each key must consist of alphanumeric characters, '-', '_' or '.'. Values with non-UTF-8 byte sequences must use the BinaryData field. The keys stored in Data must not overlap with the keys in the BinaryData field, this is enforced during validation process.",
"binaryData": "BinaryData contains the binary data. Each key must consist of alphanumeric characters, '-', '_' or '.'. BinaryData can contain byte sequences that are not in the UTF-8 range. The keys stored in BinaryData must not overlap with the ones in the Data field, this is enforced during validation process. Using this field will require 1.10+ apiserver and kubelet.",
}
@ -2015,6 +2016,7 @@ func (ScopedResourceSelectorRequirement) SwaggerDoc() map[string]string {
var map_Secret = map[string]string{
"": "Secret holds secret data of a certain type. The total bytes of the values in the Data field must be less than MaxSecretSize bytes.",
"metadata": "Standard object's metadata. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata",
"immutable": "Immutable, if set to true, ensures that data stored in the Secret cannot be updated (only object metadata can be modified). If not set to true, the field can be modified at any time. Defaulted to nil. This is an alpha field enabled by ImmutableEphemeralVolumes feature gate.",
"data": "Data contains the secret data. Each key must consist of alphanumeric characters, '-', '_' or '.'. The serialized form of the secret data is a base64 encoded string, representing the arbitrary (possibly non-string) data value here. Described in https://tools.ietf.org/html/rfc4648#section-4",
"stringData": "stringData allows specifying non-binary secret data in string form. It is provided as a write-only convenience method. All keys and values are merged into the data field on write, overwriting any existing values. It is never output when reading from the API.",
"type": "Used to facilitate programmatic handling of secret data.",

View File

@ -519,6 +519,11 @@ func (in *ConfigMap) DeepCopyInto(out *ConfigMap) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
if in.Immutable != nil {
in, out := &in.Immutable, &out.Immutable
*out = new(bool)
**out = **in
}
if in.Data != nil {
in, out := &in.Data, &out.Data
*out = make(map[string]string, len(*in))
@ -4663,6 +4668,11 @@ func (in *Secret) DeepCopyInto(out *Secret) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
if in.Immutable != nil {
in, out := &in.Immutable, &out.Immutable
*out = new(bool)
**out = **in
}
if in.Data != nil {
in, out := &in.Data, &out.Data
*out = make(map[string][]byte, len(*in))

View File

@ -40,10 +40,11 @@
}
]
},
"immutable": false,
"data": {
"19": "20"
},
"binaryData": {
"21": "Dg=="
"21": "Hg=="
}
}

View File

@ -1,8 +1,9 @@
apiVersion: v1
binaryData:
"21": Dg==
"21": Hg==
data:
"19": "20"
immutable: false
kind: ConfigMap
metadata:
annotations:

View File

@ -40,11 +40,12 @@
}
]
},
"immutable": false,
"data": {
"19": "Hg=="
"19": "xw=="
},
"stringData": {
"20": "21"
},
"type": "r鯹)晿\u003co,c鮽ort昍řČ扷5ƗǸ"
"type": "鯹)晿\u003c"
}

View File

@ -1,6 +1,7 @@
apiVersion: v1
data:
"19": Hg==
"19": xw==
immutable: false
kind: Secret
metadata:
annotations:
@ -33,4 +34,4 @@ metadata:
uid: "7"
stringData:
"20": "21"
type: r鯹)晿<o,c鮽ort昍řČ扷5ƗǸ
type: 鯹)晿<

View File

@ -90,7 +90,14 @@ func SecretHash(sec *v1.Secret) (string, error) {
// Data, Kind, and Name are taken into account.
func encodeConfigMap(cm *v1.ConfigMap) (string, error) {
// json.Marshal sorts the keys in a stable order in the encoding
m := map[string]interface{}{"kind": "ConfigMap", "name": cm.Name, "data": cm.Data}
m := map[string]interface{}{
"kind": "ConfigMap",
"name": cm.Name,
"data": cm.Data,
}
if cm.Immutable != nil {
m["immutable"] = *cm.Immutable
}
if len(cm.BinaryData) > 0 {
m["binaryData"] = cm.BinaryData
}
@ -105,7 +112,16 @@ func encodeConfigMap(cm *v1.ConfigMap) (string, error) {
// Data, Kind, Name, and Type are taken into account.
func encodeSecret(sec *v1.Secret) (string, error) {
// json.Marshal sorts the keys in a stable order in the encoding
data, err := json.Marshal(map[string]interface{}{"kind": "Secret", "type": sec.Type, "name": sec.Name, "data": sec.Data})
m := map[string]interface{}{
"kind": "Secret",
"type": sec.Type,
"name": sec.Name,
"data": sec.Data,
}
if sec.Immutable != nil {
m["immutable"] = *sec.Immutable
}
data, err := json.Marshal(m)
if err != nil {
return "", err
}

View File

@ -178,8 +178,8 @@ not their metadata (e.g. the Data of a ConfigMap, but nothing in ObjectMeta).
obj interface{}
expect int
}{
{"ConfigMap", v1.ConfigMap{}, 4},
{"Secret", v1.Secret{}, 5},
{"ConfigMap", v1.ConfigMap{}, 5},
{"Secret", v1.Secret{}, 6},
}
for _, c := range cases {
val := reflect.ValueOf(c.obj)

View File

@ -56,7 +56,14 @@ func SecretHash(sec *v1.Secret) (string, error) {
// Data, Kind, and Name are taken into account.
func encodeConfigMap(cm *v1.ConfigMap) (string, error) {
// json.Marshal sorts the keys in a stable order in the encoding
m := map[string]interface{}{"kind": "ConfigMap", "name": cm.Name, "data": cm.Data}
m := map[string]interface{}{
"kind": "ConfigMap",
"name": cm.Name,
"data": cm.Data,
}
if cm.Immutable != nil {
m["immutable"] = *cm.Immutable
}
if len(cm.BinaryData) > 0 {
m["binaryData"] = cm.BinaryData
}
@ -70,8 +77,17 @@ func encodeConfigMap(cm *v1.ConfigMap) (string, error) {
// encodeSecret encodes a Secret.
// Data, Kind, Name, and Type are taken into account.
func encodeSecret(sec *v1.Secret) (string, error) {
m := map[string]interface{}{
"kind": "Secret",
"type": sec.Type,
"name": sec.Name,
"data": sec.Data,
}
if sec.Immutable != nil {
m["immutable"] = *sec.Immutable
}
// json.Marshal sorts the keys in a stable order in the encoding
data, err := json.Marshal(map[string]interface{}{"kind": "Secret", "type": sec.Type, "name": sec.Name, "data": sec.Data})
data, err := json.Marshal(m)
if err != nil {
return "", err
}

View File

@ -164,8 +164,8 @@ not their metadata (e.g. the Data of a ConfigMap, but nothing in ObjectMeta).
obj interface{}
expect int
}{
{"ConfigMap", v1.ConfigMap{}, 4},
{"Secret", v1.Secret{}, 5},
{"ConfigMap", v1.ConfigMap{}, 5},
{"Secret", v1.Secret{}, 6},
}
for _, c := range cases {
val := reflect.ValueOf(c.obj)

View File

@ -23,6 +23,7 @@ import (
"github.com/onsi/ginkgo"
"github.com/onsi/gomega"
"k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/uuid"
"k8s.io/kubernetes/test/e2e/framework"
@ -549,9 +550,55 @@ var _ = ginkgo.Describe("[sig-storage] ConfigMap", func() {
})
//The pod is in pending during volume creation until the configMap objects are available
//or until mount the configMap volume times out. There is no configMap object defined for the pod, so it should return timout exception unless it is marked optional.
//Slow (~5 mins)
// It should be forbidden to change data for configmaps marked as immutable, but
// allowed to modify its metadata independently of its state.
ginkgo.It("should be immutable if `immutable` field is set [Feature:ImmutableEphemeralVolume]", func() {
name := "immutable"
configMap := newConfigMap(f, name)
currentConfigMap, err := f.ClientSet.CoreV1().ConfigMaps(f.Namespace.Name).Create(configMap)
framework.ExpectNoError(err, "Failed to create config map %q in namespace %q", configMap.Name, configMap.Namespace)
currentConfigMap.Data["data-4"] = "value-4"
currentConfigMap, err = f.ClientSet.CoreV1().ConfigMaps(f.Namespace.Name).Update(currentConfigMap)
framework.ExpectNoError(err, "Failed to update config map %q in namespace %q", configMap.Name, configMap.Namespace)
// Mark config map as immutable.
trueVal := true
currentConfigMap.Immutable = &trueVal
currentConfigMap, err = f.ClientSet.CoreV1().ConfigMaps(f.Namespace.Name).Update(currentConfigMap)
framework.ExpectNoError(err, "Failed to mark config map %q in namespace %q as immutable", configMap.Name, configMap.Namespace)
// Ensure data can't be changed now.
currentConfigMap.Data["data-5"] = "value-5"
_, err = f.ClientSet.CoreV1().ConfigMaps(f.Namespace.Name).Update(currentConfigMap)
framework.ExpectEqual(apierrors.IsInvalid(err), true)
// Ensure config map can't be switched from immutable to mutable.
currentConfigMap, err = f.ClientSet.CoreV1().ConfigMaps(f.Namespace.Name).Get(name, metav1.GetOptions{})
framework.ExpectNoError(err, "Failed to get config map %q in namespace %q", configMap.Name, configMap.Namespace)
framework.ExpectEqual(*currentConfigMap.Immutable, true)
falseVal := false
currentConfigMap.Immutable = &falseVal
_, err = f.ClientSet.CoreV1().ConfigMaps(f.Namespace.Name).Update(currentConfigMap)
framework.ExpectEqual(apierrors.IsInvalid(err), true)
// Ensure that metadata can be changed.
currentConfigMap, err = f.ClientSet.CoreV1().ConfigMaps(f.Namespace.Name).Get(name, metav1.GetOptions{})
framework.ExpectNoError(err, "Failed to get config map %q in namespace %q", configMap.Name, configMap.Namespace)
currentConfigMap.Labels = map[string]string{"label1": "value1"}
_, err = f.ClientSet.CoreV1().ConfigMaps(f.Namespace.Name).Update(currentConfigMap)
framework.ExpectNoError(err, "Failed to update config map %q in namespace %q", configMap.Name, configMap.Namespace)
// Ensure that immutable config map can be deleted.
err = f.ClientSet.CoreV1().ConfigMaps(f.Namespace.Name).Delete(name, &metav1.DeleteOptions{})
framework.ExpectNoError(err, "Failed to delete config map %q in namespace %q", configMap.Name, configMap.Namespace)
})
// The pod is in pending during volume creation until the configMap objects are available
// or until mount the configMap volume times out. There is no configMap object defined for the pod, so it should return timout exception unless it is marked optional.
// Slow (~5 mins)
ginkgo.It("Should fail non-optional pod creation due to configMap object does not exist [Slow]", func() {
volumeMountPath := "/etc/configmap-volumes"
podName := "pod-configmaps-" + string(uuid.NewUUID())
@ -559,9 +606,9 @@ var _ = ginkgo.Describe("[sig-storage] ConfigMap", func() {
framework.ExpectError(err, "created pod %q with non-optional configMap in namespace %q", podName, f.Namespace.Name)
})
//ConfigMap object defined for the pod, If a key is specified which is not present in the ConfigMap,
// ConfigMap object defined for the pod, If a key is specified which is not present in the ConfigMap,
// the volume setup will error unless it is marked optional, during the pod creation.
//Slow (~5 mins)
// Slow (~5 mins)
ginkgo.It("Should fail non-optional pod creation due to the key in the configMap object does not exist [Slow]", func() {
volumeMountPath := "/etc/configmap-volumes"
podName := "pod-configmaps-" + string(uuid.NewUUID())
@ -754,7 +801,7 @@ func createNonOptionalConfigMapPod(f *framework.Framework, volumeMountPath, podN
createContainerName := "createcm-volume-test"
createVolumeName := "createcm-volume"
//creating a pod without configMap object created, by mentioning the configMap volume source's local reference name
// creating a pod without configMap object created, by mentioning the configMap volume source's local reference name
pod := &v1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: podName,
@ -810,7 +857,7 @@ func createNonOptionalConfigMapPodWithConfig(f *framework.Framework, volumeMount
if configMap, err = f.ClientSet.CoreV1().ConfigMaps(f.Namespace.Name).Create(configMap); err != nil {
framework.Failf("unable to create test configMap %s: %v", configMap.Name, err)
}
//creating a pod with configMap object, but with different key which is not present in configMap object.
// creating a pod with configMap object, but with different key which is not present in configMap object.
pod := &v1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: podName,

View File

@ -21,6 +21,7 @@ import (
"path"
"k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/uuid"
"k8s.io/kubernetes/test/e2e/framework"
@ -368,9 +369,55 @@ var _ = ginkgo.Describe("[sig-storage] Secrets", func() {
gomega.Eventually(pollDeleteLogs, podLogTimeout, framework.Poll).Should(gomega.ContainSubstring("Error reading file /etc/secret-volumes/delete/data-1"))
})
//The secret is in pending during volume creation until the secret objects are available
//or until mount the secret volume times out. There is no secret object defined for the pod, so it should return timout exception unless it is marked optional.
//Slow (~5 mins)
// It should be forbidden to change data for secrets marked as immutable, but
// allowed to modify its metadata independently of its state.
ginkgo.It("should be immutable if `immutable` field is set [Feature:ImmutableEphemeralVolume]", func() {
name := "immutable"
secret := secretForTest(f.Namespace.Name, name)
currentSecret, err := f.ClientSet.CoreV1().Secrets(f.Namespace.Name).Create(secret)
framework.ExpectNoError(err, "Failed to create secret %q in namespace %q", secret.Name, secret.Namespace)
currentSecret.Data["data-4"] = []byte("value-4\n")
currentSecret, err = f.ClientSet.CoreV1().Secrets(f.Namespace.Name).Update(currentSecret)
framework.ExpectNoError(err, "Failed to update secret %q in namespace %q", secret.Name, secret.Namespace)
// Mark secret as immutable.
trueVal := true
currentSecret.Immutable = &trueVal
currentSecret, err = f.ClientSet.CoreV1().Secrets(f.Namespace.Name).Update(currentSecret)
framework.ExpectNoError(err, "Failed to mark secret %q in namespace %q as immutable", secret.Name, secret.Namespace)
// Ensure data can't be changed now.
currentSecret.Data["data-5"] = []byte("value-5\n")
_, err = f.ClientSet.CoreV1().Secrets(f.Namespace.Name).Update(currentSecret)
framework.ExpectEqual(apierrors.IsInvalid(err), true)
// Ensure secret can't be switched from immutable to mutable.
currentSecret, err = f.ClientSet.CoreV1().Secrets(f.Namespace.Name).Get(name, metav1.GetOptions{})
framework.ExpectNoError(err, "Failed to get secret %q in namespace %q", secret.Name, secret.Namespace)
framework.ExpectEqual(*currentSecret.Immutable, true)
falseVal := false
currentSecret.Immutable = &falseVal
_, err = f.ClientSet.CoreV1().Secrets(f.Namespace.Name).Update(currentSecret)
framework.ExpectEqual(apierrors.IsInvalid(err), true)
// Ensure that metadata can be changed.
currentSecret, err = f.ClientSet.CoreV1().Secrets(f.Namespace.Name).Get(name, metav1.GetOptions{})
framework.ExpectNoError(err, "Failed to get secret %q in namespace %q", secret.Name, secret.Namespace)
currentSecret.Labels = map[string]string{"label1": "value1"}
_, err = f.ClientSet.CoreV1().Secrets(f.Namespace.Name).Update(currentSecret)
framework.ExpectNoError(err, "Failed to update secret %q in namespace %q", secret.Name, secret.Namespace)
// Ensure that immutable secret can be deleted.
err = f.ClientSet.CoreV1().Secrets(f.Namespace.Name).Delete(name, &metav1.DeleteOptions{})
framework.ExpectNoError(err, "Failed to delete secret %q in namespace %q", secret.Name, secret.Namespace)
})
// The secret is in pending during volume creation until the secret objects are available
// or until mount the secret volume times out. There is no secret object defined for the pod, so it should return timout exception unless it is marked optional.
// Slow (~5 mins)
ginkgo.It("Should fail non-optional pod creation due to secret object does not exist [Slow]", func() {
volumeMountPath := "/etc/secret-volumes"
podName := "pod-secrets-" + string(uuid.NewUUID())
@ -378,9 +425,9 @@ var _ = ginkgo.Describe("[sig-storage] Secrets", func() {
framework.ExpectError(err, "created pod %q with non-optional secret in namespace %q", podName, f.Namespace.Name)
})
//Secret object defined for the pod, If a key is specified which is not present in the secret,
// Secret object defined for the pod, If a key is specified which is not present in the secret,
// the volume setup will error unless it is marked optional, during the pod creation.
//Slow (~5 mins)
// Slow (~5 mins)
ginkgo.It("Should fail non-optional pod creation due to the key in the secret object does not exist [Slow]", func() {
volumeMountPath := "/etc/secret-volumes"
podName := "pod-secrets-" + string(uuid.NewUUID())
@ -548,7 +595,7 @@ func createNonOptionalSecretPod(f *framework.Framework, volumeMountPath, podName
createContainerName := "creates-volume-test"
createVolumeName := "creates-volume"
//creating a pod without secret object created, by mentioning the secret volume source reference name
// creating a pod without secret object created, by mentioning the secret volume source reference name
pod := &v1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: podName,
@ -603,7 +650,7 @@ func createNonOptionalSecretPodWithSecret(f *framework.Framework, volumeMountPat
if secret, err = f.ClientSet.CoreV1().Secrets(f.Namespace.Name).Create(secret); err != nil {
framework.Failf("unable to create test secret %s: %v", secret.Name, err)
}
//creating a pod with secret object, with the key which is not present in secret object.
// creating a pod with secret object, with the key which is not present in secret object.
pod := &v1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: podName,