Skip to content

Commit 9a2654e

Browse files
authored
[Bugfix] Prevent unexpected rotation in case of SecurityContext change (#1649)
1 parent e5958bd commit 9a2654e

9 files changed

+323
-8
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
## [master](https://github.com/arangodb/kube-arangodb/tree/master) (N/A)
44
- (Maintenance) Bump Prometheus API Version
5+
- (Bugfix) Prevent unexpected rotation in case of SecurityContext change
56

67
## [1.2.40](https://github.com/arangodb/kube-arangodb/tree/1.2.40) (2024-04-10)
78
- (Feature) Add Core fields to the Scheduler Container Spec
+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
//
2+
// DISCLAIMER
3+
//
4+
// Copyright 2024 ArangoDB GmbH, Cologne, Germany
5+
//
6+
// Licensed under the Apache License, Version 2.0 (the "License");
7+
// you may not use this file except in compliance with the License.
8+
// You may obtain a copy of the License at
9+
//
10+
// http://www.apache.org/licenses/LICENSE-2.0
11+
//
12+
// Unless required by applicable law or agreed to in writing, software
13+
// distributed under the License is distributed on an "AS IS" BASIS,
14+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
// See the License for the specific language governing permissions and
16+
// limitations under the License.
17+
//
18+
// Copyright holder is ArangoDB GmbH, Cologne, Germany
19+
//
20+
21+
package rotation
22+
23+
import (
24+
core "k8s.io/api/core/v1"
25+
26+
api "github.com/arangodb/kube-arangodb/pkg/apis/deployment/v1"
27+
"github.com/arangodb/kube-arangodb/pkg/util"
28+
"github.com/arangodb/kube-arangodb/pkg/util/compare"
29+
)
30+
31+
func compareAndAssignEmptyField[T interface{}](spec, status *T) (*T, bool, error) {
32+
if equal, err := util.CompareJSON(spec, status); err != nil {
33+
return nil, false, err
34+
} else if !equal {
35+
if equal, err := util.CompareJSONP(spec, status); err != nil {
36+
return nil, false, err
37+
} else if equal {
38+
return spec, true, nil
39+
}
40+
}
41+
42+
return nil, false, nil
43+
}
44+
45+
func comparePodEmptyFields(_ api.DeploymentSpec, _ api.ServerGroup, spec, status *core.PodTemplateSpec) compare.Func {
46+
return func(builder api.ActionBuilder) (mode compare.Mode, plan api.Plan, e error) {
47+
if obj, replace, err := compareAndAssignEmptyField(spec.Spec.SecurityContext, status.Spec.SecurityContext); err != nil {
48+
e = err
49+
return
50+
} else if replace {
51+
mode = mode.And(compare.SilentRotation)
52+
status.Spec.SecurityContext = obj.DeepCopy()
53+
}
54+
if equal, err := util.CompareJSON(spec.Spec.SecurityContext, status.Spec.SecurityContext); err != nil {
55+
e = err
56+
return
57+
} else if !equal {
58+
if equal, err := util.CompareJSONP(spec.Spec.SecurityContext, status.Spec.SecurityContext); err != nil {
59+
e = err
60+
return
61+
} else if equal {
62+
mode = mode.And(compare.SilentRotation)
63+
status.Spec.SecurityContext = spec.Spec.SecurityContext.DeepCopy()
64+
}
65+
}
66+
return
67+
}
68+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
//
2+
// DISCLAIMER
3+
//
4+
// Copyright 2024 ArangoDB GmbH, Cologne, Germany
5+
//
6+
// Licensed under the Apache License, Version 2.0 (the "License");
7+
// you may not use this file except in compliance with the License.
8+
// You may obtain a copy of the License at
9+
//
10+
// http://www.apache.org/licenses/LICENSE-2.0
11+
//
12+
// Unless required by applicable law or agreed to in writing, software
13+
// distributed under the License is distributed on an "AS IS" BASIS,
14+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
// See the License for the specific language governing permissions and
16+
// limitations under the License.
17+
//
18+
// Copyright holder is ArangoDB GmbH, Cologne, Germany
19+
//
20+
21+
package rotation
22+
23+
import (
24+
"testing"
25+
26+
core "k8s.io/api/core/v1"
27+
28+
api "github.com/arangodb/kube-arangodb/pkg/apis/deployment/v1"
29+
"github.com/arangodb/kube-arangodb/pkg/util"
30+
"github.com/arangodb/kube-arangodb/pkg/util/compare"
31+
)
32+
33+
func Test_ArangoD_PodSecurityContext(t *testing.T) {
34+
testCases := []TestCase{
35+
{
36+
name: "With deployment",
37+
spec: buildPodSpec(),
38+
status: buildPodSpec(),
39+
40+
deploymentSpec: buildDeployment(func(depl *api.DeploymentSpec) {
41+
42+
}),
43+
44+
TestCaseOverride: TestCaseOverride{
45+
expectedMode: compare.SkippedRotation,
46+
},
47+
},
48+
{
49+
name: "Nil to Nil SecurityContext",
50+
spec: buildPodSpec(),
51+
status: buildPodSpec(),
52+
53+
deploymentSpec: buildDeployment(),
54+
55+
TestCaseOverride: TestCaseOverride{
56+
expectedMode: compare.SkippedRotation,
57+
},
58+
},
59+
{
60+
name: "Nil to Empty SecurityContext",
61+
spec: buildPodSpec(addPodSecurityContext(nil)),
62+
status: buildPodSpec(addPodSecurityContext(&core.PodSecurityContext{})),
63+
64+
deploymentSpec: buildDeployment(),
65+
66+
TestCaseOverride: TestCaseOverride{
67+
expectedMode: compare.SilentRotation,
68+
},
69+
},
70+
{
71+
name: "Empty to nil SecurityContext",
72+
spec: buildPodSpec(addPodSecurityContext(&core.PodSecurityContext{})),
73+
status: buildPodSpec(addPodSecurityContext(nil)),
74+
75+
deploymentSpec: buildDeployment(),
76+
77+
TestCaseOverride: TestCaseOverride{
78+
expectedMode: compare.SilentRotation,
79+
},
80+
},
81+
{
82+
name: "Empty to Empty SecurityContext",
83+
spec: buildPodSpec(addPodSecurityContext(&core.PodSecurityContext{})),
84+
status: buildPodSpec(addPodSecurityContext(&core.PodSecurityContext{})),
85+
86+
deploymentSpec: buildDeployment(),
87+
88+
TestCaseOverride: TestCaseOverride{
89+
expectedMode: compare.SkippedRotation,
90+
},
91+
},
92+
{
93+
name: "Empty to NonEmpty SecurityContext",
94+
spec: buildPodSpec(addPodSecurityContext(&core.PodSecurityContext{})),
95+
status: buildPodSpec(addPodSecurityContext(&core.PodSecurityContext{
96+
RunAsGroup: util.NewType[int64](1000),
97+
})),
98+
99+
deploymentSpec: buildDeployment(),
100+
101+
TestCaseOverride: TestCaseOverride{
102+
expectedMode: compare.GracefulRotation,
103+
},
104+
},
105+
{
106+
name: "Nil to NonEmpty SecurityContext",
107+
spec: buildPodSpec(addPodSecurityContext(nil)),
108+
status: buildPodSpec(addPodSecurityContext(&core.PodSecurityContext{
109+
RunAsGroup: util.NewType[int64](1000),
110+
})),
111+
112+
deploymentSpec: buildDeployment(),
113+
114+
TestCaseOverride: TestCaseOverride{
115+
expectedMode: compare.GracefulRotation,
116+
},
117+
},
118+
{
119+
name: "NonEmpty to Nil SecurityContext",
120+
spec: buildPodSpec(addPodSecurityContext(&core.PodSecurityContext{
121+
RunAsGroup: util.NewType[int64](1000),
122+
})),
123+
status: buildPodSpec(addPodSecurityContext(&core.PodSecurityContext{})),
124+
125+
deploymentSpec: buildDeployment(),
126+
127+
TestCaseOverride: TestCaseOverride{
128+
expectedMode: compare.GracefulRotation,
129+
},
130+
},
131+
{
132+
name: "NonEmpty to Nil SecurityContext",
133+
spec: buildPodSpec(addPodSecurityContext(&core.PodSecurityContext{
134+
RunAsGroup: util.NewType[int64](1000),
135+
})),
136+
status: buildPodSpec(addPodSecurityContext(nil)),
137+
138+
deploymentSpec: buildDeployment(),
139+
140+
TestCaseOverride: TestCaseOverride{
141+
expectedMode: compare.GracefulRotation,
142+
},
143+
},
144+
{
145+
name: "NonEmpty to NonEmpty SecurityContext",
146+
spec: buildPodSpec(addPodSecurityContext(&core.PodSecurityContext{
147+
RunAsGroup: util.NewType[int64](1000),
148+
})),
149+
status: buildPodSpec(addPodSecurityContext(&core.PodSecurityContext{
150+
RunAsGroup: util.NewType[int64](1000),
151+
})),
152+
153+
deploymentSpec: buildDeployment(),
154+
155+
TestCaseOverride: TestCaseOverride{
156+
expectedMode: compare.SkippedRotation,
157+
},
158+
},
159+
{
160+
name: "NonEmpty to NonEmpty Changed SecurityContext",
161+
spec: buildPodSpec(addPodSecurityContext(&core.PodSecurityContext{
162+
RunAsGroup: util.NewType[int64](1000),
163+
})),
164+
status: buildPodSpec(addPodSecurityContext(&core.PodSecurityContext{
165+
RunAsGroup: util.NewType[int64](1001),
166+
})),
167+
168+
deploymentSpec: buildDeployment(),
169+
170+
TestCaseOverride: TestCaseOverride{
171+
expectedMode: compare.GracefulRotation,
172+
},
173+
},
174+
}
175+
176+
runTestCases(t)(testCases...)
177+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
//
2+
// DISCLAIMER
3+
//
4+
// Copyright 2024 ArangoDB GmbH, Cologne, Germany
5+
//
6+
// Licensed under the Apache License, Version 2.0 (the "License");
7+
// you may not use this file except in compliance with the License.
8+
// You may obtain a copy of the License at
9+
//
10+
// http://www.apache.org/licenses/LICENSE-2.0
11+
//
12+
// Unless required by applicable law or agreed to in writing, software
13+
// distributed under the License is distributed on an "AS IS" BASIS,
14+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
// See the License for the specific language governing permissions and
16+
// limitations under the License.
17+
//
18+
// Copyright holder is ArangoDB GmbH, Cologne, Germany
19+
//
20+
21+
package rotation
22+
23+
import core "k8s.io/api/core/v1"
24+
25+
func addPodSecurityContext(context *core.PodSecurityContext) podSpecBuilder {
26+
return func(pod *core.PodTemplateSpec) {
27+
pod.Spec.SecurityContext = context.DeepCopy()
28+
}
29+
}

pkg/deployment/rotation/compare.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
//
22
// DISCLAIMER
33
//
4-
// Copyright 2016-2023 ArangoDB GmbH, Cologne, Germany
4+
// Copyright 2016-2024 ArangoDB GmbH, Cologne, Germany
55
//
66
// Licensed under the Apache License, Version 2.0 (the "License");
77
// you may not use this file except in compliance with the License.
@@ -48,5 +48,5 @@ func compareFunc(deploymentSpec api.DeploymentSpec, member api.MemberStatus, gro
4848
return checksum, nil
4949
},
5050
spec, status,
51-
podCompare, affinityCompare, comparePodVolumes, containersCompare, initContainersCompare, comparePodTolerations)
51+
podCompare, affinityCompare, comparePodVolumes, containersCompare, initContainersCompare, comparePodTolerations, comparePodEmptyFields)
5252
}

