611 lines
12 KiB
Go
611 lines
12 KiB
Go
/*
|
|
Copyright 2020 The Kubernetes Authors.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package fieldmanager_test
|
|
|
|
import (
|
|
"fmt"
|
|
"reflect"
|
|
"testing"
|
|
|
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
|
"k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal"
|
|
"sigs.k8s.io/structured-merge-diff/v4/fieldpath"
|
|
"sigs.k8s.io/structured-merge-diff/v4/merge"
|
|
"sigs.k8s.io/yaml"
|
|
)
|
|
|
|
// TestApplyUsingLastAppliedAnnotation tests that applying to an object
|
|
// created with the client-side apply last-applied annotation
|
|
// will not give conflicts
|
|
func TestApplyUsingLastAppliedAnnotation(t *testing.T) {
|
|
f := NewDefaultTestFieldManager(schema.FromAPIVersionAndKind("apps/v1", "Deployment"))
|
|
|
|
tests := []struct {
|
|
lastApplied []byte
|
|
original []byte
|
|
applied []byte
|
|
fieldManager string
|
|
expectConflictSet *fieldpath.Set
|
|
}{
|
|
{
|
|
fieldManager: "kubectl",
|
|
lastApplied: []byte(`
|
|
apiVersion: apps/v1
|
|
kind: Deployment
|
|
metadata:
|
|
name: my-deployment
|
|
spec:
|
|
replicas: 3
|
|
selector:
|
|
matchLabels:
|
|
app: my-app
|
|
template:
|
|
metadata:
|
|
labels:
|
|
app: my-app
|
|
spec:
|
|
containers:
|
|
- name: my-c
|
|
image: my-image-v1
|
|
- name: my-c2
|
|
image: my-image2
|
|
`),
|
|
original: []byte(`
|
|
apiVersion: apps/v1
|
|
kind: Deployment
|
|
metadata:
|
|
name: my-deployment
|
|
labels:
|
|
app: my-app # missing from last-applied
|
|
spec:
|
|
replicas: 100 # does not match last-applied
|
|
selector:
|
|
matchLabels:
|
|
app: my-app
|
|
template:
|
|
metadata:
|
|
labels:
|
|
app: my-app
|
|
spec:
|
|
containers:
|
|
- name: my-c
|
|
image: my-image-v2 # does no match last-applied
|
|
# note that second container in last-applied is missing
|
|
`),
|
|
applied: []byte(`
|
|
# test conflicts due to fields not allowed by last-applied
|
|
|
|
apiVersion: apps/v1
|
|
kind: Deployment
|
|
metadata:
|
|
name: my-deployment
|
|
labels:
|
|
app: my-new-label # NOT allowed: update label
|
|
spec:
|
|
replicas: 333 # NOT allowed: update replicas
|
|
selector:
|
|
matchLabels:
|
|
app: my-new-label # allowed: update label
|
|
template:
|
|
metadata:
|
|
labels:
|
|
app: my-new-label # allowed: update-label
|
|
spec:
|
|
containers:
|
|
- name: my-c
|
|
image: my-image-new # NOT allowed: update image
|
|
`),
|
|
expectConflictSet: fieldpath.NewSet(
|
|
fieldpath.MakePathOrDie("metadata", "labels", "app"),
|
|
fieldpath.MakePathOrDie("spec", "replicas"),
|
|
fieldpath.MakePathOrDie("spec", "template", "spec", "containers", fieldpath.KeyByFields("name", "my-c"), "image"),
|
|
),
|
|
},
|
|
{
|
|
fieldManager: "kubectl",
|
|
lastApplied: []byte(`
|
|
apiVersion: apps/v1
|
|
kind: Deployment
|
|
metadata:
|
|
name: my-deployment
|
|
labels:
|
|
app: my-app
|
|
spec:
|
|
replicas: 3
|
|
selector:
|
|
matchLabels:
|
|
app: my-app
|
|
template:
|
|
metadata:
|
|
labels:
|
|
app: my-app
|
|
spec:
|
|
containers:
|
|
- name: my-c
|
|
image: my-image
|
|
`),
|
|
original: []byte(`
|
|
apiVersion: apps/v1
|
|
kind: Deployment
|
|
metadata:
|
|
name: my-deployment
|
|
labels:
|
|
app: my-app
|
|
spec:
|
|
replicas: 100 # does not match last applied
|
|
selector:
|
|
matchLabels:
|
|
app: my-app
|
|
template:
|
|
metadata:
|
|
labels:
|
|
app: my-app
|
|
spec:
|
|
containers:
|
|
- name: my-c
|
|
image: my-image
|
|
`),
|
|
applied: []byte(`
|
|
apiVersion: apps/v1
|
|
kind: Deployment
|
|
metadata:
|
|
name: my-deployment
|
|
labels:
|
|
app: my-new-label
|
|
spec:
|
|
replicas: 3 # expect conflict
|
|
template:
|
|
metadata:
|
|
labels:
|
|
app: my-app
|
|
spec:
|
|
containers:
|
|
- name: my-c
|
|
image: my-image
|
|
`),
|
|
expectConflictSet: fieldpath.NewSet(
|
|
fieldpath.MakePathOrDie("spec", "replicas"),
|
|
),
|
|
},
|
|
{
|
|
fieldManager: "kubectl",
|
|
original: []byte(`
|
|
apiVersion: apps/v1
|
|
kind: Deployment
|
|
metadata:
|
|
name: my-deployment
|
|
labels:
|
|
app: my-app
|
|
spec:
|
|
replicas: 100
|
|
selector:
|
|
matchLabels:
|
|
app: my-app
|
|
template:
|
|
metadata:
|
|
labels:
|
|
app: my-app
|
|
spec:
|
|
containers:
|
|
- name: my-c
|
|
image: my-image
|
|
`),
|
|
applied: []byte(`
|
|
# applied object matches original
|
|
|
|
apiVersion: apps/v1
|
|
kind: Deployment
|
|
metadata:
|
|
name: my-deployment
|
|
labels:
|
|
app: my-app
|
|
spec:
|
|
replicas: 100
|
|
selector:
|
|
matchLabels:
|
|
app: my-app
|
|
template:
|
|
metadata:
|
|
labels:
|
|
app: my-app
|
|
spec:
|
|
containers:
|
|
- name: my-c
|
|
image: my-image
|
|
`),
|
|
},
|
|
{
|
|
fieldManager: "kubectl",
|
|
original: []byte(`
|
|
apiVersion: apps/v1
|
|
kind: Deployment
|
|
metadata:
|
|
name: my-deployment
|
|
labels:
|
|
app: my-app
|
|
spec:
|
|
replicas: 3
|
|
selector:
|
|
matchLabels:
|
|
app: my-app
|
|
template:
|
|
metadata:
|
|
labels:
|
|
app: my-app
|
|
spec:
|
|
containers:
|
|
- name: my-c
|
|
image: my-image
|
|
`),
|
|
applied: []byte(`
|
|
# test allowed update with no conflicts
|
|
|
|
apiVersion: apps/v1
|
|
kind: Deployment
|
|
metadata:
|
|
name: my-deployment
|
|
labels:
|
|
app: my-new-label # update label
|
|
spec:
|
|
replicas: 333 # update replicas
|
|
selector:
|
|
matchLabels:
|
|
app: my-new-label # update label
|
|
template:
|
|
metadata:
|
|
labels:
|
|
app: my-new-label # update-label
|
|
spec:
|
|
containers:
|
|
- name: my-c
|
|
image: my-image
|
|
`),
|
|
},
|
|
{
|
|
fieldManager: "not_kubectl",
|
|
lastApplied: []byte(`
|
|
# expect conflicts because field manager is NOT kubectl
|
|
|
|
apiVersion: apps/v1
|
|
kind: Deployment
|
|
metadata:
|
|
name: my-deployment
|
|
labels:
|
|
app: my-app
|
|
spec:
|
|
replicas: 3
|
|
selector:
|
|
matchLabels:
|
|
app: my-app
|
|
template:
|
|
metadata:
|
|
labels:
|
|
app: my-app
|
|
spec:
|
|
containers:
|
|
- name: my-c
|
|
image: my-image-v1
|
|
`),
|
|
original: []byte(`
|
|
apiVersion: apps/v1
|
|
kind: Deployment
|
|
metadata:
|
|
name: my-deployment
|
|
labels:
|
|
app: my-app
|
|
spec:
|
|
replicas: 100 # does not match last-applied
|
|
selector:
|
|
matchLabels:
|
|
app: my-app
|
|
template:
|
|
metadata:
|
|
labels:
|
|
app: my-app
|
|
spec:
|
|
containers:
|
|
- name: my-c
|
|
image: my-image-v2 # does no match last-applied
|
|
`),
|
|
applied: []byte(`
|
|
# test conflicts due to fields not allowed by last-applied
|
|
|
|
apiVersion: apps/v1
|
|
kind: Deployment
|
|
metadata:
|
|
name: my-deployment
|
|
labels:
|
|
app: my-new-label # update label
|
|
spec:
|
|
replicas: 333 # update replicas
|
|
selector:
|
|
matchLabels:
|
|
app: my-new-label # update label
|
|
template:
|
|
metadata:
|
|
labels:
|
|
app: my-new-label # update-label
|
|
spec:
|
|
containers:
|
|
- name: my-c
|
|
image: my-image-new # update image
|
|
`),
|
|
expectConflictSet: fieldpath.NewSet(
|
|
fieldpath.MakePathOrDie("metadata", "labels", "app"),
|
|
fieldpath.MakePathOrDie("spec", "replicas"),
|
|
fieldpath.MakePathOrDie("spec", "selector", "matchLabels", "app"),
|
|
fieldpath.MakePathOrDie("spec", "template", "metadata", "labels", "app"),
|
|
fieldpath.MakePathOrDie("spec", "template", "spec", "containers", fieldpath.KeyByFields("name", "my-c"), "image"),
|
|
),
|
|
},
|
|
{
|
|
fieldManager: "kubectl",
|
|
original: []byte(`
|
|
apiVersion: apps/v1
|
|
kind: Deployment
|
|
metadata:
|
|
name: my-deployment
|
|
labels:
|
|
app: my-app
|
|
spec:
|
|
replicas: 3
|
|
selector:
|
|
matchLabels:
|
|
app: my-app
|
|
template:
|
|
metadata:
|
|
labels:
|
|
app: my-app
|
|
spec:
|
|
containers:
|
|
- name: my-c
|
|
image: my-image
|
|
`),
|
|
applied: []byte(`
|
|
# test allowed update with no conflicts
|
|
|
|
apiVersion: apps/v1
|
|
kind: Deployment
|
|
metadata:
|
|
name: my-deployment
|
|
labels:
|
|
app: my-new-label
|
|
spec:
|
|
replicas: 3
|
|
selector:
|
|
matchLabels:
|
|
app: my-app
|
|
template:
|
|
metadata:
|
|
labels:
|
|
app: my-app
|
|
spec:
|
|
containers:
|
|
- name: my-c
|
|
image: my-new-image # update image
|
|
`),
|
|
},
|
|
{
|
|
fieldManager: "not_kubectl",
|
|
original: []byte(`
|
|
apiVersion: apps/v1
|
|
kind: Deployment
|
|
metadata:
|
|
name: my-deployment
|
|
labels:
|
|
app: my-app
|
|
spec:
|
|
replicas: 100
|
|
selector:
|
|
matchLabels:
|
|
app: my-app
|
|
template:
|
|
metadata:
|
|
labels:
|
|
app: my-app
|
|
spec:
|
|
containers:
|
|
- name: my-c
|
|
image: my-image
|
|
`),
|
|
applied: []byte(`
|
|
|
|
# expect changes to fail because field manager is not kubectl
|
|
|
|
apiVersion: apps/v1
|
|
kind: Deployment
|
|
metadata:
|
|
name: my-deployment
|
|
labels:
|
|
app: my-new-label # update label
|
|
spec:
|
|
replicas: 3 # update replicas
|
|
selector:
|
|
matchLabels:
|
|
app: my-app
|
|
template:
|
|
metadata:
|
|
labels:
|
|
app: my-app
|
|
spec:
|
|
containers:
|
|
- name: my-c
|
|
image: my-new-image # update image
|
|
`),
|
|
expectConflictSet: fieldpath.NewSet(
|
|
fieldpath.MakePathOrDie("metadata", "labels", "app"),
|
|
fieldpath.MakePathOrDie("spec", "replicas"),
|
|
fieldpath.MakePathOrDie("spec", "template", "spec", "containers", fieldpath.KeyByFields("name", "my-c"), "image"),
|
|
),
|
|
},
|
|
{
|
|
fieldManager: "kubectl",
|
|
original: []byte(`
|
|
apiVersion: apps/v1
|
|
kind: Deployment
|
|
metadata:
|
|
name: my-deployment
|
|
spec:
|
|
replicas: 3
|
|
`),
|
|
applied: []byte(`
|
|
apiVersion: apps/v1
|
|
kind: Deployment
|
|
metadata:
|
|
name: my-deployment
|
|
spec:
|
|
replicas: 100 # update replicas
|
|
`),
|
|
},
|
|
{
|
|
fieldManager: "kubectl",
|
|
lastApplied: []byte(`
|
|
apiVersion: extensions/v1beta1
|
|
kind: Deployment
|
|
metadata:
|
|
name: my-deployment
|
|
spec:
|
|
replicas: 3
|
|
`),
|
|
original: []byte(`
|
|
apiVersion: apps/v1 # expect conflict due to apiVersion mismatch with last-applied
|
|
kind: Deployment
|
|
metadata:
|
|
name: my-deployment
|
|
spec:
|
|
replicas: 3
|
|
`),
|
|
applied: []byte(`
|
|
apiVersion: apps/v1
|
|
kind: Deployment
|
|
metadata:
|
|
name: my-deployment
|
|
spec:
|
|
replicas: 100 # update replicas
|
|
`),
|
|
expectConflictSet: fieldpath.NewSet(
|
|
fieldpath.MakePathOrDie("spec", "replicas"),
|
|
),
|
|
},
|
|
{
|
|
fieldManager: "kubectl",
|
|
lastApplied: []byte(`
|
|
apiVerison: foo
|
|
kind: bar
|
|
spec: expect conflict due to invalid object
|
|
`),
|
|
original: []byte(`
|
|
apiVersion: apps/v1
|
|
kind: Deployment
|
|
metadata:
|
|
name: my-deployment
|
|
spec:
|
|
replicas: 3
|
|
`),
|
|
applied: []byte(`
|
|
apiVersion: apps/v1
|
|
kind: Deployment
|
|
metadata:
|
|
name: my-deployment
|
|
spec:
|
|
replicas: 100 # update replicas
|
|
`),
|
|
expectConflictSet: fieldpath.NewSet(
|
|
fieldpath.MakePathOrDie("spec", "replicas"),
|
|
),
|
|
},
|
|
{
|
|
fieldManager: "kubectl",
|
|
// last-applied is empty
|
|
lastApplied: []byte{},
|
|
original: []byte(`
|
|
apiVersion: apps/v1
|
|
kind: Deployment
|
|
metadata:
|
|
name: my-deployment
|
|
spec:
|
|
replicas: 3
|
|
`),
|
|
applied: []byte(`
|
|
apiVersion: apps/v1
|
|
kind: Deployment
|
|
metadata:
|
|
name: my-deployment
|
|
spec:
|
|
replicas: 100 # update replicas
|
|
`),
|
|
expectConflictSet: fieldpath.NewSet(
|
|
fieldpath.MakePathOrDie("spec", "replicas"),
|
|
),
|
|
},
|
|
}
|
|
|
|
for i, test := range tests {
|
|
t.Run(fmt.Sprintf("test %d", i), func(t *testing.T) {
|
|
f.Reset()
|
|
|
|
originalObj := &unstructured.Unstructured{Object: map[string]interface{}{}}
|
|
if err := yaml.Unmarshal(test.original, &originalObj.Object); err != nil {
|
|
t.Errorf("error decoding YAML: %v", err)
|
|
}
|
|
|
|
if test.lastApplied == nil {
|
|
test.lastApplied = test.original
|
|
}
|
|
if err := setLastAppliedFromEncoded(originalObj, test.lastApplied); err != nil {
|
|
t.Errorf("failed to set last applied: %v", err)
|
|
}
|
|
|
|
if err := f.Update(originalObj, "test_client_side_apply"); err != nil {
|
|
t.Errorf("failed to apply object: %v", err)
|
|
}
|
|
|
|
appliedObj := &unstructured.Unstructured{Object: map[string]interface{}{}}
|
|
if err := yaml.Unmarshal(test.applied, &appliedObj.Object); err != nil {
|
|
t.Errorf("error decoding YAML: %v", err)
|
|
}
|
|
|
|
err := f.Apply(appliedObj, test.fieldManager, false)
|
|
|
|
if test.expectConflictSet == nil {
|
|
if err != nil {
|
|
t.Errorf("expected no error but got %v", err)
|
|
}
|
|
return
|
|
}
|
|
|
|
if err == nil || !apierrors.IsConflict(err) {
|
|
t.Errorf("expected to get conflicts but got %v", err)
|
|
}
|
|
|
|
expectedConflicts := merge.Conflicts{}
|
|
test.expectConflictSet.Iterate(func(p fieldpath.Path) {
|
|
expectedConflicts = append(expectedConflicts, merge.Conflict{
|
|
Manager: `{"manager":"test_client_side_apply","operation":"Update","apiVersion":"apps/v1"}`,
|
|
Path: p,
|
|
})
|
|
})
|
|
expectedConflictErr := internal.NewConflictError(expectedConflicts)
|
|
if !reflect.DeepEqual(expectedConflictErr, err) {
|
|
t.Errorf("expected to get\n%+v\nbut got\n%+v", expectedConflictErr, err)
|
|
}
|
|
})
|
|
}
|
|
}
|