Skip to content

✨ Handle Kubernetes events for waiting CoreProvider in preflight check #703

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
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
2 changes: 1 addition & 1 deletion .golangci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ linters:
- errname
- errorlint
- exhaustive
- exportloopref
- copyloopvar
- forcetypeassert
- ginkgolinter
- goconst
Expand Down
21 changes: 11 additions & 10 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ limitations under the License.
package main

import (
"context"
"flag"
"fmt"
"os"
Expand All @@ -37,7 +38,7 @@ import (
"sigs.k8s.io/cluster-api/util/flags"
"sigs.k8s.io/cluster-api/version"
ctrl "sigs.k8s.io/controller-runtime"
cache "sigs.k8s.io/controller-runtime/pkg/cache"
"sigs.k8s.io/controller-runtime/pkg/cache"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller"
"sigs.k8s.io/controller-runtime/pkg/healthz"
Expand Down Expand Up @@ -194,7 +195,7 @@ func main() {
ctx := ctrl.SetupSignalHandler()

setupChecks(mgr)
setupReconcilers(mgr, watchConfigSecretChanges)
setupReconcilers(ctx, mgr, watchConfigSecretChanges)
setupWebhooks(mgr)

// +kubebuilder:scaffold:builder
Expand All @@ -218,14 +219,14 @@ func setupChecks(mgr ctrl.Manager) {
}
}

func setupReconcilers(mgr ctrl.Manager, watchConfigSecretChanges bool) {
func setupReconcilers(ctx context.Context, mgr ctrl.Manager, watchConfigSecretChanges bool) {
if err := (&providercontroller.GenericProviderReconciler{
Provider: &operatorv1.CoreProvider{},
ProviderList: &operatorv1.CoreProviderList{},
Client: mgr.GetClient(),
Config: mgr.GetConfig(),
WatchConfigSecretChanges: watchConfigSecretChanges,
}).SetupWithManager(mgr, concurrency(concurrencyNumber)); err != nil {
}).SetupWithManager(ctx, mgr, concurrency(concurrencyNumber)); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "CoreProvider")
os.Exit(1)
}
Expand All @@ -236,7 +237,7 @@ func setupReconcilers(mgr ctrl.Manager, watchConfigSecretChanges bool) {
Client: mgr.GetClient(),
Config: mgr.GetConfig(),
WatchConfigSecretChanges: watchConfigSecretChanges,
}).SetupWithManager(mgr, concurrency(concurrencyNumber)); err != nil {
}).SetupWithManager(ctx, mgr, concurrency(concurrencyNumber)); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "InfrastructureProvider")
os.Exit(1)
}
Expand All @@ -247,7 +248,7 @@ func setupReconcilers(mgr ctrl.Manager, watchConfigSecretChanges bool) {
Client: mgr.GetClient(),
Config: mgr.GetConfig(),
WatchConfigSecretChanges: watchConfigSecretChanges,
}).SetupWithManager(mgr, concurrency(concurrencyNumber)); err != nil {
}).SetupWithManager(ctx, mgr, concurrency(concurrencyNumber)); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "BootstrapProvider")
os.Exit(1)
}
Expand All @@ -258,7 +259,7 @@ func setupReconcilers(mgr ctrl.Manager, watchConfigSecretChanges bool) {
Client: mgr.GetClient(),
Config: mgr.GetConfig(),
WatchConfigSecretChanges: watchConfigSecretChanges,
}).SetupWithManager(mgr, concurrency(concurrencyNumber)); err != nil {
}).SetupWithManager(ctx, mgr, concurrency(concurrencyNumber)); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "ControlPlaneProvider")
os.Exit(1)
}
Expand All @@ -269,7 +270,7 @@ func setupReconcilers(mgr ctrl.Manager, watchConfigSecretChanges bool) {
Client: mgr.GetClient(),
Config: mgr.GetConfig(),
WatchConfigSecretChanges: watchConfigSecretChanges,
}).SetupWithManager(mgr, concurrency(concurrencyNumber)); err != nil {
}).SetupWithManager(ctx, mgr, concurrency(concurrencyNumber)); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "AddonProvider")
os.Exit(1)
}
Expand All @@ -280,7 +281,7 @@ func setupReconcilers(mgr ctrl.Manager, watchConfigSecretChanges bool) {
Client: mgr.GetClient(),
Config: mgr.GetConfig(),
WatchConfigSecretChanges: watchConfigSecretChanges,
}).SetupWithManager(mgr, concurrency(concurrencyNumber)); err != nil {
}).SetupWithManager(ctx, mgr, concurrency(concurrencyNumber)); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "IPAMProvider")
os.Exit(1)
}
Expand All @@ -291,7 +292,7 @@ func setupReconcilers(mgr ctrl.Manager, watchConfigSecretChanges bool) {
Client: mgr.GetClient(),
Config: mgr.GetConfig(),
WatchConfigSecretChanges: watchConfigSecretChanges,
}).SetupWithManager(mgr, concurrency(concurrencyNumber)); err != nil {
}).SetupWithManager(ctx, mgr, concurrency(concurrencyNumber)); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "RuntimeExtensionProvider")
os.Exit(1)
}
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ go 1.23

