Skip to content

Commit cbdae05

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 cbdae05

5 files changed

Lines changed: 172 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 {

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: 117 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, err := m.syncDetached(ctx, bmh.Namespace, m.HostClaim.Annotations, bmh.Annotations); err != nil {
205+
return err
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
544-
}
545-
constraints := hostDeployPolicy.Spec.HostClaimNamespaces
546-
if constraints == nil {
547-
log.V(1).Info("Ignoring HostDeployPolicy without constraint")
548-
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)
549549
}
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
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
553573
}
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-
}
574+
if !b {
575+
log.V(1).Info("Ignoring HostDeployPolicy because claim namespace does not match regex")
576+
return false, nil
566577
}
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,42 @@ 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+
if annot, err := json.Marshal(args); err != nil {
773+
return false, err
774+
} else {
775+
bmhMap[metal3api.DetachedAnnotation] = string(annot)
776+
}
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
782+
}
783+
delete(bmhMap, metal3api.DetachedAnnotation)
784+
}
785+
return false, nil
786+
}
787+
743788
type Set[T comparable] = map[T]struct{}
744789

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

pkg/hostclaim/hostclaim_manager_test.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -564,6 +564,49 @@ 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+
}
574+
575+
DescribeTable("test syncDetach",
576+
func(tc testCaseSyncDetach) {
577+
ctx := context.TODO()
578+
ns := NewNamespace(HostclaimNamespace).Build()
579+
580+
annotHc := map[string]string{}
581+
annotBmh := map[string]string{}
582+
if tc.InHostClaimAnnot != "" {
583+
annotHc = map[string]string{metal3api.DetachedAnnotation: tc.InHostClaimAnnot}
584+
}
585+
if tc.InBmhAnnot != "" {
586+
annotBmh = map[string]string{metal3api.DetachedAnnotation: tc.InBmhAnnot}
587+
}
588+
589+
bmh := NewBaremetalhost("bmh", "ns", metal3api.StateAvailable).SetAnnotations(annotBmh).SetConsumerRef(defaultConsumerRef).Build()
590+
hostClaim := NewHostclaim(HostclaimName).SetAssociatedBMH("ns", "bmh").SetAnnotations(annotHc).Build()
591+
hdp := NewHostdeploypolicy("hdp", "ns").AcceptNames([]string{tc.AcceptHostClaimNs}).AllowsDetachedMode().Build()
592+
fakeClient := fake.NewClientBuilder().WithScheme(setupScheme()).WithObjects(bmh, hostClaim, ns, hdp).Build()
593+
hostMgr, err := NewHostManager(fakeClient, GinkgoLogr, hostClaim, fakeClient)
594+
Expect(err).NotTo(HaveOccurred())
595+
hostMgr.syncDetached(ctx, "ns", hostClaim.Annotations, bmh.Annotations)
596+
outBmhAnnot := ""
597+
if bmh.Annotations != nil {
598+
outBmhAnnot = bmh.Annotations[metal3api.DetachedAnnotation]
599+
}
600+
Expect(outBmhAnnot).To(Equal(tc.OutBmhAnnot))
601+
},
602+
Entry("not allowed", testCaseSyncDetach{AcceptHostClaimNs: "other", InHostClaimAnnot: "{}", OutBmhAnnot: ""}),
603+
Entry("allowed", testCaseSyncDetach{AcceptHostClaimNs: HostclaimNamespace, InHostClaimAnnot: "{}", OutBmhAnnot: "{}"}),
604+
Entry("sanitize", testCaseSyncDetach{AcceptHostClaimNs: HostclaimNamespace, InHostClaimAnnot: "{\"deleteAction\": \"delay\", \"manager\": true}", OutBmhAnnot: "{\"deleteAction\":\"delay\"}"}),
605+
Entry("no override", testCaseSyncDetach{AcceptHostClaimNs: HostclaimNamespace, InHostClaimAnnot: "{}", InBmhAnnot: "zzz", OutBmhAnnot: "zzz"}),
606+
Entry("delete", testCaseSyncDetach{AcceptHostClaimNs: HostclaimNamespace, InHostClaimAnnot: "", InBmhAnnot: "{}", OutBmhAnnot: ""}),
607+
Entry("no delete", testCaseSyncDetach{AcceptHostClaimNs: HostclaimNamespace, InHostClaimAnnot: "", InBmhAnnot: "{\"manager\": true}", OutBmhAnnot: "{\"manager\": true}"}),
608+
)
609+
567610
type testCaseUpdate struct {
568611
HostClaim *metal3api.HostClaim
569612
ExpectFail bool

0 commit comments

Comments
 (0)