pkg/deployment/rotation/testdata/pod_lifecycle_change.000.status.json

+1
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,7 @@
289289
}
290290
],
291291
"restartPolicy": "Never",
292+
"securityContext": {},
292293
"serviceAccountName": "deployment-pod",
293294
"subdomain": "deployment-int",
294295
"terminationGracePeriodSeconds": 3600,

pkg/deployment/rotation/utils_test.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
//
22
// DISCLAIMER
33
//
4-
// Copyright 2016-2023 ArangoDB GmbH, Cologne, Germany
4+
// Copyright 2016-2024 ArangoDB GmbH, Cologne, Germany
55
//
66
// Licensed under the Apache License, Version 2.0 (the "License");
77
// you may not use this file except in compliance with the License.
@@ -111,7 +111,7 @@ func runTestCasesForModeAndGroup(t *testing.T, m api.DeploymentMode, g api.Serve
111111
require.Error(t, err)
112112
require.EqualError(t, err, q.expectedErr)
113113
} else {
114-
require.Equal(t, q.expectedMode, mode)
114+
require.Equalf(t, q.expectedMode, mode, "Expected %s, got %s", q.expectedMode.String(), mode.String())
115115

116116
switch mode {
117117
case compare2.InPlaceRotation:

pkg/util/checksum.go

+17-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
//
22
// DISCLAIMER
33
//
4-
// Copyright 2016-2022 ArangoDB GmbH, Cologne, Germany
4+
// Copyright 2016-2024 ArangoDB GmbH, Cologne, Germany
55
//
66
// Licensed under the Apache License, Version 2.0 (the "License");
77
// you may not use this file except in compliance with the License.
@@ -35,7 +35,7 @@ func SHA256(data []byte) string {
3535
return fmt.Sprintf("%0x", sha256.Sum256(data))
3636
}
3737

38-
func SHA256FromJSON(a interface{}) (string, error) {
38+
func SHA256FromJSON[T interface{}](a T) (string, error) {
3939
d, err := json.Marshal(a)
4040
if err != nil {
4141
return "", err
@@ -44,7 +44,7 @@ func SHA256FromJSON(a interface{}) (string, error) {
4444
return SHA256(d), nil
4545
}
4646

47-
func CompareJSON(a, b interface{}) (bool, error) {
47+
func CompareJSON[T interface{}](a, b T) (bool, error) {
4848
ad, err := SHA256FromJSON(a)
4949
if err != nil {
5050
return false, err
@@ -56,3 +56,17 @@ func CompareJSON(a, b interface{}) (bool, error) {
5656

5757
return ad == bd, nil
5858
}
59+
60+
func CompareJSONP[T interface{}](a, b *T) (bool, error) {
61+
var a1, b1 T
62+
63+
if a != nil {
64+
a1 = *a
65+
}
66+
67+
if b != nil {
68+
b1 = *b
69+
}
70+
71+
return CompareJSON(a1, b1)
72+
}

0 commit comments

Comments
 (0)