1008 lines
20 KiB
Go
1008 lines
20 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 internal_test
|
|
|
|
import (
|
|
"fmt"
|
|
"reflect"
|
|
"testing"
|
|
|
|
apiequality "k8s.io/apimachinery/pkg/api/equality"
|
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
|
yamlutil "k8s.io/apimachinery/pkg/util/yaml"
|
|
"k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/fieldmanagertest"
|
|
"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"
|
|
)
|
|
|
|
type testArgs struct {
|
|
lastApplied []byte
|
|
original []byte
|
|
applied []byte
|
|
fieldManager string
|
|
expectConflictSet *fieldpath.Set
|
|
}
|
|
|
|
// 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 := fieldmanagertest.NewTestFieldManager(fakeTypeConverter, schema.FromAPIVersionAndKind("apps/v1", "Deployment"))
|
|
|
|
tests := []testArgs{
|
|
{
|
|
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"), // selector is atomic
|
|
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"),
|
|
),
|
|
},
|
|
}
|
|
|
|
testConflicts(t, f, tests)
|
|
}
|
|
|
|
func TestServiceApply(t *testing.T) {
|
|
f := fieldmanagertest.NewTestFieldManager(fakeTypeConverter, schema.FromAPIVersionAndKind("v1", "Service"))
|
|
|
|
tests := []testArgs{
|
|
{
|
|
fieldManager: "kubectl",
|
|
original: []byte(`
|
|
apiVersion: v1
|
|
kind: Service
|
|
metadata:
|
|
name: test
|
|
spec:
|
|
ports:
|
|
- name: https
|
|
port: 443
|
|
protocol: TCP
|
|
targetPort: 8443
|
|
selector:
|
|
old: test
|
|
`),
|
|
applied: []byte(`
|
|
# All accepted while using the same field manager
|
|
|
|
apiVersion: v1
|
|
kind: Service
|
|
metadata:
|
|
name: test
|
|
spec:
|
|
ports:
|
|
- name: https
|
|
port: 443
|
|
protocol: TCP
|
|
targetPort: 8444
|
|
selector:
|
|
new: test
|
|
`),
|
|
},
|
|
{
|
|
fieldManager: "kubectl",
|
|
original: []byte(`
|
|
apiVersion: v1
|
|
kind: Service
|
|
metadata:
|
|
name: test
|
|
spec:
|
|
ports:
|
|
- name: https
|
|
port: 443
|
|
protocol: TCP
|
|
targetPort: 8443
|
|
selector:
|
|
old: test
|
|
`),
|
|
applied: []byte(`
|
|
# Allowed to remove selectors while using the same field manager
|
|
|
|
apiVersion: v1
|
|
kind: Service
|
|
metadata:
|
|
name: test
|
|
spec:
|
|
ports:
|
|
- name: https
|
|
port: 443
|
|
protocol: TCP
|
|
targetPort: 8444
|
|
selector: {}
|
|
`),
|
|
},
|
|
{
|
|
fieldManager: "not_kubectl",
|
|
original: []byte(`
|
|
apiVersion: v1
|
|
kind: Service
|
|
metadata:
|
|
name: test
|
|
spec:
|
|
ports:
|
|
- name: https
|
|
port: 443
|
|
protocol: TCP # TODO: issue - this is a defaulted field, should not be required in a new spec
|
|
targetPort: 8443
|
|
selector:
|
|
old: test
|
|
`),
|
|
applied: []byte(`
|
|
# test selector update not allowed by last-applied
|
|
|
|
apiVersion: v1
|
|
kind: Service
|
|
metadata:
|
|
name: test
|
|
spec:
|
|
ports:
|
|
- name: https
|
|
port: 443
|
|
protocol: TCP
|
|
targetPort: 8444
|
|
selector:
|
|
new: test
|
|
`),
|
|
expectConflictSet: fieldpath.NewSet(
|
|
fieldpath.MakePathOrDie("spec", "selector"), // selector is atomic
|
|
fieldpath.MakePathOrDie("spec", "ports", fieldpath.KeyByFields("port", 443, "protocol", "TCP"), "targetPort"),
|
|
),
|
|
},
|
|
}
|
|
|
|
testConflicts(t, f, tests)
|
|
}
|
|
|
|
func TestReplicationControllerApply(t *testing.T) {
|
|
f := fieldmanagertest.NewTestFieldManager(fakeTypeConverter, schema.FromAPIVersionAndKind("v1", "ReplicationController"))
|
|
|
|
tests := []testArgs{
|
|
{
|
|
fieldManager: "kubectl",
|
|
original: []byte(`
|
|
apiVersion: v1
|
|
kind: ReplicationController
|
|
metadata:
|
|
name: test
|
|
spec:
|
|
replicas: 0
|
|
selector:
|
|
old: test
|
|
`),
|
|
applied: []byte(`
|
|
# All accepted while using the same field manager
|
|
|
|
apiVersion: v1
|
|
kind: ReplicationController
|
|
metadata:
|
|
name: test
|
|
spec:
|
|
replicas: 3
|
|
selector:
|
|
new: test
|
|
`),
|
|
},
|
|
{
|
|
fieldManager: "not_kubectl",
|
|
original: []byte(`
|
|
apiVersion: v1
|
|
kind: ReplicationController
|
|
metadata:
|
|
name: test
|
|
spec:
|
|
replicas: 0
|
|
selector:
|
|
old: test
|
|
`),
|
|
applied: []byte(`
|
|
# test selector update not allowed by last-applied
|
|
|
|
apiVersion: v1
|
|
kind: ReplicationController
|
|
metadata:
|
|
name: test
|
|
spec:
|
|
replicas: 3
|
|
selector:
|
|
new: test
|
|
`),
|
|
expectConflictSet: fieldpath.NewSet(
|
|
fieldpath.MakePathOrDie("spec", "selector"), // selector is atomic
|
|
fieldpath.MakePathOrDie("spec", "replicas"),
|
|
),
|
|
},
|
|
}
|
|
|
|
testConflicts(t, f, tests)
|
|
}
|
|
|
|
func TestPodApply(t *testing.T) {
|
|
f := fieldmanagertest.NewTestFieldManager(fakeTypeConverter, schema.FromAPIVersionAndKind("v1", "Pod"))
|
|
|
|
tests := []testArgs{
|
|
{
|
|
fieldManager: "kubectl",
|
|
original: []byte(`
|
|
apiVersion: v1
|
|
kind: Pod
|
|
metadata:
|
|
name: test
|
|
namespace: test
|
|
spec:
|
|
containers:
|
|
- args:
|
|
- -v=2
|
|
command:
|
|
- controller
|
|
image: some.registry/app:latest
|
|
name: doJob
|
|
nodeName: definetlyControlPlane
|
|
nodeSelector:
|
|
node-role.kubernetes.io/master: ""
|
|
`),
|
|
applied: []byte(`
|
|
# All accepted while using the same field manager
|
|
|
|
apiVersion: v1
|
|
kind: Pod
|
|
metadata:
|
|
name: test
|
|
namespace: test
|
|
spec:
|
|
containers:
|
|
- args:
|
|
- -v=2
|
|
command:
|
|
- controller
|
|
image: some.registry/app:latest
|
|
name: doJob
|
|
nodeSelector:
|
|
node-role.kubernetes.io/worker: ""
|
|
`),
|
|
},
|
|
{
|
|
fieldManager: "not_kubectl",
|
|
original: []byte(`
|
|
apiVersion: v1
|
|
kind: Pod
|
|
metadata:
|
|
name: test
|
|
namespace: test
|
|
spec:
|
|
containers:
|
|
- args:
|
|
- -v=2
|
|
command:
|
|
- controller
|
|
image: some.registry/app:latest
|
|
name: doJob
|
|
nodeName: definetlyControlPlane
|
|
nodeSelector:
|
|
node-role.kubernetes.io/master: ""
|
|
`),
|
|
applied: []byte(`
|
|
# test selector update not allowed by last-applied
|
|
|
|
apiVersion: v1
|
|
kind: Pod
|
|
metadata:
|
|
name: test
|
|
namespace: test
|
|
spec:
|
|
containers:
|
|
- args:
|
|
- -v=2
|
|
command:
|
|
- controller
|
|
image: some.registry/app:latest
|
|
name: doJob
|
|
nodeName: definetlyControlPlane
|
|
nodeSelector:
|
|
node-role.kubernetes.io/master: ""
|
|
otherNodeType: ""
|
|
`),
|
|
expectConflictSet: fieldpath.NewSet(
|
|
fieldpath.MakePathOrDie("spec", "nodeSelector"), // selector is atomic
|
|
),
|
|
},
|
|
{
|
|
fieldManager: "not_kubectl",
|
|
original: []byte(`
|
|
apiVersion: v1
|
|
kind: Pod
|
|
metadata:
|
|
name: test
|
|
namespace: test
|
|
spec:
|
|
containers:
|
|
- args:
|
|
- -v=2
|
|
command:
|
|
- controller
|
|
image: some.registry/app:latest
|
|
name: doJob
|
|
nodeName: definetlyControlPlane
|
|
nodeSelector:
|
|
node-role.kubernetes.io/master: ""
|
|
`),
|
|
applied: []byte(`
|
|
# purging selector not allowed for different manager
|
|
|
|
apiVersion: v1
|
|
kind: Pod
|
|
metadata:
|
|
name: test
|
|
namespace: test
|
|
spec:
|
|
containers:
|
|
- args:
|
|
- -v=2
|
|
command:
|
|
- controller
|
|
image: some.registry/app:latest
|
|
name: doJob
|
|
nodeName: another
|
|
nodeSelector: {}
|
|
`),
|
|
expectConflictSet: fieldpath.NewSet(
|
|
fieldpath.MakePathOrDie("spec", "nodeSelector"), // selector is atomic
|
|
fieldpath.MakePathOrDie("spec", "nodeName"),
|
|
),
|
|
},
|
|
{
|
|
fieldManager: "kubectl",
|
|
original: []byte(`
|
|
apiVersion: v1
|
|
kind: Pod
|
|
metadata:
|
|
name: test
|
|
namespace: test
|
|
spec:
|
|
containers:
|
|
- args:
|
|
- -v=2
|
|
command:
|
|
- controller
|
|
image: some.registry/app:latest
|
|
name: doJob
|
|
nodeName: definetlyControlPlane
|
|
nodeSelector:
|
|
node-role.kubernetes.io/master: ""
|
|
`),
|
|
applied: []byte(`
|
|
# same manager could purge nodeSelector
|
|
|
|
apiVersion: v1
|
|
kind: Pod
|
|
metadata:
|
|
name: test
|
|
namespace: test
|
|
spec:
|
|
containers:
|
|
- args:
|
|
- -v=2
|
|
command:
|
|
- controller
|
|
image: some.registry/app:latest
|
|
name: doJob
|
|
nodeName: another
|
|
nodeSelector: {}
|
|
`),
|
|
},
|
|
}
|
|
|
|
testConflicts(t, f, tests)
|
|
}
|
|
|
|
func testConflicts(t *testing.T, f fieldmanagertest.TestFieldManager, tests []testArgs) {
|
|
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)
|
|
}
|
|
} else {
|
|
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: fmt.Sprintf(`{"manager":"test_client_side_apply","operation":"Update","apiVersion":"%s"}`, f.APIVersion()),
|
|
Path: p,
|
|
})
|
|
})
|
|
expectedConflictErr := internal.NewConflictError(expectedConflicts)
|
|
if !reflect.DeepEqual(expectedConflictErr, err) {
|
|
t.Errorf("expected to get\n%+v\nbut got\n%+v", expectedConflictErr, err)
|
|
}
|
|
|
|
// Yet force should resolve all conflicts
|
|
err = f.Apply(appliedObj, test.fieldManager, true)
|
|
if err != nil {
|
|
t.Errorf("unexpected error during force ownership apply: %v", err)
|
|
}
|
|
|
|
}
|
|
|
|
// Eventually resource should contain applied changes
|
|
if !apiequality.Semantic.DeepDerivative(appliedObj, f.Live()) {
|
|
t.Errorf("expected equal resource: \n%#v, got: \n%#v", appliedObj, f.Live())
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func yamlToJSON(y []byte) (string, error) {
|
|
obj := &unstructured.Unstructured{Object: map[string]interface{}{}}
|
|
if err := yaml.Unmarshal(y, &obj.Object); err != nil {
|
|
return "", fmt.Errorf("error decoding YAML: %v", err)
|
|
}
|
|
serialization, err := runtime.Encode(unstructured.UnstructuredJSONScheme, obj)
|
|
if err != nil {
|
|
return "", fmt.Errorf("error encoding object: %v", err)
|
|
}
|
|
json, err := yamlutil.ToJSON(serialization)
|
|
if err != nil {
|
|
return "", fmt.Errorf("error converting to json: %v", err)
|
|
}
|
|
return string(json), nil
|
|
}
|
|
|
|
func setLastAppliedFromEncoded(obj runtime.Object, lastApplied []byte) error {
|
|
lastAppliedJSON, err := yamlToJSON(lastApplied)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return internal.SetLastApplied(obj, lastAppliedJSON)
|
|
}
|