diff --git a/deploy/rbac-controller.yaml b/deploy/rbac-controller.yaml index 73d2436d..f7a65781 100644 --- a/deploy/rbac-controller.yaml +++ b/deploy/rbac-controller.yaml @@ -184,14 +184,6 @@ rules: - list - get - watch - - apiGroups: - - apps - resources: - - deployments - verbs: - - get - - list - - watch --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding diff --git a/go.mod b/go.mod index 6d134ff8..c365b750 100644 --- a/go.mod +++ b/go.mod @@ -18,15 +18,13 @@ require ( go.uber.org/zap v1.27.0 gopkg.in/yaml.v3 v3.0.1 k8c.io/reconciler v0.5.0 - k8s.io/api v0.30.0 - k8s.io/apimachinery v0.30.0 - k8s.io/client-go v0.30.0 - k8s.io/code-generator v0.30.0 + k8s.io/api v0.30.1 + k8s.io/apimachinery v0.30.1 + k8s.io/client-go v0.30.1 + k8s.io/code-generator v0.30.1 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.3 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.1 // indirect + k8s.io/component-base v0.30.1 // 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..f8d07056 100644 --- a/go.sum +++ b/go.sum @@ -502,18 +502,18 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 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/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/api v0.30.1 h1:kCm/6mADMdbAxmIh0LBjS54nQBE+U4KmbCfIkF5CpJY= +k8s.io/api v0.30.1/go.mod h1:ddbN2C0+0DIiPntan/bye3SW3PdwLa11/0yqwvuRrJM= +k8s.io/apiextensions-apiserver v0.30.1 h1:4fAJZ9985BmpJG6PkoxVRpXv9vmPUOVzl614xarePws= +k8s.io/apiextensions-apiserver v0.30.1/go.mod h1:R4GuSrlhgq43oRY9sF2IToFh7PVlF1JjfWdoG3pixk4= +k8s.io/apimachinery v0.30.1 h1:ZQStsEfo4n65yAdlGTfP/uSHMQSoYzU/oeEbkmF7P2U= +k8s.io/apimachinery v0.30.1/go.mod h1:iexa2somDaxdnj7bha06bhb43Zpa6eWH8N8dbqVjTUc= +k8s.io/client-go v0.30.1 h1:uC/Ir6A3R46wdkgCV3vbLyNOYyCJ8oZnjtJGKfytl/Q= +k8s.io/client-go v0.30.1/go.mod h1:wrAqLNs2trwiCH/wxxmT/x3hKVH9PuV0GGW0oDoHVqc= +k8s.io/code-generator v0.30.1 h1:ZsG++q5Vt0ScmKCeLhynUuWgcwFGg1Hl1AGfatqPJBI= +k8s.io/code-generator v0.30.1/go.mod h1:hFgxRsvOUg79mbpbVKfjJvRhVz1qLoe40yZDJ/hwRH4= +k8s.io/component-base v0.30.1 h1:bvAtlPh1UrdaZL20D9+sWxsJljMi0QZ3Lmw+kmZAaxQ= +k8s.io/component-base v0.30.1/go.mod h1:e/X9kDiOebwlI41AvBHuWdqFriSRrX50CdwA9TFaHLI= 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.3 h1:B5Wmmo8WMWK7izei+2LlXLVDGzMwAHBNLX68lwtlSR4= +sigs.k8s.io/controller-runtime v0.18.3/go.mod h1:TVoGrfdpbA9VRFaRnKgk9P5/atA0pMwq+f+msb9M8Sg= 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..97013f51 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,21 +120,21 @@ 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) { log := r.log.With("request", req) - log.Info("Reconciling OSC resource..") + log.Debug("Reconciling OSC resource...") machineDeployment := &clusterv1alpha1.MachineDeployment{} if err := r.workerClient.Get(ctx, req.NamespacedName, machineDeployment); err != nil { diff --git a/pkg/controllers/osp/osp_controller.go b/pkg/controllers/osp/osp_controller.go index c9247f24..8c900358 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,11 @@ 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" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 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 +48,130 @@ 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) + return fmt.Errorf("failed to load default OSPs: %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) + reconciler := &Reconciler{ + Client: mgr.GetClient(), + log: log, + defaultOSPs: defaultOSPs, + namespace: namespace, + } + + // trigger controller once upon startup to bootstrap the default OSPs + bootstrapping := make(chan event.GenericEvent, len(defaultOSPs)) + for name := range defaultOSPs { + bootstrapping <- event.GenericEvent{ + Object: &v1alpha1.OperatingSystemProfile{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + }, } + } - defaultOSPFiles[ospFile.Name()] = defaultOSPFile + _, 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(bootstrapping, &handler.EnqueueRequestForObject{})). + Build(reconciler) + + return err +} + +func (r *Reconciler) Reconcile(ctx context.Context, req ctrlruntime.Request) (reconcile.Result, error) { + if err := r.reconcileOSP(ctx, req.Name); err != nil { + return reconcile.Result{}, err } - reconciler := &Reconciler{ - Client: mgr.GetClient(), - log: log, - defaultOSPFiles: defaultOSPFiles, - namespace: namespace, + return reconcile.Result{}, nil +} + +func (r *Reconciler) reconcileOSP(ctx context.Context, name string) error { + osp, ok := r.defaultOSPs[name] + if !ok { + return nil } - c, err := controller.New(ControllerName, mgr, controller.Options{Reconciler: reconciler, MaxConcurrentReconciles: workerCount}) - if err != nil { - return err + r.log.Debugw("Reconciling OSP resource...", "osp", name) + + ospReconcilers := []reconciling.NamedOperatingSystemProfileReconcilerFactory{ + ospReconciler(name, osp), } - // 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) + if err := reconciling.ReconcileOperatingSystemProfiles(ctx, ospReconcilers, r.namespace, r.Client); err != nil { + return fmt.Errorf("failed to reconcile OSP: %w", err) } return nil } -func (r *Reconciler) Reconcile(ctx context.Context, _ ctrlruntime.Request) (reconcile.Result, error) { - r.log.Info("Reconciling default OSP resource..") +func ospReconciler(name string, source *v1alpha1.OperatingSystemProfile) reconciling.NamedOperatingSystemProfileReconcilerFactory { + return func() (string, reconciling.OperatingSystemProfileReconciler) { + return name, func(osp *v1alpha1.OperatingSystemProfile) (*v1alpha1.OperatingSystemProfile, error) { + // only attempt an update if our OSP is newer + if osp.Spec.Version != source.Spec.Version { + osp.Spec = source.Spec + } - if err := r.reconcile(ctx); err != nil { - return reconcile.Result{}, err + return osp, nil + } } - return reconcile.Result{}, nil } -func (r *Reconciler) reconcile(ctx context.Context) 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) - } +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) + } - // Remove file extension .yaml from the OSP name - name = strings.ReplaceAll(name, ".yaml", "") + var defaultOSPs = make(ospMap, len(ospDefaultDir)) + for _, ospFile := range ospDefaultDir { + filename := ospFile.Name() - // 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) + osp, err := parseOSPFile(filename) + if err != nil { + return nil, fmt.Errorf("failed to read OSP %s: %w", filename, err) } - // 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()) - - ospReconcilers = append(ospReconcilers, ospReconciler(name, osp)) - } - } + // Remove file extension .yaml to get OSP name + ospName := strings.ReplaceAll(filename, ".yaml", "") - if err := reconciling.ReconcileOperatingSystemProfiles(ctx, - ospReconcilers, - r.namespace, r.Client); err != nil { - return fmt.Errorf("failed to reconcile osps: %w", err) + defaultOSPs[ospName] = osp } - return nil + return defaultOSPs, nil } -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 - } +func parseOSPFile(filename string) (*v1alpha1.OperatingSystemProfile, error) { + content, err := osps.FS.ReadFile(filepath.Join(ospsDefaultDirName, filename)) + if err != nil { + return nil, err } -} -func parseYAMLToObject(ospByte []byte) (*v1alpha1.OperatingSystemProfile, error) { 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 - }, - } -}