require (
github.com/MakeNowJust/heredoc v1.0.0
github.com/Masterminds/goutils v1.1.1
github.com/evanphx/json-patch/v5 v5.9.11
github.com/go-errors/errors v1.5.1
github.com/go-logr/logr v1.4.2
Expand All @@ -30,7 +31,6 @@ require (

require (
dario.cat/mergo v1.0.1 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver/v3 v3.3.0 // indirect
github.com/Masterminds/sprig/v3 v3.3.0 // indirect
github.com/NYTimes/gziphandler v1.1.1 // indirect
Expand Down
6 changes: 0 additions & 6 deletions internal/controller/consts.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,7 @@ limitations under the License.

package controller

import "time"

const (
// preflightFailedRequeueAfter is how long to wait before trying to reconcile
// if some preflight check has failed.
preflightFailedRequeueAfter = 30 * time.Second

// configPath is the path to the clusterctl config file.
configPath = "/config/clusterctl.yaml"
)
69 changes: 69 additions & 0 deletions internal/controller/coreprovider_to_providers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
Copyright 2025 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package controller

import (
"context"
"fmt"

operatorv1 "sigs.k8s.io/cluster-api-operator/api/v1alpha2"
"sigs.k8s.io/cluster-api-operator/internal/controller/genericprovider"
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
"sigs.k8s.io/cluster-api/util/conditions"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/handler"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
)

// newCoreProviderToProviderFuncMapForProviderList maps a ready CoreProvider object to all other provider objects.
// It lists all the providers and if its PreflightCheckCondition is not True, this object will be added to the resulting request.
// This means that notifications will only be sent to those objects that have not pass PreflightCheck.
func newCoreProviderToProviderFuncMapForProviderList(k8sClient client.Client, providerList genericprovider.GenericProviderList) handler.MapFunc {
providerListType := fmt.Sprintf("%t", providerList)

return func(ctx context.Context, obj client.Object) []reconcile.Request {
log := ctrl.LoggerFrom(ctx).WithValues("provider", map[string]string{"name": obj.GetName(), "namespace": obj.GetNamespace()}, "providerListType", providerListType)
coreProvider, ok := obj.(*operatorv1.CoreProvider)

if !ok {
log.Error(fmt.Errorf("expected a %T but got a %T", operatorv1.CoreProvider{}, obj), "unable to cast object")
return nil
}

// We don't want to raise events if CoreProvider is not ready yet.
if !conditions.IsTrue(coreProvider, clusterv1.ReadyCondition) {
return nil
}

var requests []reconcile.Request

if err := k8sClient.List(ctx, providerList); err != nil {
log.Error(err, "failed to list providers")
return nil
}

for _, provider := range providerList.GetItems() {
if !conditions.IsTrue(provider, operatorv1.PreflightCheckCondition) {
// Raise secondary events for the providers that fail PreflightCheck.
requests = append(requests, reconcile.Request{NamespacedName: client.ObjectKeyFromObject(provider)})
}
}

return requests
}
}
143 changes: 143 additions & 0 deletions internal/controller/coreprovider_to_providers_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
/*
Copyright 2025 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package controller

import (
"testing"

. "github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
operatorv1 "sigs.k8s.io/cluster-api-operator/api/v1alpha2"
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
)

func TestCoreProviderToProvidersMapper(t *testing.T) {
g := NewWithT(t)

testCases := []struct {
name string
coreProvider client.Object
expected []ctrl.Request
}{
{
name: "Core provider Ready condition is True",
coreProvider: &operatorv1.CoreProvider{
ObjectMeta: metav1.ObjectMeta{
Name: "core-provider",
Namespace: testNamespaceName,
},
Status: operatorv1.CoreProviderStatus{
ProviderStatus: operatorv1.ProviderStatus{
Conditions: clusterv1.Conditions{
{
Type: clusterv1.ReadyCondition,
Status: corev1.ConditionTrue,
LastTransitionTime: metav1.Now(),
Message: "Provider is ready",
},
},
},
},
},
expected: []reconcile.Request{
{NamespacedName: types.NamespacedName{Namespace: testNamespaceName, Name: "preflight-checks-condition-false"}},
{NamespacedName: types.NamespacedName{Namespace: testNamespaceName, Name: "empty-status-conditions"}},
},
},
{
name: "Core provider is not ready",
coreProvider: &operatorv1.CoreProvider{
ObjectMeta: metav1.ObjectMeta{
Name: "core-provider",
Namespace: testNamespaceName,
},
},
expected: []reconcile.Request{},
},
}
k8sClient := fake.NewClientBuilder().
WithScheme(setupScheme()).
WithObjects(
&operatorv1.InfrastructureProvider{
ObjectMeta: metav1.ObjectMeta{
Name: "preflight-checks-condition-false",
Namespace: testNamespaceName,
},
Spec: operatorv1.InfrastructureProviderSpec{
ProviderSpec: operatorv1.ProviderSpec{},
},
Status: operatorv1.InfrastructureProviderStatus{
ProviderStatus: operatorv1.ProviderStatus{
Conditions: clusterv1.Conditions{
{
Type: operatorv1.PreflightCheckCondition,
Status: corev1.ConditionFalse,
LastTransitionTime: metav1.Now(),
Reason: operatorv1.WaitingForCoreProviderReadyReason,
Message: "Core provider is not ready",
},
},
},
},
},
&operatorv1.InfrastructureProvider{
ObjectMeta: metav1.ObjectMeta{
Name: "preflight-checks-condition-true",
Namespace: testNamespaceName,
},
Spec: operatorv1.InfrastructureProviderSpec{
ProviderSpec: operatorv1.ProviderSpec{},
},
Status: operatorv1.InfrastructureProviderStatus{
ProviderStatus: operatorv1.ProviderStatus{
Conditions: clusterv1.Conditions{
{
Type: operatorv1.PreflightCheckCondition,
Status: corev1.ConditionTrue,
LastTransitionTime: metav1.Now(),
Message: "Core provider is ready",
},
},
},
},
},
&operatorv1.InfrastructureProvider{
ObjectMeta: metav1.ObjectMeta{
Name: "empty-status-conditions",
Namespace: testNamespaceName,
},
Spec: operatorv1.InfrastructureProviderSpec{
ProviderSpec: operatorv1.ProviderSpec{},
},
},
).
Build()

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
requests := newCoreProviderToProviderFuncMapForProviderList(k8sClient, &operatorv1.InfrastructureProviderList{})(ctx, tc.coreProvider)
g.Expect(requests).To(HaveLen(len(tc.expected)))
g.Expect(requests).To(ContainElements(tc.expected))
})
}
}
20 changes: 19 additions & 1 deletion internal/controller/genericprovider_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"errors"
"fmt"
"hash"
"reflect"

corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
Expand Down Expand Up @@ -54,16 +55,33 @@ const (
appliedSpecHashAnnotation = "operator.cluster.x-k8s.io/applied-spec-hash"
)

func (r *GenericProviderReconciler) SetupWithManager(mgr ctrl.Manager, options controller.Options) error {
func (r *GenericProviderReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager, options controller.Options) error {
builder := ctrl.NewControllerManagedBy(mgr).
For(r.Provider)

if r.WatchConfigSecretChanges {
if err := mgr.GetFieldIndexer().IndexField(ctx, r.Provider, configSecretNameField, configSecretNameIndexFunc); err != nil {
return err
}

if err := mgr.GetFieldIndexer().IndexField(ctx, r.Provider, configSecretNamespaceField, configSecretNamespaceIndexFunc); err != nil {
return err
}

builder.Watches(
&corev1.Secret{},
handler.EnqueueRequestsFromMapFunc(newSecretToProviderFuncMapForProviderList(r.Client, r.ProviderList)),
)
}

// We don't want to receive secondary events from the CoreProvider for itself.
if reflect.TypeOf(r.Provider) != reflect.TypeOf(genericprovider.GenericProvider(&operatorv1.CoreProvider{})) {
builder.Watches(
&operatorv1.CoreProvider{},
handler.EnqueueRequestsFromMapFunc(newCoreProviderToProviderFuncMapForProviderList(r.Client, r.ProviderList)),
)
}

return builder.WithOptions(options).
Complete(r)
}
Expand Down
Loading