Skip to content

Commit b53e9dc

Browse files
committed
Handling propagation of Detached Annotation
When a HostClaim has a detached annotation, we propagate it to the BareMetalHost iff there is a HostDeployPolicy compatible with the claim that allows such a propagation. We add a manager flag in the "arguments" of the annotation. HostClaim do not set it to true and if the annotation does not appear in the HostClaim, it is not removed from the BareMetalHost if the manager field is set to true. This way the administrator can still force detachment. Signed-off-by: Pierre Crégut <pierre.cregut@orange.com>
1 parent 8a95ab0 commit b53e9dc

7 files changed

Lines changed: 201 additions & 42 deletions

File tree

apis/metal3.io/v1alpha1/baremetalhost_types.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -699,6 +699,10 @@ const (
699699
type DetachedAnnotationArguments struct {
700700
// DeleteAction indicates the desired delete logic when the detached annotation is present
701701
DeleteAction DetachedDeleteAction `json:"deleteAction,omitempty"`
702+
// Manager is an informal flag that tells that the annotation was set by an administrator.
703+
// HostClaim controller will always set it to false and will never delete an annotation with
704+
// manager set to true.
705+
Manager bool `json:"manager,omitempty"`
702706
}
703707

704708
// Match compares the saved status information with the name and

apis/metal3.io/v1alpha1/hostdeploypolicy_types.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ type HostDeployPolicySpec struct {
2525
// HostClaimNamespaces constrains the namespaces of the HostClaims allowed
2626
// to bind the BareMetalHosts in the same namespace as the HostDeployPolicy
2727
HostClaimNamespaces *HostClaimNamespaces `json:"hostClaimNamespaces,omitempty"`
28+
// AllowsDetachedMode is a boolean flag specifying if the hostClaim can set the
29+
// detached annotation of the BareMetalHost.
30+
AllowsDetachedMode bool `json:"allowsDetachedMode,omitempty"`
2831
}
2932

3033
type HostClaimNamespaces struct {

config/base/crds/bases/metal3.io_hostdeploypolicies.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@ spec:
3939
spec:
4040
description: HostDeployPolicySpec defines the desired state of HostDeployPolicy.
4141
properties:
42+
allowsDetachedMode:
43+
description: |-
44+
AllowsDetachedMode is a boolean flag specifying if the hostClaim can set the
45+
detached annotation of the BareMetalHost.
46+
type: boolean
4247
hostClaimNamespaces:
4348
description: |-
4449
HostClaimNamespaces constrains the namespaces of the HostClaims allowed

config/render/capm3.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2248,6 +2248,11 @@ spec:
22482248
spec:
22492249
description: HostDeployPolicySpec defines the desired state of HostDeployPolicy.
22502250
properties:
2251+
allowsDetachedMode:
2252+
description: |-
2253+
AllowsDetachedMode is a boolean flag specifying if the hostClaim can set the
2254+
detached annotation of the BareMetalHost.
2255+
type: boolean
22512256
hostClaimNamespaces:
22522257
description: |-
22532258
HostClaimNamespaces constrains the namespaces of the HostClaims allowed

internal/testutil/common.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,3 +360,8 @@ func (hb *HostDeployPolicyBuilder) AcceptRegexp(re string) *HostDeployPolicyBuil
360360
spec.HostClaimNamespaces.NameMatches = re
361361
return hb
362362
}
363+
364+
func (hb *HostDeployPolicyBuilder) AllowsDetachedMode() *HostDeployPolicyBuilder {
365+
hb.hostDeployPolicy.Spec.AllowsDetachedMode = true
366+
return hb
367+
}

pkg/hostclaim/hostclaim_manager.go

Lines changed: 118 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package hostclaim
1919
import (
2020
"context"
2121
"crypto/rand"
22+
"encoding/json"
2223
"errors"
2324
"math/big"
2425
"regexp"
@@ -200,6 +201,11 @@ func (m *HostManager) Update(ctx context.Context) error {
200201
if syncReboot(m.HostClaim.Annotations, bmh.Annotations) {
201202
updated = true
202203
}
204+
if upd, err2 := m.syncDetached(ctx, bmh.Namespace, m.HostClaim.Annotations, bmh.Annotations); err2 != nil {
205+
return err2
206+
} else if upd {
207+
updated = true
208+
}
203209
if updated {
204210
m.Log.Info("Update the BareMetalHost spec: changes detected.")
205211
err = m.client.Update(ctx, bmh)
@@ -535,56 +541,59 @@ func (m *HostManager) acceptableNamespaces(ctx context.Context, namespace string
535541
nsLabels = map[string]string{}
536542
}
537543
namespaces := NewSet[string]()
538-
LOOP_POLICY:
539544
for _, hostDeployPolicy := range hostdeploypolicies.Items {
540-
log := m.Log.WithValues("policyNamespace", hostDeployPolicy.Namespace, "policyName", hostDeployPolicy.Name)
541-
if SetContains(namespaces, hostDeployPolicy.Namespace) {
542-
// namespace already added, no reason to check this hdp.
543-
continue
545+
if accept, err := m.checkPolicy(&hostDeployPolicy, hostNs, nsLabels); err != nil {
546+
return nil, err
547+
} else if accept {
548+
AddSet(namespaces, hostDeployPolicy.Namespace)
544549
}
545-
constraints := hostDeployPolicy.Spec.HostClaimNamespaces
546-
if constraints == nil {
547-
log.V(1).Info("Ignoring HostDeployPolicy without constraint")
548-
continue
550+
}
551+
m.Log.Info("Acceptable namespaces", "namespaces", namespaces)
552+
return namespaces, nil
553+
}
554+
555+
func (m *HostManager) checkPolicy(hostDeployPolicy *metal3api.HostDeployPolicy, hostNs string, hostNsLabels map[string]string) (bool, error) {
556+
log := m.Log.WithValues("policyNamespace", hostDeployPolicy.Namespace, "policyName", hostDeployPolicy.Name)
557+
constraints := hostDeployPolicy.Spec.HostClaimNamespaces
558+
if constraints == nil {
559+
m.Log.V(1).Info("Ignoring HostDeployPolicy without constraint")
560+
return false, nil
561+
}
562+
if constraints.Names != nil && !slices.Contains(constraints.Names, hostNs) {
563+
log.V(1).Info("Ignoring HostDeployPolicy because claim namespace not in names", "names", constraints.Names)
564+
return false, nil
565+
}
566+
if constraints.NameMatches != "" {
567+
b, err := regexp.MatchString(constraints.NameMatches, hostNs)
568+
if err != nil {
569+
log.Error(
570+
err, "Error during regexp matching on HostClaim namespace (bad regexp)",
571+
"regexp", constraints.NameMatches)
572+
return false, err
549573
}
550-
if constraints.Names != nil && !slices.Contains(constraints.Names, hostNs) {
551-
log.V(1).Info("Ignoring HostDeployPolicy because claim namespace not in names", "names", constraints.Names)
552-
continue
574+
if !b {
575+
log.V(1).Info("Ignoring HostDeployPolicy because claim namespace does not match regex")
576+
return false, nil
553577
}
554-
if constraints.NameMatches != "" {
555-
b, err := regexp.MatchString(constraints.NameMatches, hostNs)
556-
if err != nil {
557-
log.Error(
558-
err, "Error during regexp matching on HostClaim namespace (bad regexp)",
559-
"regexp", constraints.NameMatches)
560-
return nil, err
561-
}
562-
if !b {
563-
log.V(1).Info("Ignoring HostDeployPolicy because claim namespace does not match regex")
564-
continue
565-
}
566-
}
567-
// Behaves as a 'forall' on labels
568-
for _, pair := range constraints.HasLabels {
569-
if v, ok := nsLabels[pair.Name]; ok {
570-
if pair.Value != "" && v != pair.Value {
571-
log.V(1).Info(
572-
"Ignoring HostDeployPolicy because claim namespace labels does not have correct value",
573-
"label", pair.Name, "value", v, "expected", pair.Value)
574-
continue LOOP_POLICY
575-
}
576-
} else {
578+
}
579+
// Behaves as a 'forall' on labels
580+
for _, pair := range constraints.HasLabels {
581+
if v, ok := hostNsLabels[pair.Name]; ok {
582+
if pair.Value != "" && v != pair.Value {
577583
log.V(1).Info(
578-
"Ignoring HostDeployPolicy because claim namespace labels does not have a required label",
579-
"label", pair.Name)
580-
continue LOOP_POLICY
584+
"Ignoring HostDeployPolicy because claim namespace labels does not have correct value",
585+
"label", pair.Name, "value", v, "expected", pair.Value)
586+
return false, nil
581587
}
588+
} else {
589+
log.V(1).Info(
590+
"Ignoring HostDeployPolicy because claim namespace labels does not have a required label",
591+
"label", pair.Name)
592+
return false, nil
582593
}
583-
log.V(1).Info("Accepting namespace because of HostDeployPolicy", "namespace", hostDeployPolicy.Namespace)
584-
AddSet(namespaces, hostDeployPolicy.Namespace)
585594
}
586-
m.Log.Info("Acceptable namespaces", "namespaces", namespaces)
587-
return namespaces, nil
595+
log.V(1).Info("Accepting namespace because of HostDeployPolicy", "namespace", hostDeployPolicy.Namespace)
596+
return true, nil
588597
}
589598

590599
func (m *HostManager) hostLabelSelectorForHostClaim() (labels.Selector, error) {
@@ -740,6 +749,43 @@ func syncReboot(hostMap, bmhMap map[string]string) bool {
740749
return updated
741750
}
742751

752+
// synchronize detached annotation from hostMap to bmhMap.
753+
func (m *HostManager) syncDetached(ctx context.Context, bmhNs string, hostMap, bmhMap map[string]string) (bool, error) {
754+
if annotValue, ok := hostMap[metal3api.DetachedAnnotation]; ok {
755+
if _, ok := bmhMap[metal3api.DetachedAnnotation]; ok {
756+
return false, nil
757+
}
758+
if compatiblePolicies, err := m.GetCompatiblePolicies(ctx, bmhNs); err != nil {
759+
return false, err
760+
} else if !slices.ContainsFunc(
761+
compatiblePolicies,
762+
func(hdp *metal3api.HostDeployPolicy) bool {
763+
return hdp.Spec.AllowsDetachedMode
764+
},
765+
) {
766+
m.Log.Info("HostClaim not allowed to detach underlying BareMetalHost")
767+
return false, err
768+
}
769+
inputArgs := metal3api.DetachedAnnotationArguments{}
770+
_ = json.Unmarshal([]byte(annotValue), &inputArgs)
771+
args := metal3api.DetachedAnnotationArguments{DeleteAction: inputArgs.DeleteAction}
772+
annot, err := json.Marshal(args)
773+
if err != nil {
774+
return false, err
775+
}
776+
bmhMap[metal3api.DetachedAnnotation] = string(annot)
777+
return true, nil
778+
} else if annotValue, ok := bmhMap[metal3api.DetachedAnnotation]; ok {
779+
args := metal3api.DetachedAnnotationArguments{}
780+
if err := json.Unmarshal([]byte(annotValue), &args); err != nil || args.Manager {
781+
return false, nil //nolint:nilerr // arbitrary value of annotation mean they are not handled by hostclaim
782+
}
783+
delete(bmhMap, metal3api.DetachedAnnotation)
784+
return true, nil
785+
}
786+
return false, nil
787+
}
788+
743789
type Set[T comparable] = map[T]struct{}
744790

745791
func NewSet[T comparable]() Set[T] {
@@ -754,3 +800,33 @@ func SetContains[T comparable](m Set[T], s T) bool {
754800
_, ok := m[s]
755801
return ok
756802
}
803+
804+
// GetCompatiblePolicies find all the HostDeployPolicies in the namespace of the bareMetalHost that
805+
// accept the namespace of the current HostClaim.
806+
func (m *HostManager) GetCompatiblePolicies(ctx context.Context, bmhNs string) ([]*metal3api.HostDeployPolicy, error) {
807+
policiesInNs := &metal3api.HostDeployPolicyList{}
808+
if err := m.client.List(ctx, policiesInNs, client.InNamespace(bmhNs)); err != nil {
809+
m.Log.Error(err, "Cannot access HostDeployPolicies", "bmhNamespace", bmhNs)
810+
return nil, err
811+
}
812+
hostNs := m.HostClaim.Namespace
813+
hostNsResource := &corev1.Namespace{}
814+
if err := m.client.Get(ctx, client.ObjectKey{Name: hostNs}, hostNsResource); err != nil {
815+
m.Log.Error(err, "cannot access the namespace of the claim")
816+
return nil, err
817+
}
818+
nsLabels := hostNsResource.Labels
819+
if nsLabels == nil {
820+
nsLabels = map[string]string{}
821+
}
822+
823+
var compatiblePolicies []*metal3api.HostDeployPolicy
824+
for _, hdp := range policiesInNs.Items {
825+
if accept, err := m.checkPolicy(&hdp, hostNs, nsLabels); err != nil {
826+
return nil, err
827+
} else if accept {
828+
compatiblePolicies = append(compatiblePolicies, &hdp)
829+
}
830+
}
831+
return compatiblePolicies, nil
832+
}

pkg/hostclaim/hostclaim_manager_test.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -564,6 +564,67 @@ var _ = Describe("HostClaim manager", func() {
564564
},
565565
)
566566

567+
type testCaseSyncDetach struct {
568+
AcceptHostClaimNs string
569+
InHostClaimAnnot string
570+
InBmhAnnot string
571+
OutHostClaimAnnot string
572+
OutBmhAnnot string
573+
Updated bool
574+
}
575+
576+
DescribeTable("test syncDetach",
577+
func(tc testCaseSyncDetach) {
578+
ctx := context.TODO()
579+
ns := NewNamespace(HostclaimNamespace).Build()
580+
581+
annotHc := map[string]string{}
582+
annotBmh := map[string]string{}
583+
if tc.InHostClaimAnnot != "" {
584+
annotHc = map[string]string{metal3api.DetachedAnnotation: tc.InHostClaimAnnot}
585+
}
586+
if tc.InBmhAnnot != "" {
587+
annotBmh = map[string]string{metal3api.DetachedAnnotation: tc.InBmhAnnot}
588+
}
589+
590+
bmh := NewBaremetalhost("bmh", "ns", metal3api.StateAvailable).SetAnnotations(annotBmh).SetConsumerRef(defaultConsumerRef).Build()
591+
hostClaim := NewHostclaim(HostclaimName).SetAssociatedBMH("ns", "bmh").SetAnnotations(annotHc).Build()
592+
hdp := NewHostdeploypolicy("hdp", "ns").AcceptNames([]string{tc.AcceptHostClaimNs}).AllowsDetachedMode().Build()
593+
fakeClient := fake.NewClientBuilder().WithScheme(setupScheme()).WithObjects(bmh, hostClaim, ns, hdp).Build()
594+
hostMgr, err := NewHostManager(fakeClient, GinkgoLogr, hostClaim, fakeClient)
595+
Expect(err).NotTo(HaveOccurred())
596+
updated, err := hostMgr.syncDetached(ctx, "ns", hostClaim.Annotations, bmh.Annotations)
597+
Expect(err).NotTo(HaveOccurred())
598+
Expect(updated).To(Equal(tc.Updated))
599+
outBmhAnnot := ""
600+
if bmh.Annotations != nil {
601+
outBmhAnnot = bmh.Annotations[metal3api.DetachedAnnotation]
602+
}
603+
Expect(outBmhAnnot).To(Equal(tc.OutBmhAnnot))
604+
},
605+
Entry("not allowed", testCaseSyncDetach{AcceptHostClaimNs: "other", InHostClaimAnnot: "{}", OutBmhAnnot: ""}),
606+
Entry("allowed",
607+
testCaseSyncDetach{
608+
AcceptHostClaimNs: HostclaimNamespace, InHostClaimAnnot: "{}", OutBmhAnnot: "{}", Updated: true}),
609+
Entry("sanitize",
610+
testCaseSyncDetach{
611+
AcceptHostClaimNs: HostclaimNamespace,
612+
InHostClaimAnnot: "{\"deleteAction\": \"delay\", \"manager\": true}",
613+
OutBmhAnnot: "{\"deleteAction\":\"delay\"}",
614+
Updated: true}),
615+
Entry("no override",
616+
testCaseSyncDetach{AcceptHostClaimNs: HostclaimNamespace, InHostClaimAnnot: "{}",
617+
InBmhAnnot: "zzz", OutBmhAnnot: "zzz"}),
618+
Entry("delete",
619+
testCaseSyncDetach{
620+
AcceptHostClaimNs: HostclaimNamespace, InHostClaimAnnot: "",
621+
InBmhAnnot: "{}", OutBmhAnnot: "", Updated: true}),
622+
Entry("no delete",
623+
testCaseSyncDetach{
624+
AcceptHostClaimNs: HostclaimNamespace, InHostClaimAnnot: "",
625+
InBmhAnnot: "{\"manager\": true}", OutBmhAnnot: "{\"manager\": true}"}),
626+
)
627+
567628
type testCaseUpdate struct {
568629
HostClaim *metal3api.HostClaim
569630
ExpectFail bool

0 commit comments

Comments
 (0)