diff --git a/go.mod b/go.mod index 6d134ff8..3be34096 100644 --- a/go.mod +++ b/go.mod @@ -24,9 +24,7 @@ require ( k8s.io/code-generator v0.30.0 k8s.io/klog/v2 v2.120.1 k8s.io/utils v0.0.0-20240310230437-4693a0247e57 - // Pinned due to a breaking change in k8s.io/client-go/tools/leaderelection in v0.30.0 - // TODO: Update to the latest semver version when https://github.com/kubernetes-sigs/controller-runtime/pull/2693 is released - sigs.k8s.io/controller-runtime v0.17.1-0.20240418082203-04706074d2f1 + sigs.k8s.io/controller-runtime v0.18.0 sigs.k8s.io/controller-tools v0.14.0 sigs.k8s.io/yaml v1.4.0 ) @@ -101,8 +99,8 @@ require ( google.golang.org/protobuf v1.33.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect - k8s.io/apiextensions-apiserver v0.30.0-rc.2 // indirect - k8s.io/component-base v0.30.0-rc.2 // indirect + k8s.io/apiextensions-apiserver v0.30.0 // indirect + k8s.io/component-base v0.30.0 // indirect k8s.io/gengo/v2 v2.0.0-20240228010128-51d4e06bde70 // indirect k8s.io/klog v1.0.0 // indirect k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect diff --git a/go.sum b/go.sum index 5d8c339b..f316846a 100644 --- a/go.sum +++ b/go.sum @@ -504,16 +504,16 @@ k8c.io/reconciler v0.5.0 h1:BHpelg1UfI/7oBFctqOq8sX6qzflXpl3SlvHe7e8wak= k8c.io/reconciler v0.5.0/go.mod h1:pT1+SVcVXJQeBJhpJBXQ5XW64QnKKeYTnVlQf0dGE0k= k8s.io/api v0.30.0 h1:siWhRq7cNjy2iHssOB9SCGNCl2spiF1dO3dABqZ8niA= k8s.io/api v0.30.0/go.mod h1:OPlaYhoHs8EQ1ql0R/TsUgaRPhpKNxIMrKQfWUp8QSE= -k8s.io/apiextensions-apiserver v0.30.0-rc.2 h1:nnQg+c72aanAIrrPSyds0jtazCjOQDHo2vpazxem/TI= -k8s.io/apiextensions-apiserver v0.30.0-rc.2/go.mod h1:Vfet39CooU8WJYMintiVVNCJhHHtiJ/+ZX3CgA7O+so= +k8s.io/apiextensions-apiserver v0.30.0 h1:jcZFKMqnICJfRxTgnC4E+Hpcq8UEhT8B2lhBcQ+6uAs= +k8s.io/apiextensions-apiserver v0.30.0/go.mod h1:N9ogQFGcrbWqAY9p2mUAL5mGxsLqwgtUce127VtRX5Y= k8s.io/apimachinery v0.30.0 h1:qxVPsyDM5XS96NIh9Oj6LavoVFYff/Pon9cZeDIkHHA= k8s.io/apimachinery v0.30.0/go.mod h1:iexa2somDaxdnj7bha06bhb43Zpa6eWH8N8dbqVjTUc= k8s.io/client-go v0.30.0 h1:sB1AGGlhY/o7KCyCEQ0bPWzYDL0pwOZO4vAtTSh/gJQ= k8s.io/client-go v0.30.0/go.mod h1:g7li5O5256qe6TYdAMyX/otJqMhIiGgTapdLchhmOaY= k8s.io/code-generator v0.30.0 h1:3VUVqHvWFSVSm9kqL/G6kD4ZwNdHF6J/jPyo3Jgjy3k= k8s.io/code-generator v0.30.0/go.mod h1:mBMZhfRR4IunJUh2+7LVmdcWwpouCH5+LNPkZ3t/v7Q= -k8s.io/component-base v0.30.0-rc.2 h1:0Qa6faUg01rBp9VxU76B8PmK58rBcAGB+7r4ckpLtgI= -k8s.io/component-base v0.30.0-rc.2/go.mod h1:rdQm+7+FBi+t74zJKiKBYVgQJEiNRMqvESRh8/f5z5k= +k8s.io/component-base v0.30.0 h1:cj6bp38g0ainlfYtaOQuRELh5KSYjhKxM+io7AUIk4o= +k8s.io/component-base v0.30.0/go.mod h1:V9x/0ePFNaKeKYA3bOvIbrNoluTSG+fSJKjLdjOoeXQ= k8s.io/gengo/v2 v2.0.0-20240228010128-51d4e06bde70 h1:NGrVE502P0s0/1hudf8zjgwki1X/TByhmAoILTarmzo= k8s.io/gengo/v2 v2.0.0-20240228010128-51d4e06bde70/go.mod h1:VH3AT8AaQOqiGjMF9p0/IM1Dj+82ZwjfxUP1IxaHE+8= k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8= @@ -529,8 +529,8 @@ k8s.io/utils v0.0.0-20240310230437-4693a0247e57/go.mod h1:OLgZIPagt7ERELqWJFomSt rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= -sigs.k8s.io/controller-runtime v0.17.1-0.20240418082203-04706074d2f1 h1:W15Y5zHVUsH1YJvstRqy6lG0KquU7kS2ooGC5poLnrU= -sigs.k8s.io/controller-runtime v0.17.1-0.20240418082203-04706074d2f1/go.mod h1:umEFUKWCSYpq2U4tNN7riBXU6iiulk7bdF0XZq9LzvU= +sigs.k8s.io/controller-runtime v0.18.0 h1:Z7jKuX784TQSUL1TIyeuF7j8KXZ4RtSX0YgtjKcSTME= +sigs.k8s.io/controller-runtime v0.18.0/go.mod h1:tuAt1+wbVsXIT8lPtk5RURxqAnq7xkpv2Mhttslg7Hw= sigs.k8s.io/controller-tools v0.14.0 h1:rnNoCC5wSXlrNoBKKzL70LNJKIQKEzT6lloG6/LF73A= sigs.k8s.io/controller-tools v0.14.0/go.mod h1:TV7uOtNNnnR72SpzhStvPkoS/U5ir0nMudrkrC4M9Sc= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= diff --git a/pkg/controllers/osc/osc_controller.go b/pkg/controllers/osc/osc_controller.go index 432d651b..ae1692cc 100644 --- a/pkg/controllers/osc/osc_controller.go +++ b/pkg/controllers/osc/osc_controller.go @@ -42,13 +42,12 @@ import ( "k8s.io/client-go/tools/clientcmd/api" "k8s.io/client-go/tools/record" ctrlruntime "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/builder" ctrlruntimeclient "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller" - "sigs.k8s.io/controller-runtime/pkg/handler" "sigs.k8s.io/controller-runtime/pkg/manager" "sigs.k8s.io/controller-runtime/pkg/predicate" "sigs.k8s.io/controller-runtime/pkg/reconcile" - "sigs.k8s.io/controller-runtime/pkg/source" ) const ( @@ -121,16 +120,16 @@ func Add( nodeRegistryCredentialsSecret: nodeRegistryCredentialsSecret, kubeletFeatureGates: kubeletFeatureGates, } - c, err := controller.New(ControllerName, mgr, controller.Options{Reconciler: reconciler, MaxConcurrentReconciles: workerCount}) - if err != nil { - return err - } - if err := c.Watch(source.Kind(mgr.GetCache(), &clusterv1alpha1.MachineDeployment{}), &handler.EnqueueRequestForObject{}, filterMachineDeploymentPredicate()); err != nil { - return fmt.Errorf("failed to watch MachineDeployments: %w", err) - } + _, err := builder.ControllerManagedBy(mgr). + Named(ControllerName). + WithOptions(controller.Options{ + MaxConcurrentReconciles: workerCount, + }). + For(&clusterv1alpha1.MachineDeployment{}, builder.WithPredicates(filterMachineDeploymentPredicate())). + Build(reconciler) - return nil + return err } func (r *Reconciler) Reconcile(ctx context.Context, req ctrlruntime.Request) (reconcile.Result, error) { diff --git a/pkg/controllers/osp/osp_controller.go b/pkg/controllers/osp/osp_controller.go index c9247f24..20e84305 100644 --- a/pkg/controllers/osp/osp_controller.go +++ b/pkg/controllers/osp/osp_controller.go @@ -19,7 +19,6 @@ package osp import ( "context" "fmt" - "io/fs" "path/filepath" "strings" @@ -28,13 +27,10 @@ import ( "k8c.io/operating-system-manager/deploy/osps" "k8c.io/operating-system-manager/pkg/crd/osm/v1alpha1" "k8c.io/operating-system-manager/pkg/resources/reconciling" - predicateutil "k8c.io/operating-system-manager/pkg/util/predicate" - appsv1 "k8s.io/api/apps/v1" - kerrors "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/types" yamlutil "k8s.io/apimachinery/pkg/util/yaml" ctrlruntime "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/builder" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller" "sigs.k8s.io/controller-runtime/pkg/event" @@ -51,132 +47,128 @@ const ( ospsDefaultDirName = "default" ) +type ospMap map[string]*v1alpha1.OperatingSystemProfile + type Reconciler struct { client.Client - log *zap.SugaredLogger - defaultOSPFiles map[string][]byte + log *zap.SugaredLogger + defaultOSPs ospMap namespace string } func Add(mgr manager.Manager, log *zap.SugaredLogger, namespace string, workerCount int) error { - ospDefaultDir, err := osps.FS.ReadDir(ospsDefaultDirName) + defaultOSPs, err := loadDefaultOSPs() if err != nil { - return fmt.Errorf("failed to read osps default directory: %w", err) - } - - var defaultOSPFiles = make(map[string][]byte, len(ospDefaultDir)) - for _, ospFile := range ospDefaultDir { - defaultOSPFile, err := fs.ReadFile(osps.FS, filepath.Join(ospsDefaultDirName, ospFile.Name())) - if err != nil { - return fmt.Errorf("failed to read osp file %s: %w", ospFile.Name(), err) - } - - defaultOSPFiles[ospFile.Name()] = defaultOSPFile + return fmt.Errorf("failed to load default OSPs: %w", err) } reconciler := &Reconciler{ - Client: mgr.GetClient(), - log: log, - defaultOSPFiles: defaultOSPFiles, - namespace: namespace, - } - - c, err := controller.New(ControllerName, mgr, controller.Options{Reconciler: reconciler, MaxConcurrentReconciles: workerCount}) - if err != nil { - return err + Client: mgr.GetClient(), + log: log, + defaultOSPs: defaultOSPs, + namespace: namespace, } - // Since the osp controller cares about only creating the default OSP resources, we need to watch for the creation - // of any random resource in the underlying namespace where osm is deployed. We picked deployments for this and added additional - // event filtering to avoid redundant reconciliation/requeues. - if err := c.Watch(source.Kind(mgr.GetCache(), &appsv1.Deployment{}), - &handler.EnqueueRequestForObject{}, - filterDeploymentPredicate(), - predicateutil.ByNamespace(namespace), - ); err != nil { - return fmt.Errorf("failed to create watch for deployments: %w", err) + // trigger controller once upon startup to bootstrap the default OSPs + sourceChannel := make(chan event.GenericEvent, 1) + sourceChannel <- event.GenericEvent{ + Object: &v1alpha1.OperatingSystemProfile{}, } - return nil + _, err = builder.ControllerManagedBy(mgr). + Named(ControllerName). + WithOptions(controller.Options{ + MaxConcurrentReconciles: workerCount, + }). + For(&v1alpha1.OperatingSystemProfile{}, builder.WithPredicates(predicate.NewPredicateFuncs(func(object client.Object) bool { + return object.GetNamespace() == namespace + }))). + WatchesRawSource(source.Channel(sourceChannel, &handler.EnqueueRequestForObject{})). + Build(reconciler) + + return err } -func (r *Reconciler) Reconcile(ctx context.Context, _ ctrlruntime.Request) (reconcile.Result, error) { - r.log.Info("Reconciling default OSP resource..") +func (r *Reconciler) Reconcile(ctx context.Context, req ctrlruntime.Request) (reconcile.Result, error) { + var toReconcile ospMap + if req.Name == "" { + r.log.Info("Reconciling default OSP resources...") + toReconcile = r.defaultOSPs + } else { + osp, ok := r.defaultOSPs[req.Name] + if !ok { + return reconcile.Result{}, nil + } + + r.log.Infow("Reconciling OSP resource...", "osp", req.Name) + toReconcile = ospMap{req.Name: osp} + } - if err := r.reconcile(ctx); err != nil { + if err := r.reconcileOSPs(ctx, toReconcile); err != nil { return reconcile.Result{}, err } + return reconcile.Result{}, nil } -func (r *Reconciler) reconcile(ctx context.Context) error { +func (r *Reconciler) reconcileOSPs(ctx context.Context, ospInstances ospMap) error { var ospReconcilers []reconciling.NamedOperatingSystemProfileReconcilerFactory - for name, ospFile := range r.defaultOSPFiles { - osp, err := parseYAMLToObject(ospFile) - if err != nil { - return fmt.Errorf("failed to parse osp %s: %w", name, err) - } + for name, osp := range ospInstances { + ospReconcilers = append(ospReconcilers, ospReconciler(name, osp)) + } - // Remove file extension .yaml from the OSP name - name = strings.ReplaceAll(name, ".yaml", "") + if err := reconciling.ReconcileOperatingSystemProfiles(ctx, ospReconcilers, r.namespace, r.Client); err != nil { + return fmt.Errorf("failed to reconcile OSPs: %w", err) + } - // Check if OSP already exists - existingOSP := &v1alpha1.OperatingSystemProfile{} - if err := r.Client.Get(ctx, types.NamespacedName{Name: name, Namespace: r.namespace}, existingOSP); err != nil && !kerrors.IsNotFound(err) { - return fmt.Errorf("failed to retrieve existing OperatingSystemProfile: %w", err) - } + return nil +} - // Since OSPs are immutable, we only want to reconcile resources when the version is different. - if osp.Spec.Version != existingOSP.Spec.Version { - // OSP already exists - osp.SetResourceVersion(existingOSP.GetResourceVersion()) - osp.SetGeneration(existingOSP.GetGeneration()) +func ospReconciler(name string, source *v1alpha1.OperatingSystemProfile) reconciling.NamedOperatingSystemProfileReconcilerFactory { + return func() (string, reconciling.OperatingSystemProfileReconciler) { + return name, func(osp *v1alpha1.OperatingSystemProfile) (*v1alpha1.OperatingSystemProfile, error) { + osp.Spec = source.Spec - ospReconcilers = append(ospReconcilers, ospReconciler(name, osp)) + return osp, nil } } +} - if err := reconciling.ReconcileOperatingSystemProfiles(ctx, - ospReconcilers, - r.namespace, r.Client); err != nil { - return fmt.Errorf("failed to reconcile osps: %w", err) +func loadDefaultOSPs() (ospMap, error) { + ospDefaultDir, err := osps.FS.ReadDir(ospsDefaultDirName) + if err != nil { + return nil, fmt.Errorf("failed to read OSPs default directory: %w", err) } - return nil -} + var defaultOSPs = make(ospMap, len(ospDefaultDir)) + for _, ospFile := range ospDefaultDir { + filename := ospFile.Name() -func ospReconciler(name string, osp *v1alpha1.OperatingSystemProfile) reconciling.NamedOperatingSystemProfileReconcilerFactory { - return func() (string, reconciling.OperatingSystemProfileReconciler) { - return name, func(*v1alpha1.OperatingSystemProfile) (*v1alpha1.OperatingSystemProfile, error) { - return osp, nil + osp, err := parseOSPFile(filename) + if err != nil { + return nil, fmt.Errorf("failed to read OSP %s: %w", filename, err) } + + // Remove file extension .yaml to get OSP name + ospName := strings.ReplaceAll(filename, ".yaml", "") + + defaultOSPs[ospName] = osp } + + return defaultOSPs, nil } -func parseYAMLToObject(ospByte []byte) (*v1alpha1.OperatingSystemProfile, error) { +func parseOSPFile(filename string) (*v1alpha1.OperatingSystemProfile, error) { + content, err := osps.FS.ReadFile(filepath.Join(ospsDefaultDirName, filename)) + if err != nil { + return nil, err + } + osp := &v1alpha1.OperatingSystemProfile{} - if err := yamlutil.Unmarshal(ospByte, osp); err != nil { + if err := yamlutil.Unmarshal(content, osp); err != nil { return nil, err } return osp, nil } - -// filterDeploymentPredicate filters out all deployment events except the creation one. -func filterDeploymentPredicate() predicate.Predicate { - return predicate.Funcs{ - CreateFunc: func(_ event.CreateEvent) bool { - return true - }, - DeleteFunc: func(_ event.DeleteEvent) bool { - return false - }, - UpdateFunc: func(_ event.UpdateEvent) bool { - return false - }, - GenericFunc: func(_ event.GenericEvent) bool { - return false - }, - } -}