Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 52 additions & 10 deletions internal/controller/metal3.io/baremetalhost_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import (
k8serrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/cluster-api/util/conditions"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/builder"
Expand Down Expand Up @@ -71,12 +72,13 @@ type BareMetalHostReconciler struct {
// Instead of passing a zillion arguments to the action of a phase,
// hold them in a struct.
type reconcileInfo struct {
log logr.Logger
host *metal3api.BareMetalHost
request ctrl.Request
bmcCredsSecret *corev1.Secret
events []corev1.Event
postSaveCallbacks []func()
log logr.Logger
host *metal3api.BareMetalHost
request ctrl.Request
bmcCredsSecret *corev1.Secret
preprovisioningNetworkDataSecret *corev1.Secret
events []corev1.Event
postSaveCallbacks []func()
}

// match the provisioner.EventPublisher interface.
Expand Down Expand Up @@ -205,12 +207,29 @@ func (r *BareMetalHostReconciler) Reconcile(ctx context.Context, request ctrl.Re
}
}

var preprovisioningNetworkDataSecret *corev1.Secret
if host.Spec.PreprovisioningNetworkDataName != "" &&
host.Status.Provisioning.State != metal3api.StateNone &&
host.Status.Provisioning.State != metal3api.StateUnmanaged {
preprovisioningNetworkDataSecret, err = r.acquirePreprovisioningNetworkDataSecret(ctx, host)
if err != nil {
if hostInDeletionFlow(host) && k8serrors.IsNotFound(err) {
preprovisioningNetworkDataSecret = &corev1.Secret{}
} else if !hostInDeletionFlow(host) {
reqLogger.Info("failed to acquire preprovisioning network data secret", "error", err)
} else {
return ctrl.Result{}, fmt.Errorf("failed to acquire preprovisioning network data secret during deletion: %w", err)
}
}
}

initialState := host.Status.Provisioning.State
info := &reconcileInfo{
log: reqLogger.WithValues("provisioningState", initialState),
host: host,
request: request,
bmcCredsSecret: bmcCredsSecret,
log: reqLogger.WithValues("provisioningState", initialState),
host: host,
request: request,
bmcCredsSecret: bmcCredsSecret,
preprovisioningNetworkDataSecret: preprovisioningNetworkDataSecret,
}

prov, err := r.ProvisionerFactory.NewProvisioner(ctx, provisioner.BuildHostData(*host, *bmcCreds), info.publishEvent)
Expand Down Expand Up @@ -567,6 +586,13 @@ func (r *BareMetalHostReconciler) actionDeleting(ctx context.Context, prov provi
return actionError{err}
}

if info.preprovisioningNetworkDataSecret != nil && info.preprovisioningNetworkDataSecret.Name != "" {
err = secretManager.ReleaseSecret(ctx, info.preprovisioningNetworkDataSecret)
if err != nil {
return actionError{err}
}
}

if controllerutil.RemoveFinalizer(info.host, metal3api.BareMetalHostFinalizer) {
info.log.Info("cleanup is complete, removed finalizer",
"remaining", info.host.Finalizers)
Expand Down Expand Up @@ -2401,6 +2427,22 @@ func (r *BareMetalHostReconciler) getBMCSecretAndSetOwner(ctx context.Context, r
return bmcCredsSecret, nil
}

// acquirePreprovisioningNetworkDataSecret claims the Secret referenced by
// spec.preprovisioningNetworkDataName with a finalizer so it is not removed
// before the host finishes deletion. Callers must ensure
// spec.preprovisioningNetworkDataName is set.
func (r *BareMetalHostReconciler) acquirePreprovisioningNetworkDataSecret(ctx context.Context, host *metal3api.BareMetalHost) (*corev1.Secret, error) {
secretManager := r.secretManager(ctx, r.Log.WithValues(
"baremetalhost", types.NamespacedName{Namespace: host.Namespace, Name: host.Name},
))
key := types.NamespacedName{
Name: host.Spec.PreprovisioningNetworkDataName,
Namespace: host.Namespace,
}

return secretManager.ObtainSecretWithFinalizer(ctx, key, host.Status.Provisioning.State != metal3api.StateDeleting)
}

func credentialsFromSecret(bmcCredsSecret *corev1.Secret) *bmc.Credentials {
// We trim surrounding whitespace because those characters are
// unlikely to be part of the username or password and it is
Expand Down
28 changes: 26 additions & 2 deletions internal/controller/metal3.io/host_config_data.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,24 @@ import (
metal3api "github.com/metal3-io/baremetal-operator/apis/metal3.io/v1alpha1"
"github.com/metal3-io/baremetal-operator/pkg/secretutils"
corev1 "k8s.io/api/core/v1"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/types"
)

// hostInDeletionFlow reports whether the host is being removed. During this
// window a missing preprovisioning network Secret should not block progress.
func hostInDeletionFlow(host *metal3api.BareMetalHost) bool {
if !host.DeletionTimestamp.IsZero() {
return true
}
switch host.Status.Provisioning.State {
case metal3api.StateDeleting, metal3api.StatePoweringOffBeforeDelete:
return true
default:
return false
}
}

// hostConfigData is an implementation of host configuration data interface.
// Object is able to retrieve data from secrets referenced in a host spec.
type hostConfigData struct {
Expand All @@ -23,7 +38,7 @@ type hostConfigData struct {
// Generic method for data extraction from a Secret. Function uses dataKey
// parameter to detirmine which data to return in case secret contins multiple
// keys.
func (hcd *hostConfigData) getSecretData(ctx context.Context, name, namespace, dataKey string) (string, error) {
func (hcd *hostConfigData) getSecretData(ctx context.Context, name, namespace, dataKey string, addFinalizer bool) (string, error) {
if namespace != hcd.host.Namespace {
return "", fmt.Errorf("%s secret must be in same namespace as host %s/%s", dataKey, hcd.host.Namespace, hcd.host.Name)
}
Expand All @@ -33,7 +48,7 @@ func (hcd *hostConfigData) getSecretData(ctx context.Context, name, namespace, d
Namespace: namespace,
}

secret, err := hcd.secretManager.ObtainSecret(ctx, key)
secret, err := hcd.secretManager.ObtainSecretWithFinalizer(ctx, key, addFinalizer)
if err != nil {
return "", err
}
Expand Down Expand Up @@ -67,6 +82,7 @@ func (hcd *hostConfigData) UserData(ctx context.Context) (string, error) {
hcd.host.Spec.UserData.Name,
namespace,
"userData",
false,
)
}

Expand All @@ -91,6 +107,7 @@ func (hcd *hostConfigData) NetworkData(ctx context.Context) (string, error) {
networkData.Name,
namespace,
"networkData",
false,
)
if err != nil {
var noDataErr NoDataInSecretError
Expand All @@ -107,18 +124,24 @@ func (hcd *hostConfigData) PreprovisioningNetworkData(ctx context.Context) (stri
if hcd.host.Spec.PreprovisioningNetworkDataName == "" {
return "", nil
}
addFinalizer := !hostInDeletionFlow(hcd.host)
networkDataRaw, err := hcd.getSecretData(
ctx,
hcd.host.Spec.PreprovisioningNetworkDataName,
hcd.host.Namespace,
"networkData",
addFinalizer,
)
if err != nil {
var noDataErr NoDataInSecretError
if errors.As(err, &noDataErr) {
hcd.log.Info("PreprovisioningNetworkData networkData key is not set, returning empty data")
return "", nil
}
if k8serrors.IsNotFound(err) && hostInDeletionFlow(hcd.host) {
hcd.log.Info("PreprovisioningNetworkData secret not found during host deletion, returning empty data")
return "", nil
}
}
return networkDataRaw, err
}
Expand All @@ -138,5 +161,6 @@ func (hcd *hostConfigData) MetaData(ctx context.Context) (string, error) {
hcd.host.Spec.MetaData.Name,
namespace,
"metaData",
false,
)
}
58 changes: 58 additions & 0 deletions internal/controller/metal3.io/host_config_data_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
ctrl "sigs.k8s.io/controller-runtime"
fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake"
Expand Down Expand Up @@ -80,6 +81,15 @@ func TestLabelSecrets(t *testing.T) {
},
},
},
{
name: "preprovisioning-network-data",
getter: func(hcd *hostConfigData) (string, error) {
return hcd.PreprovisioningNetworkData(t.Context())
},
hostSpec: &metal3api.BareMetalHostSpec{
PreprovisioningNetworkDataName: "preprovisioning-network-data",
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
Expand Down Expand Up @@ -107,6 +117,54 @@ func TestLabelSecrets(t *testing.T) {
}
}

func TestAcquirePreprovisioningNetworkSecret(t *testing.T) {
host := newHost("host", &metal3api.BareMetalHostSpec{
PreprovisioningNetworkDataName: "preprov-net-data",
})
host.Status.Provisioning.State = metal3api.StateRegistering

secret := newSecret("preprov-net-data", map[string]string{"networkData": "key: value"})
c := fakeclient.NewClientBuilder().WithObjects(host, secret).Build()
baselog := ctrl.Log.WithName("controllers").WithName("BareMetalHost")
hcd := &hostConfigData{
host: host,
log: baselog.WithName("host_config_data"),
secretManager: secretutils.NewSecretManager(baselog, c, c),
}

_, err := hcd.PreprovisioningNetworkData(t.Context())
require.NoError(t, err)

actualSecret := &corev1.Secret{}
err = c.Get(t.Context(), types.NamespacedName{Name: "preprov-net-data", Namespace: namespace}, actualSecret)
require.NoError(t, err)
assert.Equal(t, secretutils.LabelEnvironmentValue, actualSecret.Labels[secretutils.LabelEnvironmentName])
assert.Contains(t, actualSecret.Finalizers, secretutils.SecretsFinalizer)
assert.Empty(t, actualSecret.OwnerReferences)
}

func TestPreprovisioningNetworkSecretNotFoundDuringDeletion(t *testing.T) {
host := newHost("host", &metal3api.BareMetalHostSpec{
PreprovisioningNetworkDataName: "missing-preprov-net",
})
host.Status.Provisioning.State = metal3api.StatePoweringOffBeforeDelete
now := metav1.Now()
host.DeletionTimestamp = &now
host.Finalizers = []string{metal3api.BareMetalHostFinalizer}

c := fakeclient.NewClientBuilder().WithObjects(host).Build()
baselog := ctrl.Log.WithName("controllers").WithName("BareMetalHost")
hcd := &hostConfigData{
host: host,
log: baselog.WithName("host_config_data"),
secretManager: secretutils.NewSecretManager(baselog, c, c),
}

data, err := hcd.PreprovisioningNetworkData(t.Context())
require.NoError(t, err)
assert.Empty(t, data)
}

func TestProvisionWithHostConfig(t *testing.T) {
testBMCSecret := newBMCCredsSecret(defaultSecretName, "User", "Pass")

Expand Down
7 changes: 7 additions & 0 deletions pkg/secretutils/secret_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,13 @@ func (sm *SecretManager) ObtainSecret(ctx context.Context, key types.NamespacedN
return sm.obtainSecretForOwner(ctx, key, nil, false)
}

// ObtainSecretWithFinalizer retrieves a Secret and ensures that it has a label
// that will ensure it is present in the cache, and optionally adds the secrets
// manager finalizer without setting an owner reference.
func (sm *SecretManager) ObtainSecretWithFinalizer(ctx context.Context, key types.NamespacedName, addFinalizer bool) (*corev1.Secret, error) {
return sm.obtainSecretForOwner(ctx, key, nil, addFinalizer)
}

// ReleaseSecret removes secrets manager finalizer from specified secret when needed.
func (sm *SecretManager) ReleaseSecret(ctx context.Context, secret *corev1.Secret) error {
if !slices.Contains(secret.Finalizers, SecretsFinalizer) {
Expand Down