Skip to content

Commit ce83228

Browse files
committed
Provide top-level visibility of kubectl modifications of hypervisor CROs
As we want also human operators to modify the CRO, at the very least to set a hypervisor into maintenance, we need top-level visiblity of such an action. The HypervisorTaintController will check the managedFields for any modification through kubeclt (kubectl-apply, kubectl-edit...) and add a condition, which is prominently displayed in the overview table.
1 parent bbaf826 commit ce83228

File tree

7 files changed

+220
-0
lines changed

7 files changed

+220
-0
lines changed

api/v1/hypervisor_types.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ const (
3030
// ConditionTypeReady is the type of condition for ready status of a hypervisor
3131
ConditionTypeReady = "Ready"
3232
ConditionTypeTerminating = "Terminating"
33+
ConditionTypeTainted = "Tainted"
3334

3435
// Reasons for the various being ready...
3536
ConditionReasonReadyReady = "ready"
@@ -218,6 +219,7 @@ type HypervisorStatus struct {
218219
// +kubebuilder:printcolumn:JSONPath=.metadata.labels.worker\.garden\.sapcloud\.io/group,name="Group",type="string",priority=2
219220
// +kubebuilder:printcolumn:JSONPath=".status.conditions[?(@.type==\"Ready\")].status",name="Ready",type="string"
220221
// +kubebuilder:printcolumn:JSONPath=".status.conditions[?(@.type==\"Ready\")].reason",name="State",type="string"
222+
// +kubebuilder:printcolumn:JSONPath=".status.conditions[?(@.type==\"Tainted\")].message",name="Taint",type="string"
221223
// +kubebuilder:printcolumn:JSONPath=".spec.lifecycleEnabled",name="Lifecycle",type="boolean"
222224
// +kubebuilder:printcolumn:JSONPath=".spec.highAvailability",name="High Availability",type="boolean"
223225
// +kubebuilder:printcolumn:JSONPath=".spec.skipTests",name="Skip Tests",type="boolean"

charts/openstack-hypervisor-operator/crds/hypervisor-crd.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ spec:
3434
- jsonPath: .status.conditions[?(@.type=="Ready")].reason
3535
name: State
3636
type: string
37+
- jsonPath: .status.conditions[?(@.type=="Tainted")].message
38+
name: Taint
39+
type: string
3740
- jsonPath: .spec.lifecycleEnabled
3841
name: Lifecycle
3942
type: boolean

cmd/main.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,14 @@ func main() {
292292
os.Exit(1)
293293
}
294294

295+
if err = (&controller.HypervisorTaintController{
296+
Client: mgr.GetClient(),
297+
Scheme: mgr.GetScheme(),
298+
}).SetupWithManager(mgr); err != nil {
299+
setupLog.Error(err, "unable to create controller", "controller", controller.HypervisorTaintControllerName)
300+
os.Exit(1)
301+
}
302+
295303
// +kubebuilder:scaffold:builder
296304

297305
if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {

config/crd/bases/kvm.cloud.sap_hypervisors.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ spec:
3535
- jsonPath: .status.conditions[?(@.type=="Ready")].reason
3636
name: State
3737
type: string
38+
- jsonPath: .status.conditions[?(@.type=="Tainted")].message
39+
name: Taint
40+
type: string
3841
- jsonPath: .spec.lifecycleEnabled
3942
name: Lifecycle
4043
type: boolean
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/*
2+
SPDX-FileCopyrightText: Copyright 2025 SAP SE or an SAP affiliate company and cobaltcore-dev contributors
3+
SPDX-License-Identifier: Apache-2.0
4+
5+
Licensed under the Apache License, Version 2.0 (the "License");
6+
you may not use this file except in compliance with the License.
7+
You may obtain a copy of the License at
8+
9+
http://www.apache.org/licenses/LICENSE-2.0
10+
11+
Unless required by applicable law or agreed to in writing, software
12+
distributed under the License is distributed on an "AS IS" BASIS,
13+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
See the License for the specific language governing permissions and
15+
limitations under the License.
16+
*/
17+
18+
package controller
19+
20+
import (
21+
"context"
22+
23+
"k8s.io/apimachinery/pkg/api/equality"
24+
"k8s.io/apimachinery/pkg/api/meta"
25+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
26+
"k8s.io/apimachinery/pkg/runtime"
27+
ctrl "sigs.k8s.io/controller-runtime"
28+
"sigs.k8s.io/controller-runtime/pkg/builder"
29+
k8sclient "sigs.k8s.io/controller-runtime/pkg/client"
30+
"sigs.k8s.io/controller-runtime/pkg/predicate"
31+
32+
kvmv1 "github.com/cobaltcore-dev/openstack-hypervisor-operator/api/v1"
33+
)
34+
35+
const (
36+
HypervisorTaintControllerName = "HypervisorTaint"
37+
)
38+
39+
type HypervisorTaintController struct {
40+
k8sclient.Client
41+
Scheme *runtime.Scheme
42+
}
43+
44+
// +kubebuilder:rbac:groups=kvm.cloud.sap,resources=hypervisors,verbs=get;list;watch
45+
// +kubebuilder:rbac:groups=kvm.cloud.sap,resources=hypervisors/status,verbs=get;list;watch;create;update;patch
46+
47+
func (r *HypervisorTaintController) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
48+
hypervisor := &kvmv1.Hypervisor{
49+
ObjectMeta: metav1.ObjectMeta{
50+
Name: req.Name,
51+
Labels: map[string]string{},
52+
},
53+
Spec: kvmv1.HypervisorSpec{
54+
HighAvailability: true,
55+
InstallCertificate: true,
56+
},
57+
}
58+
59+
// Check if hypervisor already exists
60+
if err := r.Get(ctx, req.NamespacedName, hypervisor); err != nil {
61+
return ctrl.Result{}, k8sclient.IgnoreNotFound(err)
62+
}
63+
64+
before := hypervisor.DeepCopy()
65+
if HasKubectlManagedFields(&hypervisor.ObjectMeta) {
66+
meta.SetStatusCondition(&hypervisor.Status.Conditions, metav1.Condition{
67+
Type: kvmv1.ConditionTypeTainted,
68+
Status: metav1.ConditionTrue,
69+
Reason: "Kubectl",
70+
Message: "⚠️",
71+
ObservedGeneration: hypervisor.Generation,
72+
})
73+
} else {
74+
meta.SetStatusCondition(&hypervisor.Status.Conditions, metav1.Condition{
75+
Type: kvmv1.ConditionTypeTainted,
76+
Status: metav1.ConditionFalse,
77+
Reason: "NoKubectl",
78+
Message: "🟢",
79+
ObservedGeneration: hypervisor.Generation,
80+
})
81+
}
82+
83+
if equality.Semantic.DeepEqual(hypervisor, before) {
84+
return ctrl.Result{}, nil
85+
}
86+
87+
return ctrl.Result{}, r.Status().Patch(ctx, hypervisor, k8sclient.MergeFromWithOptions(before, k8sclient.MergeFromWithOptimisticLock{}))
88+
}
89+
90+
func (r *HypervisorTaintController) SetupWithManager(mgr ctrl.Manager) error {
91+
return ctrl.NewControllerManagedBy(mgr).
92+
Named(HypervisorTaintControllerName).
93+
For(&kvmv1.Hypervisor{}, builder.WithPredicates(predicate.GenerationChangedPredicate{})).
94+
Complete(r)
95+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/*
2+
SPDX-FileCopyrightText: Copyright 2024 SAP SE or an SAP affiliate company and cobaltcore-dev contributors
3+
SPDX-License-Identifier: Apache-2.0
4+
5+
Licensed under the Apache License, Version 2.0 (the "License");
6+
you may not use this file except in compliance with the License.
7+
You may obtain a copy of the License at
8+
9+
http://www.apache.org/licenses/LICENSE-2.0
10+
11+
Unless required by applicable law or agreed to in writing, software
12+
distributed under the License is distributed on an "AS IS" BASIS,
13+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
See the License for the specific language governing permissions and
15+
limitations under the License.
16+
*/
17+
18+
package controller
19+
20+
import (
21+
. "github.com/onsi/ginkgo/v2"
22+
. "github.com/onsi/gomega"
23+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
24+
"k8s.io/apimachinery/pkg/types"
25+
ctrl "sigs.k8s.io/controller-runtime"
26+
"sigs.k8s.io/controller-runtime/pkg/client"
27+
28+
kvmv1 "github.com/cobaltcore-dev/openstack-hypervisor-operator/api/v1"
29+
)
30+
31+
var _ = Describe("Hypervisor Taint Controller", func() {
32+
const (
33+
hypervisorName = "test-hv"
34+
)
35+
var (
36+
controller *HypervisorTaintController
37+
resource *kvmv1.Hypervisor
38+
namespacedName = types.NamespacedName{Name: hypervisorName}
39+
reconcileReq = ctrl.Request{NamespacedName: namespacedName}
40+
)
41+
42+
BeforeEach(func(ctx SpecContext) {
43+
controller = &HypervisorTaintController{
44+
Client: k8sClient,
45+
Scheme: k8sClient.Scheme(),
46+
}
47+
48+
// pregenerate the resource
49+
resource = &kvmv1.Hypervisor{
50+
ObjectMeta: metav1.ObjectMeta{
51+
Name: hypervisorName,
52+
},
53+
}
54+
55+
Expect(k8sClient.Create(ctx, resource)).To(Succeed())
56+
57+
DeferCleanup(func(ctx SpecContext) {
58+
Expect(k8sClient.Delete(ctx, resource)).To(Succeed())
59+
})
60+
})
61+
62+
Context("When reconciling a new hypervisor", func() {
63+
It("should successfully reconcile the hypervisor", func(ctx SpecContext) {
64+
_, err := controller.Reconcile(ctx, reconcileReq)
65+
Expect(err).NotTo(HaveOccurred())
66+
67+
Expect(k8sClient.Get(ctx, namespacedName, resource)).To(Succeed())
68+
Expect(resource.Status.Conditions).To(ContainElement(
69+
SatisfyAll(
70+
HaveField("Type", kvmv1.ConditionTypeTainted),
71+
HaveField("Status", metav1.ConditionFalse),
72+
HaveField("Message", "🟢"),
73+
HaveField("ObservedGeneration", resource.Generation),
74+
)))
75+
})
76+
})
77+
78+
Context("When reconciling an edited hypervisor ", func() {
79+
BeforeEach(func(ctx SpecContext) {
80+
resource.Spec.SkipTests = true
81+
Expect(k8sClient.Update(ctx, resource, client.FieldOwner("kubectl-edit"))).To(Succeed())
82+
})
83+
84+
It("should successfully reconcile the hypervisor", func(ctx SpecContext) {
85+
_, err := controller.Reconcile(ctx, reconcileReq)
86+
Expect(err).NotTo(HaveOccurred())
87+
88+
Expect(k8sClient.Get(ctx, namespacedName, resource)).To(Succeed())
89+
Expect(resource.Status.Conditions).To(ContainElement(
90+
SatisfyAll(
91+
HaveField("Type", kvmv1.ConditionTypeTainted),
92+
HaveField("Status", metav1.ConditionTrue),
93+
HaveField("Message", "⚠️"),
94+
HaveField("ObservedGeneration", resource.Generation),
95+
)))
96+
})
97+
})
98+
})

internal/controller/utils.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727
"net/http"
2828
"os"
2929
"slices"
30+
"strings"
3031

3132
corev1 "k8s.io/api/core/v1"
3233
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -149,3 +150,13 @@ func OwnerReference(obj metav1.Object, gvk *schema.GroupVersionKind) *v1ac.Owner
149150
}
150151

151152
var ErrRetry = errors.New("ErrRetry")
153+
154+
// returns if any ManagedField of the object has been modified by kubectl
155+
func HasKubectlManagedFields(object *metav1.ObjectMeta) bool {
156+
for _, field := range object.GetManagedFields() {
157+
if strings.HasPrefix(field.Manager, "kubectl") {
158+
return true
159+
}
160+
}
161+
return false
162+
}

0 commit comments

Comments
 (0)