diff --git a/api/v1alpha1/flowcollector_types.go b/api/v1alpha1/flowcollector_types.go index 9d74ed366..de2291ae5 100644 --- a/api/v1alpha1/flowcollector_types.go +++ b/api/v1alpha1/flowcollector_types.go @@ -732,7 +732,7 @@ type FlowCollectorStatus struct { // +kubebuilder:printcolumn:name="Agent",type="string",JSONPath=`.spec.agent.type` // +kubebuilder:printcolumn:name="Sampling (EBPF)",type="string",JSONPath=`.spec.agent.ebpf.sampling` // +kubebuilder:printcolumn:name="Deployment Model",type="string",JSONPath=`.spec.deploymentModel` -// +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.conditions[*].reason" +// +kubebuilder:printcolumn:name="Status",type="string",JSONPath=`.status.conditions[?(@.type=="Ready")].reason` // FlowCollector is the Schema for the flowcollectors API, which pilots and configures netflow collection. // diff --git a/api/v1beta1/flowcollector_types.go b/api/v1beta1/flowcollector_types.go index ce84beb5a..71ca91e09 100644 --- a/api/v1beta1/flowcollector_types.go +++ b/api/v1beta1/flowcollector_types.go @@ -863,7 +863,7 @@ type FlowCollectorStatus struct { // +kubebuilder:printcolumn:name="Agent",type="string",JSONPath=`.spec.agent.type` // +kubebuilder:printcolumn:name="Sampling (EBPF)",type="string",JSONPath=`.spec.agent.ebpf.sampling` // +kubebuilder:printcolumn:name="Deployment Model",type="string",JSONPath=`.spec.deploymentModel` -// +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.conditions[*].reason" +// +kubebuilder:printcolumn:name="Status",type="string",JSONPath=`.status.conditions[?(@.type=="Ready")].reason` // +kubebuilder:storageversion // `FlowCollector` is the schema for the network flows collection API, which pilots and configures the underlying deployments. type FlowCollector struct { diff --git a/api/v1beta2/flowcollector_types.go b/api/v1beta2/flowcollector_types.go index 18e6c837c..9674cedc0 100644 --- a/api/v1beta2/flowcollector_types.go +++ b/api/v1beta2/flowcollector_types.go @@ -936,6 +936,7 @@ type FlowCollectorStatus struct { Conditions []metav1.Condition `json:"conditions"` // Namespace where console plugin and flowlogs-pipeline have been deployed. + // Deprecated: annotations are used instead Namespace string `json:"namespace,omitempty"` } @@ -945,7 +946,7 @@ type FlowCollectorStatus struct { // +kubebuilder:printcolumn:name="Agent",type="string",JSONPath=`.spec.agent.type` // +kubebuilder:printcolumn:name="Sampling (EBPF)",type="string",JSONPath=`.spec.agent.ebpf.sampling` // +kubebuilder:printcolumn:name="Deployment Model",type="string",JSONPath=`.spec.deploymentModel` -// +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.conditions[*].reason" +// +kubebuilder:printcolumn:name="Status",type="string",JSONPath=`.status.conditions[?(@.type=="Ready")].reason` // `FlowCollector` is the schema for the network flows collection API, which pilots and configures the underlying deployments. type FlowCollector struct { metav1.TypeMeta `json:",inline"` diff --git a/bundle/manifests/flows.netobserv.io_flowcollectors.yaml b/bundle/manifests/flows.netobserv.io_flowcollectors.yaml index 83daaba38..fc6817574 100644 --- a/bundle/manifests/flows.netobserv.io_flowcollectors.yaml +++ b/bundle/manifests/flows.netobserv.io_flowcollectors.yaml @@ -36,7 +36,7 @@ spec: - jsonPath: .spec.deploymentModel name: Deployment Model type: string - - jsonPath: .status.conditions[*].reason + - jsonPath: .status.conditions[?(@.type=="Ready")].reason name: Status type: string deprecated: true @@ -2460,7 +2460,7 @@ spec: - jsonPath: .spec.deploymentModel name: Deployment Model type: string - - jsonPath: .status.conditions[*].reason + - jsonPath: .status.conditions[?(@.type=="Ready")].reason name: Status type: string name: v1beta1 @@ -5145,7 +5145,7 @@ spec: - jsonPath: .spec.deploymentModel name: Deployment Model type: string - - jsonPath: .status.conditions[*].reason + - jsonPath: .status.conditions[?(@.type=="Ready")].reason name: Status type: string name: v1beta2 @@ -8033,8 +8033,8 @@ spec: type: object type: array namespace: - description: Namespace where console plugin and flowlogs-pipeline - have been deployed. + description: 'Namespace where console plugin and flowlogs-pipeline + have been deployed. Deprecated: annotations are used instead' type: string required: - conditions diff --git a/config/crd/bases/flows.netobserv.io_flowcollectors.yaml b/config/crd/bases/flows.netobserv.io_flowcollectors.yaml index 3d5193d05..d0c4c7db1 100644 --- a/config/crd/bases/flows.netobserv.io_flowcollectors.yaml +++ b/config/crd/bases/flows.netobserv.io_flowcollectors.yaml @@ -26,7 +26,7 @@ spec: - jsonPath: .spec.deploymentModel name: Deployment Model type: string - - jsonPath: .status.conditions[*].reason + - jsonPath: .status.conditions[?(@.type=="Ready")].reason name: Status type: string deprecated: true @@ -2446,7 +2446,7 @@ spec: - jsonPath: .spec.deploymentModel name: Deployment Model type: string - - jsonPath: .status.conditions[*].reason + - jsonPath: .status.conditions[?(@.type=="Ready")].reason name: Status type: string name: v1beta1 @@ -5131,7 +5131,7 @@ spec: - jsonPath: .spec.deploymentModel name: Deployment Model type: string - - jsonPath: .status.conditions[*].reason + - jsonPath: .status.conditions[?(@.type=="Ready")].reason name: Status type: string name: v1beta2 @@ -8019,8 +8019,8 @@ spec: type: object type: array namespace: - description: Namespace where console plugin and flowlogs-pipeline - have been deployed. + description: 'Namespace where console plugin and flowlogs-pipeline + have been deployed. Deprecated: annotations are used instead' type: string required: - conditions diff --git a/controllers/consoleplugin/consoleplugin_reconciler.go b/controllers/consoleplugin/consoleplugin_reconciler.go index b2c3aa13e..2b9491cda 100644 --- a/controllers/consoleplugin/consoleplugin_reconciler.go +++ b/controllers/consoleplugin/consoleplugin_reconciler.go @@ -26,10 +26,6 @@ type pluginSpec = flowslatest.FlowCollectorConsolePlugin // CPReconciler reconciles the current console plugin state with the desired configuration type CPReconciler struct { *reconcilers.Instance - owned ownedObjects -} - -type ownedObjects struct { deployment *appsv1.Deployment service *corev1.Service metricsService *corev1.Service @@ -39,28 +35,20 @@ type ownedObjects struct { serviceMonitor *monitoringv1.ServiceMonitor } -func NewReconciler(common *reconcilers.Common, imageName string) CPReconciler { - owned := ownedObjects{ - deployment: &appsv1.Deployment{}, - service: &corev1.Service{}, - metricsService: &corev1.Service{}, - hpa: &ascv2.HorizontalPodAutoscaler{}, - serviceAccount: &corev1.ServiceAccount{}, - configMap: &corev1.ConfigMap{}, - serviceMonitor: &monitoringv1.ServiceMonitor{}, +func NewReconciler(cmn *reconcilers.Instance) CPReconciler { + rec := CPReconciler{ + Instance: cmn, + deployment: cmn.Managed.NewDeployment(constants.PluginName), + service: cmn.Managed.NewService(constants.PluginName), + metricsService: cmn.Managed.NewService(metricsSvcName), + hpa: cmn.Managed.NewHPA(constants.PluginName), + serviceAccount: cmn.Managed.NewServiceAccount(constants.PluginName), + configMap: cmn.Managed.NewConfigMap(configMapName), } - cmnInstance := common.NewInstance(imageName) - cmnInstance.Managed.AddManagedObject(constants.PluginName, owned.deployment) - cmnInstance.Managed.AddManagedObject(constants.PluginName, owned.service) - cmnInstance.Managed.AddManagedObject(metricsSvcName, owned.metricsService) - cmnInstance.Managed.AddManagedObject(constants.PluginName, owned.hpa) - cmnInstance.Managed.AddManagedObject(constants.PluginName, owned.serviceAccount) - cmnInstance.Managed.AddManagedObject(configMapName, owned.configMap) - if common.AvailableAPIs.HasSvcMonitor() { - cmnInstance.Managed.AddManagedObject(constants.PluginName, owned.serviceMonitor) + if cmn.AvailableAPIs.HasSvcMonitor() { + rec.serviceMonitor = cmn.Managed.NewServiceMonitor(constants.PluginName) } - - return CPReconciler{Instance: cmnInstance, owned: owned} + return rec } // CleanupNamespace cleans up old namespace @@ -70,6 +58,9 @@ func (r *CPReconciler) CleanupNamespace(ctx context.Context) { // Reconcile is the reconciler entry point to reconcile the current plugin state with the desired configuration func (r *CPReconciler) Reconcile(ctx context.Context, desired *flowslatest.FlowCollector) error { + l := log.FromContext(ctx).WithName("console-plugin") + ctx = log.IntoContext(ctx, l) + ns := r.Managed.Namespace // Retrieve current owned objects err := r.Managed.FetchAll(ctx) @@ -148,7 +139,7 @@ func (r *CPReconciler) checkAutoPatch(ctx context.Context, desired *flowslatest. } func (r *CPReconciler) reconcilePermissions(ctx context.Context, builder *builder) error { - if !r.Managed.Exists(r.owned.serviceAccount) { + if !r.Managed.Exists(r.serviceAccount) { return r.CreateOwned(ctx, builder.serviceAccount()) } // update not needed for now @@ -193,12 +184,12 @@ func (r *CPReconciler) reconcileConfigMap(ctx context.Context, builder *builder) if err != nil { return "", err } - if !r.Managed.Exists(r.owned.configMap) { + if !r.Managed.Exists(r.configMap) { if err := r.CreateOwned(ctx, newCM); err != nil { return "", err } - } else if !reflect.DeepEqual(newCM.Data, r.owned.configMap.Data) { - if err := r.UpdateIfOwned(ctx, r.owned.configMap, newCM); err != nil { + } else if !reflect.DeepEqual(newCM.Data, r.configMap.Data) { + if err := r.UpdateIfOwned(ctx, r.configMap, newCM); err != nil { return "", err } } @@ -209,34 +200,31 @@ func (r *CPReconciler) reconcileDeployment(ctx context.Context, builder *builder report := helper.NewChangeReport("Console deployment") defer report.LogIfNeeded(ctx) - newDepl := builder.deployment(cmDigest) - if !r.Managed.Exists(r.owned.deployment) { - if err := r.CreateOwned(ctx, newDepl); err != nil { - return err - } - } else if helper.DeploymentChanged(r.owned.deployment, newDepl, constants.PluginName, helper.HPADisabled(&desired.ConsolePlugin.Autoscaler), helper.PtrInt32(desired.ConsolePlugin.Replicas), &report) { - if err := r.UpdateIfOwned(ctx, r.owned.deployment, newDepl); err != nil { - return err - } - } else { - r.CheckDeploymentInProgress(r.owned.deployment) - } - return nil + return reconcilers.ReconcileDeployment( + ctx, + r.Instance, + r.deployment, + builder.deployment(cmDigest), + constants.PluginName, + helper.PtrInt32(desired.ConsolePlugin.Replicas), + &desired.ConsolePlugin.Autoscaler, + &report, + ) } func (r *CPReconciler) reconcileServices(ctx context.Context, builder *builder) error { report := helper.NewChangeReport("Console services") defer report.LogIfNeeded(ctx) - if err := r.ReconcileService(ctx, r.owned.service, builder.mainService(), &report); err != nil { + if err := r.ReconcileService(ctx, r.service, builder.mainService(), &report); err != nil { return err } - if err := r.ReconcileService(ctx, r.owned.metricsService, builder.metricsService(), &report); err != nil { + if err := r.ReconcileService(ctx, r.metricsService, builder.metricsService(), &report); err != nil { return err } if r.AvailableAPIs.HasSvcMonitor() { serviceMonitor := builder.serviceMonitor() - if err := reconcilers.GenericReconcile(ctx, r.Managed, &r.Client, r.owned.serviceMonitor, serviceMonitor, &report, helper.ServiceMonitorChanged); err != nil { + if err := reconcilers.GenericReconcile(ctx, r.Managed, &r.Client, r.serviceMonitor, serviceMonitor, &report, helper.ServiceMonitorChanged); err != nil { return err } } @@ -247,22 +235,14 @@ func (r *CPReconciler) reconcileHPA(ctx context.Context, builder *builder, desir report := helper.NewChangeReport("Console autoscaler") defer report.LogIfNeeded(ctx) - // Delete or Create / Update Autoscaler according to HPA option - if helper.HPADisabled(&desired.ConsolePlugin.Autoscaler) { - r.Managed.TryDelete(ctx, r.owned.hpa) - } else { - newASC := builder.autoScaler() - if !r.Managed.Exists(r.owned.hpa) { - if err := r.CreateOwned(ctx, newASC); err != nil { - return err - } - } else if helper.AutoScalerChanged(r.owned.hpa, desired.ConsolePlugin.Autoscaler, &report) { - if err := r.UpdateIfOwned(ctx, r.owned.hpa, newASC); err != nil { - return err - } - } - } - return nil + return reconcilers.ReconcileHPA( + ctx, + r.Instance, + r.hpa, + builder.autoScaler(), + &desired.ConsolePlugin.Autoscaler, + &report, + ) } func pluginNeedsUpdate(plg *osv1alpha1.ConsolePlugin, desired *pluginSpec) bool { diff --git a/controllers/controllers.go b/controllers/controllers.go new file mode 100644 index 000000000..03ab16c36 --- /dev/null +++ b/controllers/controllers.go @@ -0,0 +1,9 @@ +package controllers + +import ( + "github.com/netobserv/network-observability-operator/controllers/flp" + "github.com/netobserv/network-observability-operator/controllers/monitoring" + "github.com/netobserv/network-observability-operator/pkg/manager" +) + +var Registerers = []manager.Registerer{Start, flp.Start, monitoring.Start} diff --git a/controllers/ebpf/agent_controller.go b/controllers/ebpf/agent_controller.go index 2fbb72e14..563c4dde7 100644 --- a/controllers/ebpf/agent_controller.go +++ b/controllers/ebpf/agent_controller.go @@ -9,7 +9,6 @@ import ( flowslatest "github.com/netobserv/network-observability-operator/api/v1beta2" "github.com/netobserv/network-observability-operator/controllers/constants" "github.com/netobserv/network-observability-operator/controllers/ebpf/internal/permissions" - "github.com/netobserv/network-observability-operator/controllers/operator" "github.com/netobserv/network-observability-operator/controllers/reconcilers" "github.com/netobserv/network-observability-operator/pkg/helper" "github.com/netobserv/network-observability-operator/pkg/volumes" @@ -83,22 +82,19 @@ const ( // associated objects that are required to bind the proper permissions: namespace, service // accounts, SecurityContextConstraints... type AgentController struct { - reconcilers.Common + *reconcilers.Instance permissions permissions.Reconciler - config *operator.Config volumes volumes.Builder } -func NewAgentController(common *reconcilers.Common, config *operator.Config) *AgentController { +func NewAgentController(common *reconcilers.Instance) *AgentController { return &AgentController{ - Common: *common, + Instance: common, permissions: permissions.NewReconciler(common), - config: config, } } -func (c *AgentController) Reconcile( - ctx context.Context, target *flowslatest.FlowCollector) error { +func (c *AgentController) Reconcile(ctx context.Context, target *flowslatest.FlowCollector) error { rlog := log.FromContext(ctx).WithName("ebpf") ctx = log.IntoContext(ctx, rlog) current, err := c.current(ctx) @@ -136,13 +132,14 @@ func (c *AgentController) Reconcile( switch requiredAction(current, desired) { case actionCreate: rlog.Info("action: create agent") + c.Status.SetCreatingDaemonSet(desired) return c.CreateOwned(ctx, desired) case actionUpdate: rlog.Info("action: update agent") return c.UpdateIfOwned(ctx, current, desired) default: rlog.Info("action: nothing to do") - c.CheckDaemonSetInProgress(current) + c.Status.CheckDaemonSetProgress(current) return nil } } @@ -178,7 +175,7 @@ func (c *AgentController) desired(ctx context.Context, coll *flowslatest.FlowCol if coll == nil || !helper.UseEBPF(&coll.Spec) { return nil, nil } - version := helper.ExtractVersion(c.config.EBPFAgentImage) + version := helper.ExtractVersion(c.Image) annotations := make(map[string]string) env, err := c.envConfig(ctx, coll, annotations) if err != nil { @@ -256,7 +253,7 @@ func (c *AgentController) desired(ctx context.Context, coll *flowslatest.FlowCol Volumes: volumes, Containers: []corev1.Container{{ Name: constants.EBPFAgentName, - Image: c.config.EBPFAgentImage, + Image: c.Image, ImagePullPolicy: corev1.PullPolicy(coll.Spec.Agent.EBPF.ImagePullPolicy), Resources: coll.Spec.Agent.EBPF.Resources, SecurityContext: c.securityContext(coll), diff --git a/controllers/ebpf/internal/permissions/permissions.go b/controllers/ebpf/internal/permissions/permissions.go index 4b41a8c31..c02152c47 100644 --- a/controllers/ebpf/internal/permissions/permissions.go +++ b/controllers/ebpf/internal/permissions/permissions.go @@ -26,11 +26,11 @@ var AllowedCapabilities = []v1.Capability{"BPF", "PERFMON", "NET_ADMIN", "SYS_RE // - Create netobserv-ebpf-agent service account in the privileged namespace // - For Openshift, apply the required SecurityContextConstraints for privileged Pod operation type Reconciler struct { - reconcilers.Common + *reconcilers.Instance } -func NewReconciler(cmn *reconcilers.Common) Reconciler { - return Reconciler{Common: *cmn} +func NewReconciler(cmn *reconcilers.Instance) Reconciler { + return Reconciler{Instance: cmn} } func (c *Reconciler) Reconcile(ctx context.Context, desired *flowslatest.FlowCollectorEBPF) error { diff --git a/controllers/flowcollector_controller.go b/controllers/flowcollector_controller.go index b3c523e0e..53eb34aa3 100644 --- a/controllers/flowcollector_controller.go +++ b/controllers/flowcollector_controller.go @@ -3,38 +3,26 @@ package controllers import ( "context" "fmt" - "net" - configv1 "github.com/openshift/api/config/v1" osv1alpha1 "github.com/openshift/api/console/v1alpha1" securityv1 "github.com/openshift/api/security/v1" appsv1 "k8s.io/api/apps/v1" ascv2 "k8s.io/api/autoscaling/v2" corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/types" - "k8s.io/client-go/discovery" ctrl "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/controllerutil" "sigs.k8s.io/controller-runtime/pkg/log" flowslatest "github.com/netobserv/network-observability-operator/api/v1beta2" "github.com/netobserv/network-observability-operator/controllers/consoleplugin" - "github.com/netobserv/network-observability-operator/controllers/constants" "github.com/netobserv/network-observability-operator/controllers/ebpf" - "github.com/netobserv/network-observability-operator/controllers/flowlogspipeline" - "github.com/netobserv/network-observability-operator/controllers/globals" - "github.com/netobserv/network-observability-operator/controllers/operator" "github.com/netobserv/network-observability-operator/controllers/ovs" "github.com/netobserv/network-observability-operator/controllers/reconcilers" "github.com/netobserv/network-observability-operator/pkg/cleanup" - "github.com/netobserv/network-observability-operator/pkg/conditions" - "github.com/netobserv/network-observability-operator/pkg/discover" "github.com/netobserv/network-observability-operator/pkg/helper" + "github.com/netobserv/network-observability-operator/pkg/manager" + "github.com/netobserv/network-observability-operator/pkg/manager/status" "github.com/netobserv/network-observability-operator/pkg/watchers" ) @@ -46,39 +34,50 @@ const ( // FlowCollectorReconciler reconciles a FlowCollector object type FlowCollectorReconciler struct { client.Client - permissions discover.Permissions - availableAPIs *discover.AvailableAPIs - Scheme *runtime.Scheme - config *operator.Config - watcher *watchers.Watcher - lookupIP func(string) ([]net.IP, error) + mgr *manager.Manager + status status.Instance + watcher *watchers.Watcher } -func NewFlowCollectorReconciler(client client.Client, scheme *runtime.Scheme, config *operator.Config) *FlowCollectorReconciler { - return &FlowCollectorReconciler{ - Client: client, - Scheme: scheme, - lookupIP: net.LookupIP, - config: config, +func Start(ctx context.Context, mgr *manager.Manager) error { + log := log.FromContext(ctx) + log.Info("Starting FlowCollector controller") + r := FlowCollectorReconciler{ + Client: mgr.Client, + mgr: mgr, + status: mgr.Status.ForComponent(status.FlowCollectorLegacy), + } + + builder := ctrl.NewControllerManagedBy(mgr.Manager). + Named("legacy"). + For(&flowslatest.FlowCollector{}, reconcilers.IgnoreStatusChange). + Owns(&appsv1.Deployment{}). + Owns(&appsv1.DaemonSet{}). + Owns(&ascv2.HorizontalPodAutoscaler{}). + Owns(&corev1.Namespace{}). + Owns(&corev1.Service{}). + Owns(&corev1.ServiceAccount{}) + + if mgr.IsOpenShift() { + builder.Owns(&securityv1.SecurityContextConstraints{}) + } + if mgr.HasConsolePlugin() { + builder.Owns(&osv1alpha1.ConsolePlugin{}) + } else { + log.Info("Console not detected: the console plugin is not available") + } + if !mgr.HasCNO() { + log.Info("CNO not detected: using ovnKubernetes config and reconciler") } -} -//+kubebuilder:rbac:groups=apps,resources=deployments;daemonsets,verbs=get;list;watch;create;update;patch;delete -//+kubebuilder:rbac:groups=core,resources=namespaces;services;serviceaccounts;configmaps;secrets,verbs=get;list;watch;create;update;patch;delete -//+kubebuilder:rbac:groups=core,resources=endpoints,verbs=get;list;watch -//+kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=clusterrolebindings;clusterroles;rolebindings;roles,verbs=get;list;create;delete;update;watch -//+kubebuilder:rbac:groups=console.openshift.io,resources=consoleplugins,verbs=get;create;delete;update;patch;list;watch -//+kubebuilder:rbac:groups=operator.openshift.io,resources=consoles,verbs=get;update;list;update;watch -//+kubebuilder:rbac:groups=flows.netobserv.io,resources=flowcollectors,verbs=get;list;watch;create;update;patch;delete -//+kubebuilder:rbac:groups=flows.netobserv.io,resources=flowcollectors/status,verbs=get;update;patch -//+kubebuilder:rbac:groups=flows.netobserv.io,resources=flowcollectors/finalizers,verbs=update -//+kubebuilder:rbac:groups=security.openshift.io,resources=securitycontextconstraints,resourceNames=hostnetwork,verbs=use -//+kubebuilder:rbac:groups=security.openshift.io,resources=securitycontextconstraints,verbs=list;create;update;watch -//+kubebuilder:rbac:groups=apiregistration.k8s.io,resources=apiservices,verbs=list;get;watch -//+kubebuilder:rbac:groups=monitoring.coreos.com,resources=servicemonitors;prometheusrules,verbs=get;create;delete;update;patch;list;watch -//+kubebuilder:rbac:groups=config.openshift.io,resources=clusterversions,verbs=get;list;watch -//+kubebuilder:rbac:groups=loki.grafana.com,resources=network,resourceNames=logs,verbs=get;create -//+kubebuilder:rbac:urls="/metrics",verbs=get + ctrl, err := builder.Build(&r) + if err != nil { + return err + } + r.watcher = watchers.NewWatcher(ctrl) + + return nil +} // Reconcile is part of the main kubernetes reconciliation loop which aims to // move the current state of the cluster closer to the desired state. @@ -90,271 +89,105 @@ func NewFlowCollectorReconciler(client client.Client, scheme *runtime.Scheme, co // For more details, check Reconcile and its Result here: // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.9.2/pkg/reconcile func (r *FlowCollectorReconciler) Reconcile(ctx context.Context, _ ctrl.Request) (ctrl.Result, error) { - log := log.FromContext(ctx) - desired, err := r.getFlowCollector(ctx) + l := log.Log.WithName("legacy") // clear context (too noisy) + ctx = log.IntoContext(ctx, l) + // At the moment, status workflow is to start as ready then degrade if necessary + // Later (when legacy controller is broken down into individual controllers), status should start as unknown and only on success finishes as ready + r.status.SetReady() + defer r.status.Commit(ctx, r.Client) + + err := r.reconcile(ctx) if err != nil { - log.Error(err, "Failed to get FlowCollector") - return ctrl.Result{}, err - } else if desired == nil { - return ctrl.Result{}, nil - } - - ns := getNamespaceName(desired) - if err := cleanup.CleanPastReferences(ctx, r.Client, ns); err != nil { + l.Error(err, "FlowCollector reconcile failure") + // Set status failure unless it was already set + if !r.status.HasFailure() { + r.status.SetFailure("FlowCollectorGenericError", err.Error()) + } return ctrl.Result{}, err } - r.watcher.Reset(ns) - var didChange, isInProgress bool - previousNamespace := desired.Status.Namespace + return ctrl.Result{}, nil +} - // obtain default cluster ID - api is specific to openshift - if r.permissions.Vendor(ctx) == discover.VendorOpenShift && globals.DefaultClusterID == "" { - cversion := &configv1.ClusterVersion{} - key := client.ObjectKey{Name: "version"} - if err := r.Client.Get(ctx, key, cversion); err != nil { - log.Error(err, "unable to obtain cluster ID") - } else { - globals.DefaultClusterID = cversion.Spec.ClusterID - } +func (r *FlowCollectorReconciler) reconcile(ctx context.Context) error { + clh, desired, err := helper.NewFlowCollectorClientHelper(ctx, r.Client) + if err != nil { + return fmt.Errorf("failed to get FlowCollector: %w", err) + } else if desired == nil { + return nil } + ns := helper.GetNamespace(&desired.Spec) + previousNamespace := r.status.GetDeployedNamespace(desired) loki := helper.NewLokiConfig(&desired.Spec.Loki, ns) - reconcilersInfo := r.newCommonInfo(ctx, desired, ns, previousNamespace, &loki, func(b bool) { didChange = b }, func(b bool) { isInProgress = b }) + reconcilersInfo := r.newCommonInfo(clh, ns, previousNamespace, &loki) - err = r.reconcileOperator(ctx, &reconcilersInfo, desired) - if err != nil { - return ctrl.Result{}, err + if ret, err := r.checkFinalizer(ctx, desired, &reconcilersInfo); ret { + return err } + if err := cleanup.CleanPastReferences(ctx, r.Client, ns); err != nil { + return err + } + r.watcher.Reset(ns) + // Create reconcilers - flpReconciler := flowlogspipeline.NewReconciler(&reconcilersInfo, r.config.FlowlogsPipelineImage) var cpReconciler consoleplugin.CPReconciler - if r.availableAPIs.HasConsolePlugin() { - cpReconciler = consoleplugin.NewReconciler(&reconcilersInfo, r.config.ConsolePluginImage) + if r.mgr.HasConsolePlugin() { + cpReconciler = consoleplugin.NewReconciler(reconcilersInfo.NewInstance(r.mgr.Config.ConsolePluginImage, r.status)) } // Check namespace changed if ns != previousNamespace { - if err := r.handleNamespaceChanged(ctx, previousNamespace, ns, desired, &flpReconciler, &cpReconciler); err != nil { - return ctrl.Result{}, r.failure(ctx, conditions.CannotCreateNamespace(err), desired) + if previousNamespace != "" && r.mgr.HasConsolePlugin() { + // Namespace updated, clean up previous namespace + log.FromContext(ctx). + Info("FlowCollector namespace change detected: cleaning up previous namespace", "old", previousNamespace, "new", ns) + cpReconciler.CleanupNamespace(ctx) } - } - // Flowlogs-pipeline - if err := flpReconciler.Reconcile(ctx, desired); err != nil { - return ctrl.Result{}, r.failure(ctx, conditions.ReconcileFLPFailed(err), desired) + // Update namespace in status + if err := r.status.SetDeployedNamespace(ctx, r.Client, ns); err != nil { + return r.status.Error("ChangeNamespaceError", err) + } } // OVS config map for CNO - if r.availableAPIs.HasCNO() { + if r.mgr.HasCNO() { ovsConfigController := ovs.NewFlowsConfigCNOController(&reconcilersInfo, desired.Spec.Agent.IPFIX.ClusterNetworkOperator.Namespace, ovsFlowsConfigMapName) if err := ovsConfigController.Reconcile(ctx, desired); err != nil { - return ctrl.Result{}, r.failure(ctx, conditions.ReconcileCNOFailed(err), desired) + return r.status.Error("ReconcileCNOFailed", err) } } else { ovsConfigController := ovs.NewFlowsConfigOVNKController(&reconcilersInfo, desired.Spec.Agent.IPFIX.OVNKubernetes) if err := ovsConfigController.Reconcile(ctx, desired); err != nil { - return ctrl.Result{}, r.failure(ctx, conditions.ReconcileOVNKFailed(err), desired) + return r.status.Error("ReconcileOVNKFailed", err) } } // eBPF agent - ebpfAgentController := ebpf.NewAgentController(&reconcilersInfo, r.config) + ebpfAgentController := ebpf.NewAgentController(reconcilersInfo.NewInstance(r.mgr.Config.EBPFAgentImage, r.status)) if err := ebpfAgentController.Reconcile(ctx, desired); err != nil { - return ctrl.Result{}, r.failure(ctx, conditions.ReconcileAgentFailed(err), desired) + return r.status.Error("ReconcileAgentFailed", err) } // Console plugin - if r.availableAPIs.HasConsolePlugin() { + if r.mgr.HasConsolePlugin() { err := cpReconciler.Reconcile(ctx, desired) if err != nil { - return ctrl.Result{}, r.failure(ctx, conditions.ReconcileConsolePluginFailed(err), desired) - } - } - - // Set readiness status - var status *metav1.Condition - if didChange { - status = conditions.Updating() - } else if isInProgress { - status = conditions.DeploymentInProgress() - } else { - status = conditions.Ready() - } - return ctrl.Result{}, r.updateCondition(ctx, status, desired) -} - -func (r *FlowCollectorReconciler) getFlowCollector(ctx context.Context) (*flowslatest.FlowCollector, error) { - log := log.FromContext(ctx) - desired := &flowslatest.FlowCollector{} - if err := r.Get(ctx, constants.FlowCollectorName, desired); err != nil { - if errors.IsNotFound(err) { - // Request object not found, could have been deleted after reconcile request. - // Owned objects are automatically garbage collected. For additional cleanup logic use finalizers. - // Return and don't requeue - log.Info("FlowCollector resource not found. Ignoring since object must be deleted") - return nil, nil - } - // Error reading the object - requeue the request. - return nil, err - } - - if ret, err := r.checkFinalizer(ctx, desired); ret { - return nil, err - } - return desired, nil -} - -func (r *FlowCollectorReconciler) handleNamespaceChanged( - ctx context.Context, - oldNS, newNS string, - desired *flowslatest.FlowCollector, - flpReconciler *flowlogspipeline.FLPReconciler, - cpReconciler *consoleplugin.CPReconciler, -) error { - log := log.FromContext(ctx) - if oldNS != "" { - // Namespace updated, clean up previous namespace - log.Info("FlowCollector namespace change detected: cleaning up previous namespace", "old namespace", oldNS, "new namepace", newNS) - flpReconciler.CleanupNamespace(ctx) - if r.availableAPIs.HasConsolePlugin() { - cpReconciler.CleanupNamespace(ctx) - } - } - - // Update namespace in status - log.Info("Updating status with new namespace " + newNS) - desired.Status.Namespace = newNS - return r.updateCondition(ctx, conditions.Updating(), desired) -} - -// SetupWithManager sets up the controller with the Manager. -func (r *FlowCollectorReconciler) SetupWithManager(ctx context.Context, mgr ctrl.Manager) error { - builder := ctrl.NewControllerManagedBy(mgr). - For(&flowslatest.FlowCollector{}). - Owns(&appsv1.Deployment{}). - Owns(&appsv1.DaemonSet{}). - Owns(&ascv2.HorizontalPodAutoscaler{}). - Owns(&corev1.Namespace{}). - Owns(&corev1.Service{}). - Owns(&corev1.ServiceAccount{}) - - if err := r.setupDiscovery(ctx, mgr, builder); err != nil { - return err - } - - ctrl, err := builder.Build(r) - if err != nil { - return err - } - r.watcher = watchers.NewWatcher(ctrl) - - return nil -} - -func (r *FlowCollectorReconciler) setupDiscovery(ctx context.Context, mgr ctrl.Manager, builder *builder.Builder) error { - log := log.FromContext(ctx) - dc, err := discovery.NewDiscoveryClientForConfig(mgr.GetConfig()) - if err != nil { - return fmt.Errorf("can't instantiate discovery client: %w", err) - } - r.permissions = discover.Permissions{ - Client: dc, - } - if r.permissions.Vendor(ctx) == discover.VendorOpenShift { - builder.Owns(&securityv1.SecurityContextConstraints{}) - } - apis, err := discover.NewAvailableAPIs(dc) - if err != nil { - return fmt.Errorf("can't discover available APIs: %w", err) - } - r.availableAPIs = apis - if apis.HasConsolePlugin() { - builder.Owns(&osv1alpha1.ConsolePlugin{}) - } else { - log.Info("Console not detected: the console plugin is not available") - } - if !apis.HasCNO() { - log.Info("CNO not detected: using ovnKubernetes config and reconciler") - } - return nil -} - -func getNamespaceName(desired *flowslatest.FlowCollector) string { - if desired.Spec.Namespace != "" { - return desired.Spec.Namespace - } - return constants.DefaultOperatorNamespace -} - -func (r *FlowCollectorReconciler) namespaceExist(ctx context.Context, nsName string) (*corev1.Namespace, error) { - ns := &corev1.Namespace{} - err := r.Get(ctx, types.NamespacedName{Name: nsName}, ns) - if err != nil { - if errors.IsNotFound(err) { - return nil, nil - } - log.FromContext(ctx).Error(err, "Failed to get namespace") - return nil, err - } - return ns, nil -} - -func (r *FlowCollectorReconciler) reconcileOperator(ctx context.Context, cmn *reconcilers.Common, desired *flowslatest.FlowCollector) error { - // If namespace does not exist, we create it - nsExist, err := r.namespaceExist(ctx, cmn.Namespace) - if err != nil { - return err - } - desiredNs := buildNamespace(cmn.Namespace, r.config.DownstreamDeployment) - if nsExist == nil { - err = r.Create(ctx, desiredNs) - if err != nil { - return r.failure(ctx, conditions.CannotCreateNamespace(err), desired) - } - } else if !helper.IsSubSet(nsExist.ObjectMeta.Labels, desiredNs.ObjectMeta.Labels) { - err = r.Update(ctx, desiredNs) - if err != nil { - return err - } - } - if r.config.DownstreamDeployment { - desiredRole := buildRoleMonitoringReader() - if err := cmn.ReconcileClusterRole(ctx, desiredRole); err != nil { - return err - } - desiredBinding := buildRoleBindingMonitoringReader(cmn.Namespace) - if err := cmn.ReconcileClusterRoleBinding(ctx, desiredBinding); err != nil { - return err + return r.status.Error("ReconcileConsolePluginFailed", err) } } - if r.availableAPIs.HasSvcMonitor() { - names := helper.GetIncludeList(&desired.Spec) - desiredFlowDashboardCM, del, err := buildFlowMetricsDashboard(cmn.Namespace, names) - if err != nil { - return err - } else if err = cmn.ReconcileConfigMap(ctx, desiredFlowDashboardCM, del); err != nil { - return err - } - - desiredHealthDashboardCM, del, err := buildHealthDashboard(cmn.Namespace, names) - if err != nil { - return err - } else if err = cmn.ReconcileConfigMap(ctx, desiredHealthDashboardCM, del); err != nil { - return err - } - } return nil } // checkFinalizer returns true (and/or error) if the calling function needs to return -func (r *FlowCollectorReconciler) checkFinalizer(ctx context.Context, desired *flowslatest.FlowCollector) (bool, error) { +func (r *FlowCollectorReconciler) checkFinalizer(ctx context.Context, desired *flowslatest.FlowCollector, info *reconcilers.Common) (bool, error) { if !desired.ObjectMeta.DeletionTimestamp.IsZero() { if controllerutil.ContainsFinalizer(desired, flowsFinalizer) { // Run finalization logic - if err := r.finalize(ctx, desired); err != nil { + if err := r.finalize(ctx, desired, info); err != nil { return true, err } // Remove finalizer @@ -376,11 +209,9 @@ func (r *FlowCollectorReconciler) checkFinalizer(ctx context.Context, desired *f return false, nil } -func (r *FlowCollectorReconciler) finalize(ctx context.Context, desired *flowslatest.FlowCollector) error { - if !r.availableAPIs.HasCNO() { - ns := getNamespaceName(desired) - info := r.newCommonInfo(ctx, desired, ns, ns, nil, func(b bool) {}, func(b bool) {}) - ovsConfigController := ovs.NewFlowsConfigOVNKController(&info, desired.Spec.Agent.IPFIX.OVNKubernetes) +func (r *FlowCollectorReconciler) finalize(ctx context.Context, desired *flowslatest.FlowCollector, info *reconcilers.Common) error { + if !r.mgr.HasCNO() { + ovsConfigController := ovs.NewFlowsConfigOVNKController(info, desired.Spec.Agent.IPFIX.OVNKubernetes) if err := ovsConfigController.Finalize(ctx, desired); err != nil { return fmt.Errorf("failed to finalize ovn-kubernetes reconciler: %w", err) } @@ -388,46 +219,14 @@ func (r *FlowCollectorReconciler) finalize(ctx context.Context, desired *flowsla return nil } -func (r *FlowCollectorReconciler) newCommonInfo(ctx context.Context, desired *flowslatest.FlowCollector, ns, prevNs string, loki *helper.LokiConfig, changeHook, inProgressHook func(bool)) reconcilers.Common { +func (r *FlowCollectorReconciler) newCommonInfo(clh *helper.Client, ns, prevNs string, loki *helper.LokiConfig) reconcilers.Common { return reconcilers.Common{ - Client: helper.Client{ - Client: r.Client, - SetControllerReference: func(obj client.Object) error { - return ctrl.SetControllerReference(desired, obj, r.Scheme) - }, - SetChanged: changeHook, - SetInProgress: inProgressHook, - }, + Client: *clh, Namespace: ns, PreviousNamespace: prevNs, - UseOpenShiftSCC: r.permissions.Vendor(ctx) == discover.VendorOpenShift, - AvailableAPIs: r.availableAPIs, + UseOpenShiftSCC: r.mgr.IsOpenShift(), + AvailableAPIs: &r.mgr.AvailableAPIs, Watcher: r.watcher, Loki: loki, } } - -func (r *FlowCollectorReconciler) failure(ctx context.Context, errcond *conditions.ErrorCondition, fc *flowslatest.FlowCollector) error { - log.FromContext(ctx).Info("Updating failure status to " + errcond.Reason) - log := log.FromContext(ctx) - log.Error(errcond.Error, errcond.Message) - conditions.AddUniqueCondition(&errcond.Condition, fc) - if errUpdate := r.Status().Update(ctx, fc); errUpdate != nil { - log.Error(errUpdate, "Set conditions failed") - } - return errcond.Error -} - -func (r *FlowCollectorReconciler) updateCondition(ctx context.Context, cond *metav1.Condition, fc *flowslatest.FlowCollector) error { - log.FromContext(ctx).Info("Updating status to " + cond.Reason) - conditions.AddUniqueCondition(cond, fc) - if err := r.Status().Update(ctx, fc); err != nil { - log.FromContext(ctx).Error(err, "Set conditions failed") - // Do not propagate this update failure if it was modified concurrently: - // in that case, it will anyway trigger new reconcile loops so the conditions will be updated soon. - if !errors.IsConflict(err) { - return err - } - } - return nil -} diff --git a/controllers/flowcollector_controller_certificates_test.go b/controllers/flowcollector_controller_certificates_test.go index 907b76ba6..ebe56c9af 100644 --- a/controllers/flowcollector_controller_certificates_test.go +++ b/controllers/flowcollector_controller_certificates_test.go @@ -13,7 +13,7 @@ import ( flowslatest "github.com/netobserv/network-observability-operator/api/v1beta2" "github.com/netobserv/network-observability-operator/controllers/constants" . "github.com/netobserv/network-observability-operator/controllers/controllerstest" - "github.com/netobserv/network-observability-operator/controllers/flowlogspipeline" + "github.com/netobserv/network-observability-operator/controllers/flp" "github.com/netobserv/network-observability-operator/pkg/watchers" ) @@ -27,7 +27,7 @@ func flowCollectorCertificatesSpecs() { Name: "cluster", } flpKey := types.NamespacedName{ - Name: constants.FLPName + flowlogspipeline.FlpConfSuffix[flowlogspipeline.ConfKafkaTransformer], + Name: constants.FLPName + flp.FlpConfSuffix[flp.ConfKafkaTransformer], Namespace: operatorNamespace, } pluginKey := types.NamespacedName{ diff --git a/controllers/flowcollector_controller_test.go b/controllers/flowcollector_controller_test.go index ba2778e78..a16da8db3 100644 --- a/controllers/flowcollector_controller_test.go +++ b/controllers/flowcollector_controller_test.go @@ -21,7 +21,7 @@ import ( flowslatest "github.com/netobserv/network-observability-operator/api/v1beta2" "github.com/netobserv/network-observability-operator/controllers/constants" . "github.com/netobserv/network-observability-operator/controllers/controllerstest" - "github.com/netobserv/network-observability-operator/controllers/flowlogspipeline" + "github.com/netobserv/network-observability-operator/controllers/flp" "github.com/netobserv/network-observability-operator/pkg/test" ) @@ -55,11 +55,11 @@ func flowCollectorControllerSpecs() { Namespace: otherNamespace, } flpKeyKafkaIngester := types.NamespacedName{ - Name: constants.FLPName + flowlogspipeline.FlpConfSuffix[flowlogspipeline.ConfKafkaIngester], + Name: constants.FLPName + flp.FlpConfSuffix[flp.ConfKafkaIngester], Namespace: operatorNamespace, } flpKeyKafkaTransformer := types.NamespacedName{ - Name: constants.FLPName + flowlogspipeline.FlpConfSuffix[flowlogspipeline.ConfKafkaTransformer], + Name: constants.FLPName + flp.FlpConfSuffix[flp.ConfKafkaTransformer], Namespace: operatorNamespace, } cpKey1 := types.NamespacedName{ @@ -70,10 +70,10 @@ func flowCollectorControllerSpecs() { Name: "netobserv-plugin", Namespace: otherNamespace, } - rbKeyIngest := types.NamespacedName{Name: flowlogspipeline.RoleBindingName(flowlogspipeline.ConfKafkaIngester)} - rbKeyTransform := types.NamespacedName{Name: flowlogspipeline.RoleBindingName(flowlogspipeline.ConfKafkaTransformer)} - rbKeyIngestMono := types.NamespacedName{Name: flowlogspipeline.RoleBindingMonoName(flowlogspipeline.ConfKafkaIngester)} - rbKeyTransformMono := types.NamespacedName{Name: flowlogspipeline.RoleBindingMonoName(flowlogspipeline.ConfKafkaTransformer)} + rbKeyIngest := types.NamespacedName{Name: flp.RoleBindingName(flp.ConfKafkaIngester)} + rbKeyTransform := types.NamespacedName{Name: flp.RoleBindingName(flp.ConfKafkaTransformer)} + rbKeyIngestMono := types.NamespacedName{Name: flp.RoleBindingMonoName(flp.ConfKafkaIngester)} + rbKeyTransformMono := types.NamespacedName{Name: flp.RoleBindingMonoName(flp.ConfKafkaTransformer)} rbKeyPlugin := types.NamespacedName{Name: constants.PluginName} // Created objects to cleanup @@ -144,15 +144,6 @@ func flowCollectorControllerSpecs() { // Create Expect(k8sClient.Create(ctx, created)).Should(Succeed()) - By("Expecting status to be updating") - Eventually(func() error { - updatedCr := flowslatest.FlowCollector{} - if err := k8sClient.Get(ctx, crKey, &updatedCr); err != nil { - return err - } - return conditionMatch(updatedCr.Status.Conditions, "Pending", "Updating") - }, timeout, interval).Should(Succeed()) - By("Expecting to create the flowlogs-pipeline DaemonSet") Eventually(func() error { if err := k8sClient.Get(ctx, flpKey1, &ds); err != nil { @@ -963,21 +954,6 @@ func flowCollectorControllerSpecs() { }, timeout, interval).Should(Succeed()) }) - It("Should condition be ready", func() { - // Do a dummy change that will trigger reconcile - UpdateCR(crKey, func(fc *flowslatest.FlowCollector) { - fc.Spec.ConsolePlugin.LogLevel = "debug" - }) - By("Expecting status to be ready") - Eventually(func() error { - updatedCr := flowslatest.FlowCollector{} - if err := k8sClient.Get(ctx, crKey, &updatedCr); err != nil { - return err - } - return conditionMatch(updatedCr.Status.Conditions, "Ready", "Ready") - }, timeout, interval).Should(Succeed()) - }) - It("Should delete CR", func() { Eventually(func() error { return k8sClient.Delete(ctx, &flowCR) @@ -1081,20 +1057,3 @@ func checkDigestUpdate(oldDigest *string, annots map[string]string) error { *oldDigest = newDigest return nil } - -func conditionMatch(conditions []metav1.Condition, conditionType string, conditionReason string) error { - if len(conditions) < 1 { - return fmt.Errorf("Invalid status condition length %d\nconditions: %v", len(conditions), conditions) - } - // check only first condition since AddUniqueCondition function sort them - if conditions[0].Type != conditionType { - return fmt.Errorf("Invalid condition type %s != %s\nconditions: %v", conditions[0].Type, conditionType, conditions) - } - if conditions[0].Reason != conditionReason { - return fmt.Errorf("Invalid condition reason %s != %s\nconditions: %v", conditions[0].Reason, conditionReason, conditions) - } - if conditions[0].Status != metav1.ConditionTrue { - return fmt.Errorf("Invalid condition status %s != %s\nconditions: %v", conditions[0].Status, metav1.ConditionTrue, conditions) - } - return nil -} diff --git a/controllers/flowlogspipeline/flp_reconciler.go b/controllers/flowlogspipeline/flp_reconciler.go deleted file mode 100644 index 62229bc5a..000000000 --- a/controllers/flowlogspipeline/flp_reconciler.go +++ /dev/null @@ -1,138 +0,0 @@ -package flowlogspipeline - -import ( - "context" - "fmt" - - flowslatest "github.com/netobserv/network-observability-operator/api/v1beta2" - "github.com/netobserv/network-observability-operator/controllers/reconcilers" - "github.com/netobserv/network-observability-operator/pkg/helper" - "github.com/netobserv/network-observability-operator/pkg/loki" - "github.com/netobserv/network-observability-operator/pkg/watchers" -) - -// Type alias -type flpSpec = flowslatest.FlowCollectorFLP - -// FLPReconciler reconciles the current flowlogs-pipeline state with the desired configuration -type FLPReconciler struct { - reconcilers []singleReconciler -} - -const contextReconcilerName = "FLP kind" - -type singleReconciler interface { - context(ctx context.Context) context.Context - cleanupNamespace(ctx context.Context) - reconcile(ctx context.Context, desired *flowslatest.FlowCollector) error -} - -func NewReconciler(cmn *reconcilers.Common, image string) FLPReconciler { - return FLPReconciler{ - reconcilers: []singleReconciler{ - newMonolithReconciler(cmn.NewInstance(image)), - newTransformerReconciler(cmn.NewInstance(image)), - newIngesterReconciler(cmn.NewInstance(image)), - }, - } -} - -// CleanupNamespace cleans up old namespace -func (r *FLPReconciler) CleanupNamespace(ctx context.Context) { - for _, sr := range r.reconcilers { - sr.cleanupNamespace(sr.context(ctx)) - } -} - -func validateDesired(desired *flpSpec) error { - if desired.Port == 4789 || - desired.Port == 6081 || - desired.Port == 500 || - desired.Port == 4500 { - return fmt.Errorf("flowlogs-pipeline port value is not authorized") - } - return nil -} - -func (r *FLPReconciler) Reconcile(ctx context.Context, desired *flowslatest.FlowCollector) error { - if err := validateDesired(&desired.Spec.Processor); err != nil { - return err - } - for _, sr := range r.reconcilers { - if err := sr.reconcile(sr.context(ctx), desired); err != nil { - return err - } - } - return nil -} - -func annotateKafkaExporterCerts(ctx context.Context, info *reconcilers.Common, exp []*flowslatest.FlowCollectorExporter, annotations map[string]string) error { - for i, exporter := range exp { - if exporter.Type == flowslatest.KafkaExporter { - if err := annotateKafkaCerts(ctx, info, &exporter.Kafka, fmt.Sprintf("kafka-export-%d", i), annotations); err != nil { - return err - } - } - } - return nil -} - -func annotateKafkaCerts(ctx context.Context, info *reconcilers.Common, spec *flowslatest.FlowCollectorKafka, prefix string, annotations map[string]string) error { - caDigest, userDigest, err := info.Watcher.ProcessMTLSCerts(ctx, info.Client, &spec.TLS, info.Namespace) - if err != nil { - return err - } - if caDigest != "" { - annotations[watchers.Annotation(prefix+"-ca")] = caDigest - } - if userDigest != "" { - annotations[watchers.Annotation(prefix+"-user")] = userDigest - } - if helper.UseSASL(&spec.SASL) { - saslDigest1, saslDigest2, err := info.Watcher.ProcessSASL(ctx, info.Client, &spec.SASL, info.Namespace) - if err != nil { - return err - } - if saslDigest1 != "" { - annotations[watchers.Annotation(prefix+"-sd1")] = saslDigest1 - } - if saslDigest2 != "" { - annotations[watchers.Annotation(prefix+"-sd2")] = saslDigest2 - } - } - return nil -} - -func reconcileMonitoringCerts(ctx context.Context, info *reconcilers.Common, tlsConfig *flowslatest.ServerTLS, ns string) error { - if tlsConfig.Type == flowslatest.ServerTLSProvided && tlsConfig.Provided != nil { - _, err := info.Watcher.ProcessCertRef(ctx, info.Client, tlsConfig.Provided, ns) - if err != nil { - return err - } - } - if !tlsConfig.InsecureSkipVerify && tlsConfig.ProvidedCaFile != nil && tlsConfig.ProvidedCaFile.File != "" { - _, err := info.Watcher.ProcessFileReference(ctx, info.Client, *tlsConfig.ProvidedCaFile, ns) - if err != nil { - return err - } - } - - return nil -} - -func reconcileLokiRoles(ctx context.Context, r *reconcilers.Common, b *builder) error { - roles := loki.ClusterRoles(b.desired.Loki.Mode) - if len(roles) > 0 { - for i := range roles { - if err := r.ReconcileClusterRole(ctx, &roles[i]); err != nil { - return err - } - } - // Binding - crb := loki.ClusterRoleBinding(b.name(), b.name(), b.info.Namespace) - if err := r.ReconcileClusterRoleBinding(ctx, crb); err != nil { - return err - } - } - return nil -} diff --git a/controllers/flowlogspipeline/flp_transfo_reconciler.go b/controllers/flowlogspipeline/flp_transfo_reconciler.go deleted file mode 100644 index e29c201d5..000000000 --- a/controllers/flowlogspipeline/flp_transfo_reconciler.go +++ /dev/null @@ -1,217 +0,0 @@ -package flowlogspipeline - -import ( - "context" - - monitoringv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1" - appsv1 "k8s.io/api/apps/v1" - ascv2 "k8s.io/api/autoscaling/v2" - corev1 "k8s.io/api/core/v1" - rbacv1 "k8s.io/api/rbac/v1" - "k8s.io/apimachinery/pkg/api/equality" - "sigs.k8s.io/controller-runtime/pkg/log" - - flowslatest "github.com/netobserv/network-observability-operator/api/v1beta2" - "github.com/netobserv/network-observability-operator/controllers/constants" - "github.com/netobserv/network-observability-operator/controllers/reconcilers" - "github.com/netobserv/network-observability-operator/pkg/helper" -) - -// flpTransformerReconciler reconciles the current flowlogs-pipeline-transformer state with the desired configuration -type flpTransformerReconciler struct { - *reconcilers.Instance - owned transfoOwnedObjects -} - -type transfoOwnedObjects struct { - deployment *appsv1.Deployment - promService *corev1.Service - hpa *ascv2.HorizontalPodAutoscaler - serviceAccount *corev1.ServiceAccount - configMap *corev1.ConfigMap - roleBinding *rbacv1.ClusterRoleBinding - serviceMonitor *monitoringv1.ServiceMonitor - prometheusRule *monitoringv1.PrometheusRule -} - -func newTransformerReconciler(cmn *reconcilers.Instance) *flpTransformerReconciler { - name := name(ConfKafkaTransformer) - owned := transfoOwnedObjects{ - deployment: &appsv1.Deployment{}, - promService: &corev1.Service{}, - hpa: &ascv2.HorizontalPodAutoscaler{}, - serviceAccount: &corev1.ServiceAccount{}, - configMap: &corev1.ConfigMap{}, - roleBinding: &rbacv1.ClusterRoleBinding{}, - serviceMonitor: &monitoringv1.ServiceMonitor{}, - prometheusRule: &monitoringv1.PrometheusRule{}, - } - cmn.Managed.AddManagedObject(name, owned.deployment) - cmn.Managed.AddManagedObject(name, owned.hpa) - cmn.Managed.AddManagedObject(name, owned.serviceAccount) - cmn.Managed.AddManagedObject(promServiceName(ConfKafkaTransformer), owned.promService) - cmn.Managed.AddManagedObject(RoleBindingName(ConfKafkaTransformer), owned.roleBinding) - cmn.Managed.AddManagedObject(configMapName(ConfKafkaTransformer), owned.configMap) - if cmn.AvailableAPIs.HasSvcMonitor() { - cmn.Managed.AddManagedObject(serviceMonitorName(ConfKafkaTransformer), owned.serviceMonitor) - } - if cmn.AvailableAPIs.HasPromRule() { - cmn.Managed.AddManagedObject(prometheusRuleName(ConfKafkaTransformer), owned.prometheusRule) - } - - return &flpTransformerReconciler{ - Instance: cmn, - owned: owned, - } -} - -func (r *flpTransformerReconciler) context(ctx context.Context) context.Context { - l := log.FromContext(ctx).WithValues(contextReconcilerName, "transformer") - return log.IntoContext(ctx, l) -} - -// cleanupNamespace cleans up old namespace -func (r *flpTransformerReconciler) cleanupNamespace(ctx context.Context) { - r.Managed.CleanupPreviousNamespace(ctx) -} - -func (r *flpTransformerReconciler) reconcile(ctx context.Context, desired *flowslatest.FlowCollector) error { - // Retrieve current owned objects - err := r.Managed.FetchAll(ctx) - if err != nil { - return err - } - - // Transformer only used with Kafka - if !helper.UseKafka(&desired.Spec) { - r.Managed.TryDeleteAll(ctx) - return nil - } - - builder, err := newTransfoBuilder(r.Instance, &desired.Spec) - if err != nil { - return err - } - newCM, configDigest, err := builder.configMap() - if err != nil { - return err - } - annotations := map[string]string{ - constants.PodConfigurationDigest: configDigest, - } - if !r.Managed.Exists(r.owned.configMap) { - if err := r.CreateOwned(ctx, newCM); err != nil { - return err - } - } else if !equality.Semantic.DeepDerivative(newCM.Data, r.owned.configMap.Data) { - if err := r.UpdateIfOwned(ctx, r.owned.configMap, newCM); err != nil { - return err - } - } - if err := r.reconcilePermissions(ctx, &builder); err != nil { - return err - } - - err = r.reconcilePrometheusService(ctx, &builder) - if err != nil { - return err - } - - // Watch for Loki certificate if necessary; we'll ignore in that case the returned digest, as we don't need to restart pods on cert rotation - // because certificate is always reloaded from file - if _, err = r.Watcher.ProcessCACert(ctx, r.Client, &r.Loki.TLS, r.Namespace); err != nil { - return err - } - - // Watch for Kafka certificate if necessary; need to restart pods in case of cert rotation - if err = annotateKafkaCerts(ctx, r.Common, &desired.Spec.Kafka, "kafka", annotations); err != nil { - return err - } - // Same for Kafka exporters - if err = annotateKafkaExporterCerts(ctx, r.Common, desired.Spec.Exporters, annotations); err != nil { - return err - } - // Watch for monitoring caCert - if err = reconcileMonitoringCerts(ctx, r.Common, &desired.Spec.Processor.Metrics.Server.TLS, r.Namespace); err != nil { - return err - } - - return r.reconcileDeployment(ctx, &desired.Spec.Processor, &builder, annotations) -} - -func (r *flpTransformerReconciler) reconcileDeployment(ctx context.Context, desiredFLP *flpSpec, builder *transfoBuilder, annotations map[string]string) error { - report := helper.NewChangeReport("FLP Deployment") - defer report.LogIfNeeded(ctx) - - newDep := builder.deployment(annotations) - - if !r.Managed.Exists(r.owned.deployment) { - if err := r.CreateOwned(ctx, newDep); err != nil { - return err - } - } else if helper.DeploymentChanged(r.owned.deployment, newDep, constants.FLPName, helper.HPADisabled(&desiredFLP.KafkaConsumerAutoscaler), helper.PtrInt32(desiredFLP.KafkaConsumerReplicas), &report) { - if err := r.UpdateIfOwned(ctx, r.owned.deployment, newDep); err != nil { - return err - } - } else { - // Deployment up to date, check if it's ready - r.CheckDeploymentInProgress(r.owned.deployment) - } - - // Delete or Create / Update Autoscaler according to HPA option - if helper.HPADisabled(&desiredFLP.KafkaConsumerAutoscaler) { - r.Managed.TryDelete(ctx, r.owned.hpa) - } else { - newASC := builder.autoScaler() - if !r.Managed.Exists(r.owned.hpa) { - if err := r.CreateOwned(ctx, newASC); err != nil { - return err - } - } else if helper.AutoScalerChanged(r.owned.hpa, desiredFLP.KafkaConsumerAutoscaler, &report) { - if err := r.UpdateIfOwned(ctx, r.owned.hpa, newASC); err != nil { - return err - } - } - } - return nil -} - -func (r *flpTransformerReconciler) reconcilePrometheusService(ctx context.Context, builder *transfoBuilder) error { - report := helper.NewChangeReport("FLP prometheus service") - defer report.LogIfNeeded(ctx) - - if err := r.ReconcileService(ctx, r.owned.promService, builder.promService(), &report); err != nil { - return err - } - if r.AvailableAPIs.HasSvcMonitor() { - serviceMonitor := builder.generic.serviceMonitor() - if err := reconcilers.GenericReconcile(ctx, r.Managed, &r.Client, r.owned.serviceMonitor, serviceMonitor, &report, helper.ServiceMonitorChanged); err != nil { - return err - } - } - if r.AvailableAPIs.HasPromRule() { - promRules := builder.generic.prometheusRule() - if err := reconcilers.GenericReconcile(ctx, r.Managed, &r.Client, r.owned.prometheusRule, promRules, &report, helper.PrometheusRuleChanged); err != nil { - return err - } - } - return nil -} - -func (r *flpTransformerReconciler) reconcilePermissions(ctx context.Context, builder *transfoBuilder) error { - if !r.Managed.Exists(r.owned.serviceAccount) { - return r.CreateOwned(ctx, builder.serviceAccount()) - } // We only configure name, update is not needed for now - - cr := BuildClusterRoleTransformer() - if err := r.ReconcileClusterRole(ctx, cr); err != nil { - return err - } - - desired := builder.clusterRoleBinding() - if err := r.ReconcileClusterRoleBinding(ctx, desired); err != nil { - return err - } - - return reconcileLokiRoles(ctx, r.Common, &builder.generic) -} diff --git a/controllers/flowlogspipeline/flp_common_objects.go b/controllers/flp/flp_common_objects.go similarity index 99% rename from controllers/flowlogspipeline/flp_common_objects.go rename to controllers/flp/flp_common_objects.go index 47be4b9e7..3639172e4 100644 --- a/controllers/flowlogspipeline/flp_common_objects.go +++ b/controllers/flp/flp_common_objects.go @@ -1,4 +1,4 @@ -package flowlogspipeline +package flp import ( "encoding/json" @@ -141,7 +141,7 @@ func (b *builder) NewKafkaPipeline() PipelineBuilder { } func (b *builder) initPipeline(ingest config.PipelineBuilderStage) PipelineBuilder { - pipeline := newPipelineBuilder(b.desired, b.info.Loki, &b.volumes, &ingest) + pipeline := newPipelineBuilder(b.desired, b.info.Loki, b.info.ClusterID, &b.volumes, &ingest) b.pipeline = &pipeline return pipeline } diff --git a/controllers/flp/flp_controller.go b/controllers/flp/flp_controller.go new file mode 100644 index 000000000..3de93895c --- /dev/null +++ b/controllers/flp/flp_controller.go @@ -0,0 +1,232 @@ +package flp + +import ( + "context" + "fmt" + + flowslatest "github.com/netobserv/network-observability-operator/api/v1beta2" + "github.com/netobserv/network-observability-operator/controllers/reconcilers" + "github.com/netobserv/network-observability-operator/pkg/helper" + "github.com/netobserv/network-observability-operator/pkg/loki" + "github.com/netobserv/network-observability-operator/pkg/manager" + "github.com/netobserv/network-observability-operator/pkg/manager/status" + "github.com/netobserv/network-observability-operator/pkg/watchers" + configv1 "github.com/openshift/api/config/v1" + appsv1 "k8s.io/api/apps/v1" + ascv2 "k8s.io/api/autoscaling/v2" + corev1 "k8s.io/api/core/v1" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" +) + +// Reconciler reconciles the current flowlogs-pipeline state with the desired configuration +type Reconciler struct { + client.Client + mgr *manager.Manager + watcher *watchers.Watcher + status status.Instance + clusterID string +} + +func Start(ctx context.Context, mgr *manager.Manager) error { + log := log.FromContext(ctx) + log.Info("Starting Flowlogs Pipeline parent controller") + + r := Reconciler{ + Client: mgr.Client, + mgr: mgr, + status: mgr.Status.ForComponent(status.FLPParent), + } + builder := ctrl.NewControllerManagedBy(mgr). + For(&flowslatest.FlowCollector{}, reconcilers.IgnoreStatusChange). + Named("flp"). + Owns(&appsv1.Deployment{}). + Owns(&appsv1.DaemonSet{}). + Owns(&ascv2.HorizontalPodAutoscaler{}). + Owns(&corev1.Namespace{}). + Owns(&corev1.Service{}). + Owns(&corev1.ServiceAccount{}) + + ctrl, err := builder.Build(&r) + if err != nil { + return err + } + r.watcher = watchers.NewWatcher(ctrl) + + return nil +} + +type subReconciler interface { + context(ctx context.Context) context.Context + cleanupNamespace(ctx context.Context) + reconcile(ctx context.Context, desired *flowslatest.FlowCollector) error + getStatus() *status.Instance +} + +// Reconcile is the controller entry point for reconciling current state with desired state. +// It manages the controller status at a high level. Business logic is delegated into `reconcile`. +func (r *Reconciler) Reconcile(ctx context.Context, _ ctrl.Request) (ctrl.Result, error) { + l := log.Log.WithName("flp") // clear context (too noisy) + ctx = log.IntoContext(ctx, l) + + r.status.SetUnknown() + defer r.status.Commit(ctx, r.Client) + + err := r.reconcile(ctx) + if err != nil { + l.Error(err, "FLP reconcile failure") + // Set status failure unless it was already set + if !r.status.HasFailure() { + r.status.SetFailure("FLPError", err.Error()) + } + return ctrl.Result{}, err + } + + r.status.SetReady() + return ctrl.Result{}, nil +} + +func (r *Reconciler) reconcile(ctx context.Context) error { + log := log.FromContext(ctx) + + clh, fc, err := helper.NewFlowCollectorClientHelper(ctx, r.Client) + if err != nil { + return fmt.Errorf("failed to get FlowCollector: %w", err) + } else if fc == nil { + return nil + } + + ns := helper.GetNamespace(&fc.Spec) + previousNamespace := r.status.GetDeployedNamespace(fc) + loki := helper.NewLokiConfig(&fc.Spec.Loki, ns) + cmn := r.newCommonInfo(clh, ns, previousNamespace, &loki) + + r.watcher.Reset(ns) + + // obtain default cluster ID - api is specific to openshift + if r.mgr.IsOpenShift() && r.clusterID == "" { + cversion := &configv1.ClusterVersion{} + key := client.ObjectKey{Name: "version"} + if err := r.Client.Get(ctx, key, cversion); err != nil { + log.Error(err, "unable to obtain cluster ID") + } else { + r.clusterID = string(cversion.Spec.ClusterID) + } + } + + // Create sub-reconcilers + // TODO: refactor to move these subReconciler allocations in `Start`. It will involve some decoupling work, as currently + // `reconcilers.Common` is dependent on the FlowCollector object, which isn't known at start time. + reconcilers := []subReconciler{ + newMonolithReconciler(cmn.NewInstance(r.mgr.Config.FlowlogsPipelineImage, r.mgr.Status.ForComponent(status.FLPMonolith))), + newTransformerReconciler(cmn.NewInstance(r.mgr.Config.FlowlogsPipelineImage, r.mgr.Status.ForComponent(status.FLPTransformOnly))), + newIngesterReconciler(cmn.NewInstance(r.mgr.Config.FlowlogsPipelineImage, r.mgr.Status.ForComponent(status.FLPIngestOnly))), + } + + // Check namespace changed + if ns != previousNamespace { + if previousNamespace != "" { + log.Info("FlowCollector namespace change detected: cleaning up previous namespace", "old", previousNamespace, "new", ns) + for _, sr := range reconcilers { + sr.cleanupNamespace(sr.context(ctx)) + } + } + // Update namespace in status + if err := r.status.SetDeployedNamespace(ctx, r.Client, ns); err != nil { + return r.status.Error("ChangeNamespaceError", err) + } + } + + for _, sr := range reconcilers { + if err := sr.reconcile(sr.context(ctx), fc); err != nil { + return sr.getStatus().Error("FLPReconcileError", err) + } + } + + return nil +} + +func (r *Reconciler) newCommonInfo(clh *helper.Client, ns, prevNs string, loki *helper.LokiConfig) reconcilers.Common { + return reconcilers.Common{ + Client: *clh, + Namespace: ns, + PreviousNamespace: prevNs, + UseOpenShiftSCC: r.mgr.IsOpenShift(), + AvailableAPIs: &r.mgr.AvailableAPIs, + Watcher: r.watcher, + Loki: loki, + ClusterID: r.clusterID, + } +} + +func annotateKafkaExporterCerts(ctx context.Context, info *reconcilers.Common, exp []*flowslatest.FlowCollectorExporter, annotations map[string]string) error { + for i, exporter := range exp { + if exporter.Type == flowslatest.KafkaExporter { + if err := annotateKafkaCerts(ctx, info, &exporter.Kafka, fmt.Sprintf("kafka-export-%d", i), annotations); err != nil { + return err + } + } + } + return nil +} + +func annotateKafkaCerts(ctx context.Context, info *reconcilers.Common, spec *flowslatest.FlowCollectorKafka, prefix string, annotations map[string]string) error { + caDigest, userDigest, err := info.Watcher.ProcessMTLSCerts(ctx, info.Client, &spec.TLS, info.Namespace) + if err != nil { + return err + } + if caDigest != "" { + annotations[watchers.Annotation(prefix+"-ca")] = caDigest + } + if userDigest != "" { + annotations[watchers.Annotation(prefix+"-user")] = userDigest + } + if helper.UseSASL(&spec.SASL) { + saslDigest1, saslDigest2, err := info.Watcher.ProcessSASL(ctx, info.Client, &spec.SASL, info.Namespace) + if err != nil { + return err + } + if saslDigest1 != "" { + annotations[watchers.Annotation(prefix+"-sd1")] = saslDigest1 + } + if saslDigest2 != "" { + annotations[watchers.Annotation(prefix+"-sd2")] = saslDigest2 + } + } + return nil +} + +func reconcileMonitoringCerts(ctx context.Context, info *reconcilers.Common, tlsConfig *flowslatest.ServerTLS, ns string) error { + if tlsConfig.Type == flowslatest.ServerTLSProvided && tlsConfig.Provided != nil { + _, err := info.Watcher.ProcessCertRef(ctx, info.Client, tlsConfig.Provided, ns) + if err != nil { + return err + } + } + if !tlsConfig.InsecureSkipVerify && tlsConfig.ProvidedCaFile != nil && tlsConfig.ProvidedCaFile.File != "" { + _, err := info.Watcher.ProcessFileReference(ctx, info.Client, *tlsConfig.ProvidedCaFile, ns) + if err != nil { + return err + } + } + + return nil +} + +func reconcileLokiRoles(ctx context.Context, r *reconcilers.Common, b *builder) error { + roles := loki.ClusterRoles(b.desired.Loki.Mode) + if len(roles) > 0 { + for i := range roles { + if err := r.ReconcileClusterRole(ctx, &roles[i]); err != nil { + return err + } + } + // Binding + crb := loki.ClusterRoleBinding(b.name(), b.name(), b.info.Namespace) + if err := r.ReconcileClusterRoleBinding(ctx, crb); err != nil { + return err + } + } + return nil +} diff --git a/controllers/flowlogspipeline/flp_ingest_objects.go b/controllers/flp/flp_ingest_objects.go similarity index 98% rename from controllers/flowlogspipeline/flp_ingest_objects.go rename to controllers/flp/flp_ingest_objects.go index 834f34160..a03566dfc 100644 --- a/controllers/flowlogspipeline/flp_ingest_objects.go +++ b/controllers/flp/flp_ingest_objects.go @@ -1,4 +1,4 @@ -package flowlogspipeline +package flp import ( appsv1 "k8s.io/api/apps/v1" diff --git a/controllers/flowlogspipeline/flp_ingest_reconciler.go b/controllers/flp/flp_ingest_reconciler.go similarity index 52% rename from controllers/flowlogspipeline/flp_ingest_reconciler.go rename to controllers/flp/flp_ingest_reconciler.go index 195dd465b..dad6e3dc1 100644 --- a/controllers/flowlogspipeline/flp_ingest_reconciler.go +++ b/controllers/flp/flp_ingest_reconciler.go @@ -1,4 +1,4 @@ -package flowlogspipeline +package flp import ( "context" @@ -14,15 +14,11 @@ import ( "github.com/netobserv/network-observability-operator/controllers/constants" "github.com/netobserv/network-observability-operator/controllers/reconcilers" "github.com/netobserv/network-observability-operator/pkg/helper" + "github.com/netobserv/network-observability-operator/pkg/manager/status" ) -// flpIngesterReconciler reconciles the current flowlogs-pipeline-ingester state with the desired configuration -type flpIngesterReconciler struct { +type ingesterReconciler struct { *reconcilers.Instance - owned ingestOwnedObjects -} - -type ingestOwnedObjects struct { daemonSet *appsv1.DaemonSet promService *corev1.Service serviceAccount *corev1.ServiceAccount @@ -32,58 +28,54 @@ type ingestOwnedObjects struct { prometheusRule *monitoringv1.PrometheusRule } -func newIngesterReconciler(cmn *reconcilers.Instance) *flpIngesterReconciler { +func newIngesterReconciler(cmn *reconcilers.Instance) *ingesterReconciler { name := name(ConfKafkaIngester) - owned := ingestOwnedObjects{ - daemonSet: &appsv1.DaemonSet{}, - promService: &corev1.Service{}, - serviceAccount: &corev1.ServiceAccount{}, - configMap: &corev1.ConfigMap{}, - roleBinding: &rbacv1.ClusterRoleBinding{}, - serviceMonitor: &monitoringv1.ServiceMonitor{}, - prometheusRule: &monitoringv1.PrometheusRule{}, - } - cmn.Managed.AddManagedObject(name, owned.daemonSet) - cmn.Managed.AddManagedObject(name, owned.serviceAccount) - cmn.Managed.AddManagedObject(promServiceName(ConfKafkaIngester), owned.promService) - cmn.Managed.AddManagedObject(RoleBindingName(ConfKafkaIngester), owned.roleBinding) - cmn.Managed.AddManagedObject(configMapName(ConfKafkaIngester), owned.configMap) + rec := ingesterReconciler{ + Instance: cmn, + daemonSet: cmn.Managed.NewDaemonSet(name), + promService: cmn.Managed.NewService(promServiceName(ConfKafkaIngester)), + serviceAccount: cmn.Managed.NewServiceAccount(name), + configMap: cmn.Managed.NewConfigMap(configMapName(ConfKafkaIngester)), + roleBinding: cmn.Managed.NewCRB(RoleBindingName(ConfKafkaIngester)), + } if cmn.AvailableAPIs.HasSvcMonitor() { - cmn.Managed.AddManagedObject(serviceMonitorName(ConfKafkaIngester), owned.serviceMonitor) + rec.serviceMonitor = cmn.Managed.NewServiceMonitor(serviceMonitorName(ConfKafkaIngester)) } if cmn.AvailableAPIs.HasPromRule() { - cmn.Managed.AddManagedObject(prometheusRuleName(ConfKafkaIngester), owned.prometheusRule) - } - - return &flpIngesterReconciler{ - Instance: cmn, - owned: owned, + rec.prometheusRule = cmn.Managed.NewPrometheusRule(prometheusRuleName(ConfKafkaIngester)) } + return &rec } -func (r *flpIngesterReconciler) context(ctx context.Context) context.Context { - l := log.FromContext(ctx).WithValues(contextReconcilerName, "ingester") +func (r *ingesterReconciler) context(ctx context.Context) context.Context { + l := log.FromContext(ctx).WithName("ingester") return log.IntoContext(ctx, l) } // cleanupNamespace cleans up old namespace -func (r *flpIngesterReconciler) cleanupNamespace(ctx context.Context) { +func (r *ingesterReconciler) cleanupNamespace(ctx context.Context) { r.Managed.CleanupPreviousNamespace(ctx) } -func (r *flpIngesterReconciler) reconcile(ctx context.Context, desired *flowslatest.FlowCollector) error { +func (r *ingesterReconciler) getStatus() *status.Instance { + return &r.Status +} + +func (r *ingesterReconciler) reconcile(ctx context.Context, desired *flowslatest.FlowCollector) error { // Retrieve current owned objects err := r.Managed.FetchAll(ctx) if err != nil { return err } - // Ingester only used with Kafka and without eBPF if !helper.UseKafka(&desired.Spec) || helper.UseEBPF(&desired.Spec) { + r.Status.SetUnused("Ingester only used with Kafka and without eBPF") r.Managed.TryDeleteAll(ctx) return nil } + r.Status.SetReady() // will be overidden if necessary, as error or pending + builder, err := newIngestBuilder(r.Instance, &desired.Spec) if err != nil { return err @@ -95,12 +87,12 @@ func (r *flpIngesterReconciler) reconcile(ctx context.Context, desired *flowslat annotations := map[string]string{ constants.PodConfigurationDigest: configDigest, } - if !r.Managed.Exists(r.owned.configMap) { + if !r.Managed.Exists(r.configMap) { if err := r.CreateOwned(ctx, newCM); err != nil { return err } - } else if !equality.Semantic.DeepDerivative(newCM.Data, r.owned.configMap.Data) { - if err := r.UpdateIfOwned(ctx, r.owned.configMap, newCM); err != nil { + } else if !equality.Semantic.DeepDerivative(newCM.Data, r.configMap.Data) { + if err := r.UpdateIfOwned(ctx, r.configMap, newCM); err != nil { return err } } @@ -127,45 +119,44 @@ func (r *flpIngesterReconciler) reconcile(ctx context.Context, desired *flowslat return r.reconcileDaemonSet(ctx, builder.daemonSet(annotations)) } -func (r *flpIngesterReconciler) reconcilePrometheusService(ctx context.Context, builder *ingestBuilder) error { +func (r *ingesterReconciler) reconcilePrometheusService(ctx context.Context, builder *ingestBuilder) error { report := helper.NewChangeReport("FLP prometheus service") defer report.LogIfNeeded(ctx) - if err := r.ReconcileService(ctx, r.owned.promService, builder.promService(), &report); err != nil { + if err := r.ReconcileService(ctx, r.promService, builder.promService(), &report); err != nil { return err } if r.AvailableAPIs.HasSvcMonitor() { serviceMonitor := builder.generic.serviceMonitor() - if err := reconcilers.GenericReconcile(ctx, r.Managed, &r.Client, r.owned.serviceMonitor, serviceMonitor, &report, helper.ServiceMonitorChanged); err != nil { + if err := reconcilers.GenericReconcile(ctx, r.Managed, &r.Client, r.serviceMonitor, serviceMonitor, &report, helper.ServiceMonitorChanged); err != nil { return err } } if r.AvailableAPIs.HasPromRule() { promRules := builder.generic.prometheusRule() - if err := reconcilers.GenericReconcile(ctx, r.Managed, &r.Client, r.owned.prometheusRule, promRules, &report, helper.PrometheusRuleChanged); err != nil { + if err := reconcilers.GenericReconcile(ctx, r.Managed, &r.Client, r.prometheusRule, promRules, &report, helper.PrometheusRuleChanged); err != nil { return err } } return nil } -func (r *flpIngesterReconciler) reconcileDaemonSet(ctx context.Context, desiredDS *appsv1.DaemonSet) error { +func (r *ingesterReconciler) reconcileDaemonSet(ctx context.Context, desiredDS *appsv1.DaemonSet) error { report := helper.NewChangeReport("FLP DaemonSet") defer report.LogIfNeeded(ctx) - if !r.Managed.Exists(r.owned.daemonSet) { - return r.CreateOwned(ctx, desiredDS) - } else if helper.PodChanged(&r.owned.daemonSet.Spec.Template, &desiredDS.Spec.Template, constants.FLPName, &report) { - return r.UpdateIfOwned(ctx, r.owned.daemonSet, desiredDS) - } else { - // DaemonSet up to date, check if it's ready - r.CheckDaemonSetInProgress(r.owned.daemonSet) - } - return nil + return reconcilers.ReconcileDaemonSet( + ctx, + r.Instance, + r.daemonSet, + desiredDS, + constants.FLPName, + &report, + ) } -func (r *flpIngesterReconciler) reconcilePermissions(ctx context.Context, builder *ingestBuilder) error { - if !r.Managed.Exists(r.owned.serviceAccount) { +func (r *ingesterReconciler) reconcilePermissions(ctx context.Context, builder *ingestBuilder) error { + if !r.Managed.Exists(r.serviceAccount) { return r.CreateOwned(ctx, builder.serviceAccount()) } // We only configure name, update is not needed for now diff --git a/controllers/flowlogspipeline/flp_monolith_objects.go b/controllers/flp/flp_monolith_objects.go similarity index 98% rename from controllers/flowlogspipeline/flp_monolith_objects.go rename to controllers/flp/flp_monolith_objects.go index 4f0bab0b9..19c94b84a 100644 --- a/controllers/flowlogspipeline/flp_monolith_objects.go +++ b/controllers/flp/flp_monolith_objects.go @@ -1,4 +1,4 @@ -package flowlogspipeline +package flp import ( appsv1 "k8s.io/api/apps/v1" diff --git a/controllers/flowlogspipeline/flp_monolith_reconciler.go b/controllers/flp/flp_monolith_reconciler.go similarity index 56% rename from controllers/flowlogspipeline/flp_monolith_reconciler.go rename to controllers/flp/flp_monolith_reconciler.go index a3e370a35..81020cd99 100644 --- a/controllers/flowlogspipeline/flp_monolith_reconciler.go +++ b/controllers/flp/flp_monolith_reconciler.go @@ -1,4 +1,4 @@ -package flowlogspipeline +package flp import ( "context" @@ -14,15 +14,11 @@ import ( "github.com/netobserv/network-observability-operator/controllers/constants" "github.com/netobserv/network-observability-operator/controllers/reconcilers" "github.com/netobserv/network-observability-operator/pkg/helper" + "github.com/netobserv/network-observability-operator/pkg/manager/status" ) -// flpMonolithReconciler reconciles the current flowlogs-pipeline monolith state with the desired configuration -type flpMonolithReconciler struct { +type monolithReconciler struct { *reconcilers.Instance - owned monolithOwnedObjects -} - -type monolithOwnedObjects struct { daemonSet *appsv1.DaemonSet promService *corev1.Service serviceAccount *corev1.ServiceAccount @@ -33,60 +29,55 @@ type monolithOwnedObjects struct { prometheusRule *monitoringv1.PrometheusRule } -func newMonolithReconciler(cmn *reconcilers.Instance) *flpMonolithReconciler { +func newMonolithReconciler(cmn *reconcilers.Instance) *monolithReconciler { name := name(ConfMonolith) - owned := monolithOwnedObjects{ - daemonSet: &appsv1.DaemonSet{}, - promService: &corev1.Service{}, - serviceAccount: &corev1.ServiceAccount{}, - configMap: &corev1.ConfigMap{}, - roleBindingIn: &rbacv1.ClusterRoleBinding{}, - roleBindingTr: &rbacv1.ClusterRoleBinding{}, - serviceMonitor: &monitoringv1.ServiceMonitor{}, - prometheusRule: &monitoringv1.PrometheusRule{}, - } - cmn.Managed.AddManagedObject(name, owned.daemonSet) - cmn.Managed.AddManagedObject(name, owned.serviceAccount) - cmn.Managed.AddManagedObject(promServiceName(ConfMonolith), owned.promService) - cmn.Managed.AddManagedObject(RoleBindingMonoName(ConfKafkaIngester), owned.roleBindingIn) - cmn.Managed.AddManagedObject(RoleBindingMonoName(ConfKafkaTransformer), owned.roleBindingTr) - cmn.Managed.AddManagedObject(configMapName(ConfMonolith), owned.configMap) + rec := monolithReconciler{ + Instance: cmn, + daemonSet: cmn.Managed.NewDaemonSet(name), + promService: cmn.Managed.NewService(promServiceName(ConfMonolith)), + serviceAccount: cmn.Managed.NewServiceAccount(name), + configMap: cmn.Managed.NewConfigMap(configMapName(ConfMonolith)), + roleBindingIn: cmn.Managed.NewCRB(RoleBindingMonoName(ConfKafkaIngester)), + roleBindingTr: cmn.Managed.NewCRB(RoleBindingMonoName(ConfKafkaTransformer)), + } if cmn.AvailableAPIs.HasSvcMonitor() { - cmn.Managed.AddManagedObject(serviceMonitorName(ConfMonolith), owned.serviceMonitor) + rec.serviceMonitor = cmn.Managed.NewServiceMonitor(serviceMonitorName(ConfMonolith)) } if cmn.AvailableAPIs.HasPromRule() { - cmn.Managed.AddManagedObject(prometheusRuleName(ConfMonolith), owned.prometheusRule) - } - - return &flpMonolithReconciler{ - Instance: cmn, - owned: owned, + rec.prometheusRule = cmn.Managed.NewPrometheusRule(prometheusRuleName(ConfMonolith)) } + return &rec } -func (r *flpMonolithReconciler) context(ctx context.Context) context.Context { - l := log.FromContext(ctx).WithValues(contextReconcilerName, "monolith") +func (r *monolithReconciler) context(ctx context.Context) context.Context { + l := log.FromContext(ctx).WithName("monolith") return log.IntoContext(ctx, l) } // cleanupNamespace cleans up old namespace -func (r *flpMonolithReconciler) cleanupNamespace(ctx context.Context) { +func (r *monolithReconciler) cleanupNamespace(ctx context.Context) { r.Managed.CleanupPreviousNamespace(ctx) } -func (r *flpMonolithReconciler) reconcile(ctx context.Context, desired *flowslatest.FlowCollector) error { +func (r *monolithReconciler) getStatus() *status.Instance { + return &r.Status +} + +func (r *monolithReconciler) reconcile(ctx context.Context, desired *flowslatest.FlowCollector) error { // Retrieve current owned objects err := r.Managed.FetchAll(ctx) if err != nil { return err } - // Monolith only used without Kafka if helper.UseKafka(&desired.Spec) { + r.Status.SetUnused("Monolith only used without Kafka") r.Managed.TryDeleteAll(ctx) return nil } + r.Status.SetReady() // will be overidden if necessary, as error or pending + builder, err := newMonolithBuilder(r.Instance, &desired.Spec) if err != nil { return err @@ -98,12 +89,12 @@ func (r *flpMonolithReconciler) reconcile(ctx context.Context, desired *flowslat annotations := map[string]string{ constants.PodConfigurationDigest: configDigest, } - if !r.Managed.Exists(r.owned.configMap) { + if !r.Managed.Exists(r.configMap) { if err := r.CreateOwned(ctx, newCM); err != nil { return err } - } else if !equality.Semantic.DeepDerivative(newCM.Data, r.owned.configMap.Data) { - if err := r.UpdateIfOwned(ctx, r.owned.configMap, newCM); err != nil { + } else if !equality.Semantic.DeepDerivative(newCM.Data, r.configMap.Data) { + if err := r.UpdateIfOwned(ctx, r.configMap, newCM); err != nil { return err } } @@ -136,45 +127,44 @@ func (r *flpMonolithReconciler) reconcile(ctx context.Context, desired *flowslat return r.reconcileDaemonSet(ctx, builder.daemonSet(annotations)) } -func (r *flpMonolithReconciler) reconcilePrometheusService(ctx context.Context, builder *monolithBuilder) error { +func (r *monolithReconciler) reconcilePrometheusService(ctx context.Context, builder *monolithBuilder) error { report := helper.NewChangeReport("FLP prometheus service") defer report.LogIfNeeded(ctx) - if err := r.ReconcileService(ctx, r.owned.promService, builder.promService(), &report); err != nil { + if err := r.ReconcileService(ctx, r.promService, builder.promService(), &report); err != nil { return err } if r.AvailableAPIs.HasSvcMonitor() { serviceMonitor := builder.generic.serviceMonitor() - if err := reconcilers.GenericReconcile(ctx, r.Managed, &r.Client, r.owned.serviceMonitor, serviceMonitor, &report, helper.ServiceMonitorChanged); err != nil { + if err := reconcilers.GenericReconcile(ctx, r.Managed, &r.Client, r.serviceMonitor, serviceMonitor, &report, helper.ServiceMonitorChanged); err != nil { return err } } if r.AvailableAPIs.HasPromRule() { promRules := builder.generic.prometheusRule() - if err := reconcilers.GenericReconcile(ctx, r.Managed, &r.Client, r.owned.prometheusRule, promRules, &report, helper.PrometheusRuleChanged); err != nil { + if err := reconcilers.GenericReconcile(ctx, r.Managed, &r.Client, r.prometheusRule, promRules, &report, helper.PrometheusRuleChanged); err != nil { return err } } return nil } -func (r *flpMonolithReconciler) reconcileDaemonSet(ctx context.Context, desiredDS *appsv1.DaemonSet) error { +func (r *monolithReconciler) reconcileDaemonSet(ctx context.Context, desiredDS *appsv1.DaemonSet) error { report := helper.NewChangeReport("FLP DaemonSet") defer report.LogIfNeeded(ctx) - if !r.Managed.Exists(r.owned.daemonSet) { - return r.CreateOwned(ctx, desiredDS) - } else if helper.PodChanged(&r.owned.daemonSet.Spec.Template, &desiredDS.Spec.Template, constants.FLPName, &report) { - return r.UpdateIfOwned(ctx, r.owned.daemonSet, desiredDS) - } else { - // DaemonSet up to date, check if it's ready - r.CheckDaemonSetInProgress(r.owned.daemonSet) - } - return nil + return reconcilers.ReconcileDaemonSet( + ctx, + r.Instance, + r.daemonSet, + desiredDS, + constants.FLPName, + &report, + ) } -func (r *flpMonolithReconciler) reconcilePermissions(ctx context.Context, builder *monolithBuilder) error { - if !r.Managed.Exists(r.owned.serviceAccount) { +func (r *monolithReconciler) reconcilePermissions(ctx context.Context, builder *monolithBuilder) error { + if !r.Managed.Exists(r.serviceAccount) { return r.CreateOwned(ctx, builder.serviceAccount()) } // We only configure name, update is not needed for now diff --git a/controllers/flowlogspipeline/flp_pipeline_builder.go b/controllers/flp/flp_pipeline_builder.go similarity index 98% rename from controllers/flowlogspipeline/flp_pipeline_builder.go rename to controllers/flp/flp_pipeline_builder.go index d12e5e158..0bae6434f 100644 --- a/controllers/flowlogspipeline/flp_pipeline_builder.go +++ b/controllers/flp/flp_pipeline_builder.go @@ -1,4 +1,4 @@ -package flowlogspipeline +package flp import ( "fmt" @@ -11,7 +11,6 @@ import ( flowslatest "github.com/netobserv/network-observability-operator/api/v1beta2" "github.com/netobserv/network-observability-operator/controllers/constants" - "github.com/netobserv/network-observability-operator/controllers/globals" "github.com/netobserv/network-observability-operator/pkg/filters" "github.com/netobserv/network-observability-operator/pkg/helper" "github.com/netobserv/network-observability-operator/pkg/metrics" @@ -27,14 +26,16 @@ const ( type PipelineBuilder struct { *config.PipelineBuilderStage - desired *flowslatest.FlowCollectorSpec - volumes *volumes.Builder - loki *helper.LokiConfig + desired *flowslatest.FlowCollectorSpec + volumes *volumes.Builder + loki *helper.LokiConfig + clusterID string } func newPipelineBuilder( desired *flowslatest.FlowCollectorSpec, loki *helper.LokiConfig, + clusterID string, volumes *volumes.Builder, pipeline *config.PipelineBuilderStage, ) PipelineBuilder { @@ -42,6 +43,7 @@ func newPipelineBuilder( PipelineBuilderStage: pipeline, desired: desired, loki: loki, + clusterID: clusterID, volumes: volumes, } } @@ -326,7 +328,7 @@ func (b *PipelineBuilder) addTransformFilter(lastStage config.PipelineBuilderSta clusterName = b.desired.Processor.ClusterName } else { //take clustername from openshift - clusterName = string(globals.DefaultClusterID) + clusterName = string(b.clusterID) } if clusterName != "" { transformFilterRules = []api.TransformFilterRule{ diff --git a/controllers/flowlogspipeline/flp_test.go b/controllers/flp/flp_test.go similarity index 96% rename from controllers/flowlogspipeline/flp_test.go rename to controllers/flp/flp_test.go index 492cb565f..47a8eb0e5 100644 --- a/controllers/flowlogspipeline/flp_test.go +++ b/controllers/flp/flp_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package flowlogspipeline +package flp import ( "encoding/json" @@ -37,6 +37,7 @@ import ( "github.com/netobserv/network-observability-operator/controllers/constants" "github.com/netobserv/network-observability-operator/controllers/reconcilers" "github.com/netobserv/network-observability-operator/pkg/helper" + "github.com/netobserv/network-observability-operator/pkg/manager/status" ) var resources = corev1.ResourceRequirements{ @@ -189,14 +190,14 @@ func getAutoScalerSpecs() (ascv2.HorizontalPodAutoscaler, flowslatest.FlowCollec func monoBuilder(ns string, cfg *flowslatest.FlowCollectorSpec) monolithBuilder { loki := helper.NewLokiConfig(&cfg.Loki, "any") info := reconcilers.Common{Namespace: ns, Loki: &loki} - b, _ := newMonolithBuilder(info.NewInstance(image), cfg) + b, _ := newMonolithBuilder(info.NewInstance(image, status.Instance{}), cfg) return b } func transfBuilder(ns string, cfg *flowslatest.FlowCollectorSpec) transfoBuilder { loki := helper.NewLokiConfig(&cfg.Loki, "any") info := reconcilers.Common{Namespace: ns, Loki: &loki} - b, _ := newTransfoBuilder(info.NewInstance(image), cfg) + b, _ := newTransfoBuilder(info.NewInstance(image, status.Instance{}), cfg) return b } @@ -369,7 +370,7 @@ func TestDeploymentNoChange(t *testing.T) { second := b.deployment(annotate(digest)) report := helper.NewChangeReport("") - assert.False(helper.DeploymentChanged(first, second, constants.FLPName, helper.HPADisabled(&cfg.Processor.KafkaConsumerAutoscaler), *cfg.Processor.KafkaConsumerReplicas, &report)) + assert.False(helper.DeploymentChanged(first, second, constants.FLPName, !helper.HPAEnabled(&cfg.Processor.KafkaConsumerAutoscaler), *cfg.Processor.KafkaConsumerReplicas, &report)) assert.Contains(report.String(), "no change") } @@ -393,7 +394,7 @@ func TestDeploymentChanged(t *testing.T) { report := helper.NewChangeReport("") checkChanged := func(old, new *appsv1.Deployment, spec flowslatest.FlowCollectorSpec) bool { - return helper.DeploymentChanged(old, new, constants.FLPName, helper.HPADisabled(&spec.Processor.KafkaConsumerAutoscaler), *spec.Processor.KafkaConsumerReplicas, &report) + return helper.DeploymentChanged(old, new, constants.FLPName, !helper.HPAEnabled(&spec.Processor.KafkaConsumerAutoscaler), *spec.Processor.KafkaConsumerReplicas, &report) } assert.True(checkChanged(first, second, cfg)) @@ -474,7 +475,7 @@ func TestDeploymentChangedReplicasNoHPA(t *testing.T) { second := b.deployment(annotate(digest)) report := helper.NewChangeReport("") - assert.True(helper.DeploymentChanged(first, second, constants.FLPName, helper.HPADisabled(&cfg2.Processor.KafkaConsumerAutoscaler), *cfg2.Processor.KafkaConsumerReplicas, &report)) + assert.True(helper.DeploymentChanged(first, second, constants.FLPName, !helper.HPAEnabled(&cfg2.Processor.KafkaConsumerAutoscaler), *cfg2.Processor.KafkaConsumerReplicas, &report)) assert.Contains(report.String(), "Replicas changed") } @@ -571,7 +572,7 @@ func TestServiceMonitorChanged(t *testing.T) { // Check labels change info := reconcilers.Common{Namespace: "namespace2"} - b, _ = newMonolithBuilder(info.NewInstance(image2), &cfg) + b, _ = newMonolithBuilder(info.NewInstance(image2, status.Instance{}), &cfg) third := b.generic.serviceMonitor() report = helper.NewChangeReport("") @@ -579,7 +580,7 @@ func TestServiceMonitorChanged(t *testing.T) { assert.Contains(report.String(), "ServiceMonitor labels changed") // Check scheme changed - b, _ = newMonolithBuilder(info.NewInstance(image2), &cfg) + b, _ = newMonolithBuilder(info.NewInstance(image2, status.Instance{}), &cfg) fourth := b.generic.serviceMonitor() fourth.Spec.Endpoints[0].Scheme = "https" @@ -624,7 +625,7 @@ func TestPrometheusRuleChanged(t *testing.T) { // Check labels change info := reconcilers.Common{Namespace: "namespace2"} - b, _ = newMonolithBuilder(info.NewInstance(image2), &cfg) + b, _ = newMonolithBuilder(info.NewInstance(image2, status.Instance{}), &cfg) third := b.generic.prometheusRule() report = helper.NewChangeReport("") @@ -765,9 +766,9 @@ func TestLabels(t *testing.T) { cfg := getConfig() info := reconcilers.Common{Namespace: "ns"} - builder, _ := newMonolithBuilder(info.NewInstance(image), &cfg) - tBuilder, _ := newTransfoBuilder(info.NewInstance(image), &cfg) - iBuilder, _ := newIngestBuilder(info.NewInstance(image), &cfg) + builder, _ := newMonolithBuilder(info.NewInstance(image, status.Instance{}), &cfg) + tBuilder, _ := newTransfoBuilder(info.NewInstance(image, status.Instance{}), &cfg) + iBuilder, _ := newIngestBuilder(info.NewInstance(image, status.Instance{}), &cfg) // Deployment depl := tBuilder.deployment(annotate("digest")) @@ -850,7 +851,7 @@ func TestPipelineConfig(t *testing.T) { // Kafka Ingester cfg.DeploymentModel = flowslatest.DeploymentModelKafka info := reconcilers.Common{Namespace: ns} - bi, _ := newIngestBuilder(info.NewInstance(image), &cfg) + bi, _ := newIngestBuilder(info.NewInstance(image, status.Instance{}), &cfg) cm, _, err = bi.configMap() assert.NoError(err) _, pipeline = validatePipelineConfig(t, cm) diff --git a/controllers/flowlogspipeline/flp_transfo_objects.go b/controllers/flp/flp_transfo_objects.go similarity index 99% rename from controllers/flowlogspipeline/flp_transfo_objects.go rename to controllers/flp/flp_transfo_objects.go index d576d2c52..02fc15fdf 100644 --- a/controllers/flowlogspipeline/flp_transfo_objects.go +++ b/controllers/flp/flp_transfo_objects.go @@ -1,4 +1,4 @@ -package flowlogspipeline +package flp import ( appsv1 "k8s.io/api/apps/v1" diff --git a/controllers/flp/flp_transfo_reconciler.go b/controllers/flp/flp_transfo_reconciler.go new file mode 100644 index 000000000..08625703d --- /dev/null +++ b/controllers/flp/flp_transfo_reconciler.go @@ -0,0 +1,205 @@ +package flp + +import ( + "context" + + monitoringv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1" + appsv1 "k8s.io/api/apps/v1" + ascv2 "k8s.io/api/autoscaling/v2" + corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + "k8s.io/apimachinery/pkg/api/equality" + "sigs.k8s.io/controller-runtime/pkg/log" + + flowslatest "github.com/netobserv/network-observability-operator/api/v1beta2" + "github.com/netobserv/network-observability-operator/controllers/constants" + "github.com/netobserv/network-observability-operator/controllers/reconcilers" + "github.com/netobserv/network-observability-operator/pkg/helper" + "github.com/netobserv/network-observability-operator/pkg/manager/status" +) + +type transformerReconciler struct { + *reconcilers.Instance + deployment *appsv1.Deployment + promService *corev1.Service + hpa *ascv2.HorizontalPodAutoscaler + serviceAccount *corev1.ServiceAccount + configMap *corev1.ConfigMap + roleBinding *rbacv1.ClusterRoleBinding + serviceMonitor *monitoringv1.ServiceMonitor + prometheusRule *monitoringv1.PrometheusRule +} + +func newTransformerReconciler(cmn *reconcilers.Instance) *transformerReconciler { + name := name(ConfKafkaTransformer) + rec := transformerReconciler{ + Instance: cmn, + deployment: cmn.Managed.NewDeployment(name), + promService: cmn.Managed.NewService(promServiceName(ConfKafkaTransformer)), + hpa: cmn.Managed.NewHPA(name), + serviceAccount: cmn.Managed.NewServiceAccount(name), + configMap: cmn.Managed.NewConfigMap(configMapName(ConfKafkaTransformer)), + roleBinding: cmn.Managed.NewCRB(RoleBindingName(ConfKafkaTransformer)), + } + if cmn.AvailableAPIs.HasSvcMonitor() { + rec.serviceMonitor = cmn.Managed.NewServiceMonitor(serviceMonitorName(ConfKafkaTransformer)) + } + if cmn.AvailableAPIs.HasPromRule() { + rec.prometheusRule = cmn.Managed.NewPrometheusRule(prometheusRuleName(ConfKafkaTransformer)) + } + return &rec +} + +func (r *transformerReconciler) context(ctx context.Context) context.Context { + l := log.FromContext(ctx).WithName("transformer") + return log.IntoContext(ctx, l) +} + +// cleanupNamespace cleans up old namespace +func (r *transformerReconciler) cleanupNamespace(ctx context.Context) { + r.Managed.CleanupPreviousNamespace(ctx) +} + +func (r *transformerReconciler) getStatus() *status.Instance { + return &r.Status +} + +func (r *transformerReconciler) reconcile(ctx context.Context, desired *flowslatest.FlowCollector) error { + // Retrieve current owned objects + err := r.Managed.FetchAll(ctx) + if err != nil { + return err + } + + if !helper.UseKafka(&desired.Spec) { + r.Status.SetUnused("Transformer only used with Kafka") + r.Managed.TryDeleteAll(ctx) + return nil + } + + r.Status.SetReady() // will be overidden if necessary, as error or pending + + builder, err := newTransfoBuilder(r.Instance, &desired.Spec) + if err != nil { + return err + } + newCM, configDigest, err := builder.configMap() + if err != nil { + return err + } + annotations := map[string]string{ + constants.PodConfigurationDigest: configDigest, + } + if !r.Managed.Exists(r.configMap) { + if err := r.CreateOwned(ctx, newCM); err != nil { + return err + } + } else if !equality.Semantic.DeepDerivative(newCM.Data, r.configMap.Data) { + if err := r.UpdateIfOwned(ctx, r.configMap, newCM); err != nil { + return err + } + } + if err := r.reconcilePermissions(ctx, &builder); err != nil { + return err + } + + err = r.reconcilePrometheusService(ctx, &builder) + if err != nil { + return err + } + + // Watch for Loki certificate if necessary; we'll ignore in that case the returned digest, as we don't need to restart pods on cert rotation + // because certificate is always reloaded from file + if _, err = r.Watcher.ProcessCACert(ctx, r.Client, &r.Loki.TLS, r.Namespace); err != nil { + return err + } + + // Watch for Kafka certificate if necessary; need to restart pods in case of cert rotation + if err = annotateKafkaCerts(ctx, r.Common, &desired.Spec.Kafka, "kafka", annotations); err != nil { + return err + } + // Same for Kafka exporters + if err = annotateKafkaExporterCerts(ctx, r.Common, desired.Spec.Exporters, annotations); err != nil { + return err + } + // Watch for monitoring caCert + if err = reconcileMonitoringCerts(ctx, r.Common, &desired.Spec.Processor.Metrics.Server.TLS, r.Namespace); err != nil { + return err + } + + if err = r.reconcileDeployment(ctx, &desired.Spec.Processor, &builder, annotations); err != nil { + return err + } + + return r.reconcileHPA(ctx, &desired.Spec.Processor, &builder) +} + +func (r *transformerReconciler) reconcileDeployment(ctx context.Context, desiredFLP *flowslatest.FlowCollectorFLP, builder *transfoBuilder, annotations map[string]string) error { + report := helper.NewChangeReport("FLP Deployment") + defer report.LogIfNeeded(ctx) + + return reconcilers.ReconcileDeployment( + ctx, + r.Instance, + r.deployment, + builder.deployment(annotations), + constants.FLPName, + helper.PtrInt32(desiredFLP.KafkaConsumerReplicas), + &desiredFLP.KafkaConsumerAutoscaler, + &report, + ) +} + +func (r *transformerReconciler) reconcileHPA(ctx context.Context, desiredFLP *flowslatest.FlowCollectorFLP, builder *transfoBuilder) error { + report := helper.NewChangeReport("FLP autoscaler") + defer report.LogIfNeeded(ctx) + + return reconcilers.ReconcileHPA( + ctx, + r.Instance, + r.hpa, + builder.autoScaler(), + &desiredFLP.KafkaConsumerAutoscaler, + &report, + ) +} + +func (r *transformerReconciler) reconcilePrometheusService(ctx context.Context, builder *transfoBuilder) error { + report := helper.NewChangeReport("FLP prometheus service") + defer report.LogIfNeeded(ctx) + + if err := r.ReconcileService(ctx, r.promService, builder.promService(), &report); err != nil { + return err + } + if r.AvailableAPIs.HasSvcMonitor() { + serviceMonitor := builder.generic.serviceMonitor() + if err := reconcilers.GenericReconcile(ctx, r.Managed, &r.Client, r.serviceMonitor, serviceMonitor, &report, helper.ServiceMonitorChanged); err != nil { + return err + } + } + if r.AvailableAPIs.HasPromRule() { + promRules := builder.generic.prometheusRule() + if err := reconcilers.GenericReconcile(ctx, r.Managed, &r.Client, r.prometheusRule, promRules, &report, helper.PrometheusRuleChanged); err != nil { + return err + } + } + return nil +} + +func (r *transformerReconciler) reconcilePermissions(ctx context.Context, builder *transfoBuilder) error { + if !r.Managed.Exists(r.serviceAccount) { + return r.CreateOwned(ctx, builder.serviceAccount()) + } // We only configure name, update is not needed for now + + cr := BuildClusterRoleTransformer() + if err := r.ReconcileClusterRole(ctx, cr); err != nil { + return err + } + + desired := builder.clusterRoleBinding() + if err := r.ReconcileClusterRoleBinding(ctx, desired); err != nil { + return err + } + + return reconcileLokiRoles(ctx, r.Common, &builder.generic) +} diff --git a/controllers/globals/globals.go b/controllers/globals/globals.go deleted file mode 100644 index 995c74c71..000000000 --- a/controllers/globals/globals.go +++ /dev/null @@ -1,6 +0,0 @@ -// Package globals defines some variables that are shared across multiple packages -package globals - -import "github.com/openshift/api/config/v1" - -var DefaultClusterID v1.ClusterID diff --git a/controllers/monitoring/monitoring_controller.go b/controllers/monitoring/monitoring_controller.go new file mode 100644 index 000000000..0b5dfd376 --- /dev/null +++ b/controllers/monitoring/monitoring_controller.go @@ -0,0 +1,133 @@ +package monitoring + +import ( + "context" + "fmt" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" + + flowslatest "github.com/netobserv/network-observability-operator/api/v1beta2" + "github.com/netobserv/network-observability-operator/controllers/reconcilers" + "github.com/netobserv/network-observability-operator/pkg/helper" + "github.com/netobserv/network-observability-operator/pkg/manager" + "github.com/netobserv/network-observability-operator/pkg/manager/status" +) + +type Reconciler struct { + client.Client + mgr *manager.Manager + status status.Instance +} + +func Start(ctx context.Context, mgr *manager.Manager) error { + log := log.FromContext(ctx) + log.Info("Starting Monitoring controller") + r := Reconciler{ + Client: mgr.Client, + mgr: mgr, + status: mgr.Status.ForComponent(status.Monitoring), + } + return ctrl.NewControllerManagedBy(mgr). + For(&flowslatest.FlowCollector{}, reconcilers.IgnoreStatusChange). + Named("monitoring"). + Owns(&corev1.Namespace{}). + Complete(&r) +} + +// Reconcile is the controller entry point for reconciling current state with desired state. +// It manages the controller status at a high level. Business logic is delegated into `reconcile`. +func (r *Reconciler) Reconcile(ctx context.Context, _ ctrl.Request) (ctrl.Result, error) { + l := log.Log.WithName("monitoring") // clear context (too noisy) + ctx = log.IntoContext(ctx, l) + + r.status.SetUnknown() + defer r.status.Commit(ctx, r.Client) + + err := r.reconcile(ctx) + if err != nil { + l.Error(err, "Monitoring reconcile failure") + // Set status failure unless it was already set + if !r.status.HasFailure() { + r.status.SetFailure("MonitoringError", err.Error()) + } + return ctrl.Result{}, err + } + + r.status.SetReady() + return ctrl.Result{}, nil +} + +func (r *Reconciler) reconcile(ctx context.Context) error { + clh, desired, err := helper.NewFlowCollectorClientHelper(ctx, r.Client) + if err != nil { + return fmt.Errorf("failed to get FlowCollector: %w", err) + } else if desired == nil { + return nil + } + + ns := helper.GetNamespace(&desired.Spec) + + // If namespace does not exist, we create it + nsExist, err := r.namespaceExist(ctx, ns) + if err != nil { + return err + } + desiredNs := buildNamespace(ns, r.mgr.Config.DownstreamDeployment) + if nsExist == nil { + err = r.Create(ctx, desiredNs) + if err != nil { + return err + } + } else if !helper.IsSubSet(nsExist.ObjectMeta.Labels, desiredNs.ObjectMeta.Labels) { + err = r.Update(ctx, desiredNs) + if err != nil { + return err + } + } + if r.mgr.Config.DownstreamDeployment { + desiredRole := buildRoleMonitoringReader() + if err := reconcilers.ReconcileClusterRole(ctx, clh, desiredRole); err != nil { + return err + } + desiredBinding := buildRoleBindingMonitoringReader(ns) + if err := reconcilers.ReconcileClusterRoleBinding(ctx, clh, desiredBinding); err != nil { + return err + } + } + + if r.mgr.HasSvcMonitor() { + names := helper.GetIncludeList(&desired.Spec) + desiredFlowDashboardCM, del, err := buildFlowMetricsDashboard(ns, names) + if err != nil { + return err + } else if err = reconcilers.ReconcileConfigMap(ctx, clh, desiredFlowDashboardCM, del); err != nil { + return err + } + + desiredHealthDashboardCM, del, err := buildHealthDashboard(ns, names) + if err != nil { + return err + } else if err = reconcilers.ReconcileConfigMap(ctx, clh, desiredHealthDashboardCM, del); err != nil { + return err + } + } + return nil +} + +func (r *Reconciler) namespaceExist(ctx context.Context, nsName string) (*corev1.Namespace, error) { + ns := &corev1.Namespace{} + err := r.Get(ctx, types.NamespacedName{Name: nsName}, ns) + if err != nil { + if errors.IsNotFound(err) { + return nil, nil + } + log.FromContext(ctx).Error(err, "Failed to get namespace") + return nil, err + } + return ns, nil +} diff --git a/controllers/flowcollector_objects.go b/controllers/monitoring/monitoring_objects.go similarity index 98% rename from controllers/flowcollector_objects.go rename to controllers/monitoring/monitoring_objects.go index 3075e3bce..70019fce9 100644 --- a/controllers/flowcollector_objects.go +++ b/controllers/monitoring/monitoring_objects.go @@ -1,4 +1,4 @@ -package controllers +package monitoring import ( "github.com/netobserv/network-observability-operator/controllers/constants" @@ -41,7 +41,8 @@ func buildRoleMonitoringReader() *rbacv1.ClusterRole { Name: constants.OperatorName + roleSuffix, }, Rules: []rbacv1.PolicyRule{ - {APIGroups: []string{""}, + { + APIGroups: []string{""}, Verbs: []string{"get", "list", "watch"}, Resources: []string{"pods", "services", "endpoints"}, }, diff --git a/controllers/reconcilers/common.go b/controllers/reconcilers/common.go index 0c87d34ce..3b5111877 100644 --- a/controllers/reconcilers/common.go +++ b/controllers/reconcilers/common.go @@ -2,19 +2,14 @@ package reconcilers import ( "context" - "fmt" - "reflect" "github.com/netobserv/network-observability-operator/controllers/constants" "github.com/netobserv/network-observability-operator/pkg/discover" "github.com/netobserv/network-observability-operator/pkg/helper" + "github.com/netobserv/network-observability-operator/pkg/manager/status" "github.com/netobserv/network-observability-operator/pkg/watchers" corev1 "k8s.io/api/core/v1" rbacv1 "k8s.io/api/rbac/v1" - "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/types" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/log" ) type Common struct { @@ -25,6 +20,7 @@ type Common struct { UseOpenShiftSCC bool AvailableAPIs *discover.AvailableAPIs Loki *helper.LokiConfig + ClusterID string } func (c *Common) PrivilegedNamespace() string { @@ -39,155 +35,39 @@ type Instance struct { *Common Managed *NamespacedObjectManager Image string + Status status.Instance } -func (c *Common) NewInstance(image string) *Instance { +func (c *Common) NewInstance(image string, st status.Instance) *Instance { managed := NewNamespacedObjectManager(c) return &Instance{ Common: c, Managed: managed, Image: image, + Status: st, } } func (c *Common) ReconcileClusterRoleBinding(ctx context.Context, desired *rbacv1.ClusterRoleBinding) error { - actual := rbacv1.ClusterRoleBinding{} - if err := c.Get(ctx, types.NamespacedName{Name: desired.ObjectMeta.Name}, &actual); err != nil { - if errors.IsNotFound(err) { - return c.CreateOwned(ctx, desired) - } - return fmt.Errorf("can't reconcile ClusterRoleBinding %s: %w", desired.Name, err) - } - if helper.IsSubSet(actual.Labels, desired.Labels) && - actual.RoleRef == desired.RoleRef && - reflect.DeepEqual(actual.Subjects, desired.Subjects) { - if actual.RoleRef != desired.RoleRef { - //Roleref cannot be updated deleting and creating a new rolebinding - log := log.FromContext(ctx) - log.Info("Deleting old ClusterRoleBinding", "Namespace", actual.GetNamespace(), "Name", actual.GetName()) - err := c.Delete(ctx, &actual) - if err != nil { - log.Error(err, "error deleting old ClusterRoleBinding", "Namespace", actual.GetNamespace(), "Name", actual.GetName()) - } - return c.CreateOwned(ctx, desired) - } - // cluster role binding already reconciled. Exiting - return nil - } - return c.UpdateIfOwned(ctx, &actual, desired) + return ReconcileClusterRoleBinding(ctx, &c.Client, desired) } func (c *Common) ReconcileRoleBinding(ctx context.Context, desired *rbacv1.RoleBinding) error { - actual := rbacv1.RoleBinding{} - if err := c.Get(ctx, types.NamespacedName{Name: desired.ObjectMeta.Name}, &actual); err != nil { - if errors.IsNotFound(err) { - return c.CreateOwned(ctx, desired) - } - return fmt.Errorf("can't reconcile RoleBinding %s: %w", desired.Name, err) - } - if helper.IsSubSet(actual.Labels, desired.Labels) && - actual.RoleRef == desired.RoleRef && - reflect.DeepEqual(actual.Subjects, desired.Subjects) { - if actual.RoleRef != desired.RoleRef { - //Roleref cannot be updated deleting and creating a new rolebinding - log := log.FromContext(ctx) - log.Info("Deleting old RoleBinding", "Namespace", actual.GetNamespace(), "Name", actual.GetName()) - err := c.Delete(ctx, &actual) - if err != nil { - log.Error(err, "error deleting old RoleBinding", "Namespace", actual.GetNamespace(), "Name", actual.GetName()) - } - return c.CreateOwned(ctx, desired) - } - // role binding already reconciled. Exiting - return nil - } - return c.UpdateIfOwned(ctx, &actual, desired) + return ReconcileRoleBinding(ctx, &c.Client, desired) } func (c *Common) ReconcileClusterRole(ctx context.Context, desired *rbacv1.ClusterRole) error { - actual := rbacv1.ClusterRole{} - if err := c.Get(ctx, types.NamespacedName{Name: desired.Name}, &actual); err != nil { - if errors.IsNotFound(err) { - return c.CreateOwned(ctx, desired) - } - return fmt.Errorf("can't reconcile ClusterRole %s: %w", desired.Name, err) - } - - if helper.IsSubSet(actual.Labels, desired.Labels) && - reflect.DeepEqual(actual.Rules, desired.Rules) { - // cluster role already reconciled. Exiting - return nil - } - - return c.UpdateIfOwned(ctx, &actual, desired) + return ReconcileClusterRole(ctx, &c.Client, desired) } func (c *Common) ReconcileRole(ctx context.Context, desired *rbacv1.Role) error { - actual := rbacv1.Role{} - if err := c.Get(ctx, types.NamespacedName{Name: desired.Name}, &actual); err != nil { - if errors.IsNotFound(err) { - return c.CreateOwned(ctx, desired) - } - return fmt.Errorf("can't reconcile Role %s: %w", desired.Name, err) - } - - if helper.IsSubSet(actual.Labels, desired.Labels) && - reflect.DeepEqual(actual.Rules, desired.Rules) { - // role already reconciled. Exiting - return nil - } - - return c.UpdateIfOwned(ctx, &actual, desired) + return ReconcileRole(ctx, &c.Client, desired) } func (c *Common) ReconcileConfigMap(ctx context.Context, desired *corev1.ConfigMap, delete bool) error { - actual := corev1.ConfigMap{} - if err := c.Get(ctx, types.NamespacedName{Name: desired.Name, Namespace: desired.Namespace}, &actual); err != nil { - if errors.IsNotFound(err) { - if delete { - return nil - } - return c.CreateOwned(ctx, desired) - } - return fmt.Errorf("can't reconcile Configmap %s: %w", desired.Name, err) - } - - if delete { - return c.Delete(ctx, desired) - } - - if helper.IsSubSet(actual.Labels, desired.Labels) && - reflect.DeepEqual(actual.Data, desired.Data) { - // configmap already reconciled. Exiting - return nil - } - - return c.UpdateIfOwned(ctx, &actual, desired) + return ReconcileConfigMap(ctx, &c.Client, desired, delete) } func (i *Instance) ReconcileService(ctx context.Context, old, new *corev1.Service, report *helper.ChangeReport) error { - if !i.Managed.Exists(old) { - if err := i.CreateOwned(ctx, new); err != nil { - return err - } - } else if helper.ServiceChanged(old, new, report) { - // In case we're updating an existing service, we need to build from the old one to keep immutable fields such as clusterIP - newSVC := old.DeepCopy() - newSVC.Spec.Ports = new.Spec.Ports - newSVC.ObjectMeta.Annotations = new.ObjectMeta.Annotations - if err := i.UpdateIfOwned(ctx, old, newSVC); err != nil { - return err - } - } - return nil -} - -func GenericReconcile[K client.Object](ctx context.Context, m *NamespacedObjectManager, cl *helper.Client, old, new K, report *helper.ChangeReport, changeFunc func(old, new K, report *helper.ChangeReport) bool) error { - if !m.Exists(old) { - return cl.CreateOwned(ctx, new) - } - if changeFunc(old, new, report) { - return cl.UpdateIfOwned(ctx, old, new) - } - return nil + return ReconcileService(ctx, i, old, new, report) } diff --git a/controllers/reconcilers/namespaced_objects_manager.go b/controllers/reconcilers/namespaced_objects_manager.go index bfce82903..d75f41107 100644 --- a/controllers/reconcilers/namespaced_objects_manager.go +++ b/controllers/reconcilers/namespaced_objects_manager.go @@ -5,6 +5,11 @@ import ( "reflect" "strings" + monitoringv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1" + appsv1 "k8s.io/api/apps/v1" + ascv2 "k8s.io/api/autoscaling/v2" + corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" @@ -45,6 +50,60 @@ func (m *NamespacedObjectManager) AddManagedObject(name string, placeholder clie }) } +func (m *NamespacedObjectManager) NewConfigMap(name string) *corev1.ConfigMap { + cm := corev1.ConfigMap{} + m.AddManagedObject(name, &cm) + return &cm +} + +func (m *NamespacedObjectManager) NewDeployment(name string) *appsv1.Deployment { + d := appsv1.Deployment{} + m.AddManagedObject(name, &d) + return &d +} + +func (m *NamespacedObjectManager) NewDaemonSet(name string) *appsv1.DaemonSet { + ds := appsv1.DaemonSet{} + m.AddManagedObject(name, &ds) + return &ds +} + +func (m *NamespacedObjectManager) NewService(name string) *corev1.Service { + s := corev1.Service{} + m.AddManagedObject(name, &s) + return &s +} + +func (m *NamespacedObjectManager) NewServiceAccount(name string) *corev1.ServiceAccount { + sa := corev1.ServiceAccount{} + m.AddManagedObject(name, &sa) + return &sa +} + +func (m *NamespacedObjectManager) NewHPA(name string) *ascv2.HorizontalPodAutoscaler { + hpa := ascv2.HorizontalPodAutoscaler{} + m.AddManagedObject(name, &hpa) + return &hpa +} + +func (m *NamespacedObjectManager) NewServiceMonitor(name string) *monitoringv1.ServiceMonitor { + sm := monitoringv1.ServiceMonitor{} + m.AddManagedObject(name, &sm) + return &sm +} + +func (m *NamespacedObjectManager) NewPrometheusRule(name string) *monitoringv1.PrometheusRule { + sm := monitoringv1.PrometheusRule{} + m.AddManagedObject(name, &sm) + return &sm +} + +func (m *NamespacedObjectManager) NewCRB(name string) *rbacv1.ClusterRoleBinding { + crb := rbacv1.ClusterRoleBinding{} + m.AddManagedObject(name, &crb) + return &crb +} + // FetchAll fetches all managed objects (registered using AddManagedObject) in the current namespace. // Placeholders are filled with fetched resources. Resources not found are flagged internally. func (m *NamespacedObjectManager) FetchAll(ctx context.Context) error { diff --git a/controllers/reconcilers/reconcilers.go b/controllers/reconcilers/reconcilers.go new file mode 100644 index 000000000..89e5a5db8 --- /dev/null +++ b/controllers/reconcilers/reconcilers.go @@ -0,0 +1,215 @@ +package reconcilers + +import ( + "context" + "fmt" + "reflect" + + flowslatest "github.com/netobserv/network-observability-operator/api/v1beta2" + "github.com/netobserv/network-observability-operator/pkg/helper" + appsv1 "k8s.io/api/apps/v1" + ascv2 "k8s.io/api/autoscaling/v2" + corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + "k8s.io/apimachinery/pkg/api/equality" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/builder" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/predicate" +) + +var ( + IgnoreStatusChange = builder.WithPredicates(predicate.Funcs{ + UpdateFunc: func(e event.UpdateEvent) bool { + // Update only if spec / annotations / labels change, ie. ignore status changes + return (e.ObjectOld.GetGeneration() != e.ObjectNew.GetGeneration()) || + !equality.Semantic.DeepEqual(e.ObjectNew.GetAnnotations(), e.ObjectOld.GetAnnotations()) || + !equality.Semantic.DeepEqual(e.ObjectNew.GetLabels(), e.ObjectOld.GetLabels()) + }, + CreateFunc: func(e event.CreateEvent) bool { return true }, + DeleteFunc: func(e event.DeleteEvent) bool { return true }, + GenericFunc: func(e event.GenericEvent) bool { return false }, + }) +) + +func ReconcileClusterRoleBinding(ctx context.Context, cl *helper.Client, desired *rbacv1.ClusterRoleBinding) error { + actual := rbacv1.ClusterRoleBinding{} + if err := cl.Get(ctx, types.NamespacedName{Name: desired.ObjectMeta.Name}, &actual); err != nil { + if errors.IsNotFound(err) { + return cl.CreateOwned(ctx, desired) + } + return fmt.Errorf("can't reconcile ClusterRoleBinding %s: %w", desired.Name, err) + } + if helper.IsSubSet(actual.Labels, desired.Labels) && + actual.RoleRef == desired.RoleRef && + reflect.DeepEqual(actual.Subjects, desired.Subjects) { + if actual.RoleRef != desired.RoleRef { + //Roleref cannot be updated deleting and creating a new rolebinding + log := log.FromContext(ctx) + log.Info("Deleting old ClusterRoleBinding", "Namespace", actual.GetNamespace(), "Name", actual.GetName()) + err := cl.Delete(ctx, &actual) + if err != nil { + log.Error(err, "error deleting old ClusterRoleBinding", "Namespace", actual.GetNamespace(), "Name", actual.GetName()) + } + return cl.CreateOwned(ctx, desired) + } + // cluster role binding already reconciled. Exiting + return nil + } + return cl.UpdateIfOwned(ctx, &actual, desired) +} + +func ReconcileRoleBinding(ctx context.Context, cl *helper.Client, desired *rbacv1.RoleBinding) error { + actual := rbacv1.RoleBinding{} + if err := cl.Get(ctx, types.NamespacedName{Name: desired.ObjectMeta.Name}, &actual); err != nil { + if errors.IsNotFound(err) { + return cl.CreateOwned(ctx, desired) + } + return fmt.Errorf("can't reconcile RoleBinding %s: %w", desired.Name, err) + } + if helper.IsSubSet(actual.Labels, desired.Labels) && + actual.RoleRef == desired.RoleRef && + reflect.DeepEqual(actual.Subjects, desired.Subjects) { + if actual.RoleRef != desired.RoleRef { + //Roleref cannot be updated deleting and creating a new rolebinding + log := log.FromContext(ctx) + log.Info("Deleting old RoleBinding", "Namespace", actual.GetNamespace(), "Name", actual.GetName()) + err := cl.Delete(ctx, &actual) + if err != nil { + log.Error(err, "error deleting old RoleBinding", "Namespace", actual.GetNamespace(), "Name", actual.GetName()) + } + return cl.CreateOwned(ctx, desired) + } + // role binding already reconciled. Exiting + return nil + } + return cl.UpdateIfOwned(ctx, &actual, desired) +} + +func ReconcileClusterRole(ctx context.Context, cl *helper.Client, desired *rbacv1.ClusterRole) error { + actual := rbacv1.ClusterRole{} + if err := cl.Get(ctx, types.NamespacedName{Name: desired.Name}, &actual); err != nil { + if errors.IsNotFound(err) { + return cl.CreateOwned(ctx, desired) + } + return fmt.Errorf("can't reconcile ClusterRole %s: %w", desired.Name, err) + } + + if helper.IsSubSet(actual.Labels, desired.Labels) && + reflect.DeepEqual(actual.Rules, desired.Rules) { + // cluster role already reconciled. Exiting + return nil + } + + return cl.UpdateIfOwned(ctx, &actual, desired) +} + +func ReconcileRole(ctx context.Context, cl *helper.Client, desired *rbacv1.Role) error { + actual := rbacv1.Role{} + if err := cl.Get(ctx, types.NamespacedName{Name: desired.Name}, &actual); err != nil { + if errors.IsNotFound(err) { + return cl.CreateOwned(ctx, desired) + } + return fmt.Errorf("can't reconcile Role %s: %w", desired.Name, err) + } + + if helper.IsSubSet(actual.Labels, desired.Labels) && + reflect.DeepEqual(actual.Rules, desired.Rules) { + // role already reconciled. Exiting + return nil + } + + return cl.UpdateIfOwned(ctx, &actual, desired) +} + +func ReconcileConfigMap(ctx context.Context, cl *helper.Client, desired *corev1.ConfigMap, delete bool) error { + actual := corev1.ConfigMap{} + if err := cl.Get(ctx, types.NamespacedName{Name: desired.Name, Namespace: desired.Namespace}, &actual); err != nil { + if errors.IsNotFound(err) { + if delete { + return nil + } + return cl.CreateOwned(ctx, desired) + } + return fmt.Errorf("can't reconcile Configmap %s: %w", desired.Name, err) + } + + if delete { + return cl.Delete(ctx, desired) + } + + if helper.IsSubSet(actual.Labels, desired.Labels) && + reflect.DeepEqual(actual.Data, desired.Data) { + // configmap already reconciled. Exiting + return nil + } + + return cl.UpdateIfOwned(ctx, &actual, desired) +} + +func ReconcileDaemonSet(ctx context.Context, ci *Instance, old, new *appsv1.DaemonSet, containerName string, report *helper.ChangeReport) error { + if !ci.Managed.Exists(old) { + ci.Status.SetCreatingDaemonSet(new) + return ci.CreateOwned(ctx, new) + } + ci.Status.CheckDaemonSetProgress(old) + if helper.PodChanged(&old.Spec.Template, &new.Spec.Template, containerName, report) { + return ci.UpdateIfOwned(ctx, old, new) + } + return nil +} + +func ReconcileDeployment(ctx context.Context, ci *Instance, old, new *appsv1.Deployment, containerName string, replicas int32, hpa *flowslatest.FlowCollectorHPA, report *helper.ChangeReport) error { + if !ci.Managed.Exists(old) { + ci.Status.SetCreatingDeployment(new) + return ci.CreateOwned(ctx, new) + } + ci.Status.CheckDeploymentProgress(old) + if helper.DeploymentChanged(old, new, containerName, !helper.HPAEnabled(hpa), replicas, report) { + return ci.UpdateIfOwned(ctx, old, new) + } + return nil +} + +func ReconcileHPA(ctx context.Context, ci *Instance, old, new *ascv2.HorizontalPodAutoscaler, desired *flowslatest.FlowCollectorHPA, report *helper.ChangeReport) error { + // Delete or Create / Update Autoscaler according to HPA option + if helper.HPAEnabled(desired) { + if !ci.Managed.Exists(old) { + return ci.CreateOwned(ctx, new) + } else if helper.AutoScalerChanged(old, *desired, report) { + return ci.UpdateIfOwned(ctx, old, new) + } + } + ci.Managed.TryDelete(ctx, old) + return nil +} + +func ReconcileService(ctx context.Context, ci *Instance, old, new *corev1.Service, report *helper.ChangeReport) error { + if !ci.Managed.Exists(old) { + if err := ci.CreateOwned(ctx, new); err != nil { + return err + } + } else if helper.ServiceChanged(old, new, report) { + // In case we're updating an existing service, we need to build from the old one to keep immutable fields such as clusterIP + newSVC := old.DeepCopy() + newSVC.Spec.Ports = new.Spec.Ports + newSVC.ObjectMeta.Annotations = new.ObjectMeta.Annotations + if err := ci.UpdateIfOwned(ctx, old, newSVC); err != nil { + return err + } + } + return nil +} + +func GenericReconcile[K client.Object](ctx context.Context, m *NamespacedObjectManager, cl *helper.Client, old, new K, report *helper.ChangeReport, changeFunc func(old, new K, report *helper.ChangeReport) bool) error { + if !m.Exists(old) { + return cl.CreateOwned(ctx, new) + } + if changeFunc(old, new, report) { + return cl.UpdateIfOwned(ctx, old, new) + } + return nil +} diff --git a/controllers/suite_test.go b/controllers/suite_test.go index bc9dea66d..5a758817d 100644 --- a/controllers/suite_test.go +++ b/controllers/suite_test.go @@ -18,7 +18,6 @@ package controllers import ( "context" - "net" "path/filepath" "testing" @@ -28,11 +27,9 @@ import ( osv1alpha1 "github.com/openshift/api/console/v1alpha1" operatorsv1 "github.com/openshift/api/operator/v1" monitoringv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1" - "github.com/stretchr/testify/mock" ascv2 "k8s.io/api/autoscaling/v2" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/kubernetes/scheme" apiregv1 "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1" ctrl "sigs.k8s.io/controller-runtime" @@ -40,12 +37,10 @@ import ( "sigs.k8s.io/controller-runtime/pkg/envtest" logf "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/log/zap" - "sigs.k8s.io/controller-runtime/pkg/manager" flowsv1beta1 "github.com/netobserv/network-observability-operator/api/v1beta1" flowsv1beta2 "github.com/netobserv/network-observability-operator/api/v1beta2" - "github.com/netobserv/network-observability-operator/controllers/operator" - "github.com/netobserv/network-observability-operator/pkg/narrowcache" + "github.com/netobserv/network-observability-operator/pkg/manager" //+kubebuilder:scaffold:imports ) @@ -57,12 +52,10 @@ const testCnoNamespace = "openshift-network-operator" var namespacesToPrepare = []string{testCnoNamespace, "openshift-config-managed", "loki-namespace", "kafka-exporter-namespace", "main-namespace", "main-namespace-privileged"} var ( - ctx context.Context - k8sManager manager.Manager - k8sClient client.Client - testEnv *envtest.Environment - cancel context.CancelFunc - ipResolver ipResolverMock + ctx context.Context + k8sClient client.Client + testEnv *envtest.Environment + cancel context.CancelFunc ) func TestAPIs(t *testing.T) { @@ -150,20 +143,23 @@ var _ = BeforeSuite(func() { Expect(prepareNamespaces()).NotTo(HaveOccurred()) - narrowCache := narrowcache.NewConfig(cfg, narrowcache.ConfigMaps, narrowcache.Secrets) - k8sManager, err = ctrl.NewManager(cfg, ctrl.Options{ - Scheme: scheme.Scheme, - Client: client.Options{Cache: narrowCache.ControllerRuntimeClientCacheOptions()}, - }) - Expect(err).ToNot(HaveOccurred()) - Expect(k8sManager).NotTo(BeNil()) - - client, err := narrowCache.CreateClient(k8sManager.GetClient()) - Expect(err).ToNot(HaveOccurred()) + k8sManager, err := manager.NewManager( + context.Background(), + cfg, + &manager.Config{ + EBPFAgentImage: "registry-proxy.engineering.redhat.com/rh-osbs/network-observability-ebpf-agent@sha256:6481481ba23375107233f8d0a4f839436e34e50c2ec550ead0a16c361ae6654e", + FlowlogsPipelineImage: "registry-proxy.engineering.redhat.com/rh-osbs/network-observability-flowlogs-pipeline@sha256:6481481ba23375107233f8d0a4f839436e34e50c2ec550ead0a16c361ae6654e", + ConsolePluginImage: "registry-proxy.engineering.redhat.com/rh-osbs/network-observability-console-plugin@sha256:6481481ba23375107233f8d0a4f839436e34e50c2ec550ead0a16c361ae6654e", + DownstreamDeployment: false, + }, + &ctrl.Options{ + Scheme: scheme.Scheme, + }, + Registerers, + ) - err = NewTestFlowCollectorReconciler(client, k8sManager.GetScheme()). - SetupWithManager(ctx, k8sManager) Expect(err).ToNot(HaveOccurred()) + Expect(k8sManager).NotTo(BeNil()) go func() { defer GinkgoRecover() @@ -191,28 +187,3 @@ func prepareNamespaces() error { } return nil } - -// NewTestFlowCollectorReconciler allows mocking the IP resolutor of a -// FlowCollectorReconciler -func NewTestFlowCollectorReconciler(client client.Client, scheme *runtime.Scheme) *FlowCollectorReconciler { - return &FlowCollectorReconciler{ - Client: client, - Scheme: scheme, - lookupIP: ipResolver.LookupIP, - config: &operator.Config{ - EBPFAgentImage: "registry-proxy.engineering.redhat.com/rh-osbs/network-observability-ebpf-agent@sha256:6481481ba23375107233f8d0a4f839436e34e50c2ec550ead0a16c361ae6654e", - FlowlogsPipelineImage: "registry-proxy.engineering.redhat.com/rh-osbs/network-observability-flowlogs-pipeline@sha256:6481481ba23375107233f8d0a4f839436e34e50c2ec550ead0a16c361ae6654e", - ConsolePluginImage: "registry-proxy.engineering.redhat.com/rh-osbs/network-observability-console-plugin@sha256:6481481ba23375107233f8d0a4f839436e34e50c2ec550ead0a16c361ae6654e", - DownstreamDeployment: false, - }, - } -} - -type ipResolverMock struct { - mock.Mock -} - -func (ipr *ipResolverMock) LookupIP(host string) ([]net.IP, error) { - m := ipr.Called(host) - return m.Get(0).([]net.IP), m.Error(1) -} diff --git a/docs/FlowCollector.md b/docs/FlowCollector.md index 71972e7fd..871623d0d 100644 --- a/docs/FlowCollector.md +++ b/docs/FlowCollector.md @@ -14049,7 +14049,7 @@ ResourceClaim references one entry in PodSpec.ResourceClaims. namespace string - Namespace where console plugin and flowlogs-pipeline have been deployed.
+ Namespace where console plugin and flowlogs-pipeline have been deployed. Deprecated: annotations are used instead
false diff --git a/hack/cloned.flows.netobserv.io_flowcollectors.yaml b/hack/cloned.flows.netobserv.io_flowcollectors.yaml index 990d1afab..411962246 100644 --- a/hack/cloned.flows.netobserv.io_flowcollectors.yaml +++ b/hack/cloned.flows.netobserv.io_flowcollectors.yaml @@ -26,7 +26,7 @@ spec: - jsonPath: .spec.deploymentModel name: Deployment Model type: string - - jsonPath: .status.conditions[*].reason + - jsonPath: .status.conditions[?(@.type=="Ready")].reason name: Status type: string deprecated: true @@ -1716,7 +1716,7 @@ spec: - jsonPath: .spec.deploymentModel name: Deployment Model type: string - - jsonPath: .status.conditions[*].reason + - jsonPath: .status.conditions[?(@.type=="Ready")].reason name: Status type: string name: v1beta1 @@ -3573,7 +3573,7 @@ spec: - jsonPath: .spec.deploymentModel name: Deployment Model type: string - - jsonPath: .status.conditions[*].reason + - jsonPath: .status.conditions[?(@.type=="Ready")].reason name: Status type: string name: v1beta2 @@ -5567,7 +5567,7 @@ spec: type: object type: array namespace: - description: Namespace where console plugin and flowlogs-pipeline have been deployed. + description: 'Namespace where console plugin and flowlogs-pipeline have been deployed. Deprecated: annotations are used instead' type: string required: - conditions diff --git a/main.go b/main.go index 6e1b59d79..ffae60c02 100644 --- a/main.go +++ b/main.go @@ -40,7 +40,6 @@ import ( _ "k8s.io/client-go/plugin/pkg/client/auth" apiregv1 "k8s.io/kube-aggregator/pkg/apis/apiregistration/v1" ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/healthz" "sigs.k8s.io/controller-runtime/pkg/log/zap" "sigs.k8s.io/controller-runtime/pkg/metrics/server" @@ -52,8 +51,7 @@ import ( flowsv1beta2 "github.com/netobserv/network-observability-operator/api/v1beta2" "github.com/netobserv/network-observability-operator/controllers" "github.com/netobserv/network-observability-operator/controllers/constants" - "github.com/netobserv/network-observability-operator/controllers/operator" - "github.com/netobserv/network-observability-operator/pkg/narrowcache" + "github.com/netobserv/network-observability-operator/pkg/manager" //+kubebuilder:scaffold:imports ) @@ -90,7 +88,7 @@ func main() { var enableHTTP2 bool var versionFlag bool - config := operator.Config{} + config := manager.Config{} flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.") flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") @@ -133,9 +131,8 @@ func main() { } cfg := ctrl.GetConfigOrDie() - narrowCache := narrowcache.NewConfig(cfg, narrowcache.ConfigMaps, narrowcache.Secrets) - mgr, err := ctrl.NewManager(cfg, ctrl.Options{ + mgr, err := manager.NewManager(context.Background(), cfg, &config, &ctrl.Options{ Scheme: scheme, Metrics: server.Options{ BindAddress: metricsAddr, @@ -149,25 +146,12 @@ func main() { HealthProbeBindAddress: probeAddr, LeaderElection: enableLeaderElection, LeaderElectionID: "7a7ecdcd.netobserv.io", - Client: client.Options{Cache: narrowCache.ControllerRuntimeClientCacheOptions()}, - }) + }, controllers.Registerers) if err != nil { - setupLog.Error(err, "unable to start manager") - os.Exit(1) - } - client, err := narrowCache.CreateClient(mgr.GetClient()) - if err != nil { - setupLog.Error(err, "unable to create narrow cache client") + setupLog.Error(err, "unable to setup manager") os.Exit(1) } - ctrl.Log.Info("using eBPF Agent image", "image", config.EBPFAgentImage) - - if err = controllers.NewFlowCollectorReconciler(client, mgr.GetScheme(), &config). - SetupWithManager(context.Background(), mgr); err != nil { - setupLog.Error(err, "unable to create controller", "controller", "FlowCollector") - os.Exit(1) - } if err = (&flowsv1beta2.FlowCollector{}).SetupWebhookWithManager(mgr); err != nil { setupLog.Error(err, "unable to create v1beta2 webhook", "webhook", "FlowCollector") os.Exit(1) diff --git a/pkg/conditions/conditions.go b/pkg/conditions/conditions.go deleted file mode 100644 index 7b4fc73c8..000000000 --- a/pkg/conditions/conditions.go +++ /dev/null @@ -1,144 +0,0 @@ -package conditions - -import ( - "sort" - - flowslatest "github.com/netobserv/network-observability-operator/api/v1beta2" - "k8s.io/apimachinery/pkg/api/meta" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -const ( - TypePending = "Pending" - TypeFailed = "Failed" - MessagePending = "Some FlowCollector components pending on dependencies" -) - -type ErrorCondition struct { - metav1.Condition - Error error -} - -func Updating() *metav1.Condition { - return &metav1.Condition{ - Type: TypePending, - Reason: "Updating", - Message: MessagePending, - } -} - -func DeploymentInProgress() *metav1.Condition { - return &metav1.Condition{ - Type: TypePending, - Reason: "DeploymentInProgress", - Message: MessagePending, - } -} - -func Ready() *metav1.Condition { - return &metav1.Condition{ - Type: "Ready", - Reason: "Ready", - Message: "All components ready", - } -} - -func CannotCreateNamespace(err error) *ErrorCondition { - return &ErrorCondition{ - Condition: metav1.Condition{ - Type: TypeFailed, - Reason: "CannotCreateNamespace", - Message: "Cannot create namespace: " + err.Error(), - }, - Error: err, - } -} - -func NamespaceChangeFailed(err error) *ErrorCondition { - return &ErrorCondition{ - Condition: metav1.Condition{ - Type: TypeFailed, - Reason: "NamespaceChangeFailed", - Message: "Failed to handle namespace change: " + err.Error(), - }, - Error: err, - } -} - -func ReconcileFLPFailed(err error) *ErrorCondition { - return &ErrorCondition{ - Condition: metav1.Condition{ - Type: TypeFailed, - Reason: "ReconcileFLPFailed", - Message: "Failed to reconcile flowlogs-pipeline: " + err.Error(), - }, - Error: err, - } -} - -func ReconcileCNOFailed(err error) *ErrorCondition { - return &ErrorCondition{ - Condition: metav1.Condition{ - Type: TypeFailed, - Reason: "ReconcileCNOFailed", - Message: "Failed to reconcile ovs-flows-config ConfigMap: " + err.Error(), - }, - Error: err, - } -} - -func ReconcileOVNKFailed(err error) *ErrorCondition { - return &ErrorCondition{ - Condition: metav1.Condition{ - Type: TypeFailed, - Reason: "ReconcileOVNKFailed", - Message: "Failed to reconcile ovn-kubernetes DaemonSet: " + err.Error(), - }, - Error: err, - } -} - -func ReconcileAgentFailed(err error) *ErrorCondition { - return &ErrorCondition{ - Condition: metav1.Condition{ - Type: TypeFailed, - Reason: "ReconcileAgentFailed", - Message: "Failed to reconcile eBPF Netobserv Agent: " + err.Error(), - }, - Error: err, - } -} - -func ReconcileConsolePluginFailed(err error) *ErrorCondition { - return &ErrorCondition{ - Condition: metav1.Condition{ - Type: TypeFailed, - Reason: "ReconcileConsolePluginFailed", - Message: "Failed to reconcile Console plugin: " + err.Error(), - }, - Error: err, - } -} - -// set previous conditions to false as FlowCollector manage only one status at a time -func clearPreviousConditions(fc *flowslatest.FlowCollector) { - for _, existingCondition := range fc.Status.Conditions { - existingCondition.Status = metav1.ConditionFalse - meta.SetStatusCondition(&fc.Status.Conditions, existingCondition) - } -} - -// sort conditions by date, latest first -func sortConditions(fc *flowslatest.FlowCollector) { - sort.SliceStable(fc.Status.Conditions, func(i, j int) bool { - return !fc.Status.Conditions[i].LastTransitionTime.Before(&fc.Status.Conditions[j].LastTransitionTime) - }) -} - -// add a single condition to true, keeping the others to status false -func AddUniqueCondition(cond *metav1.Condition, fc *flowslatest.FlowCollector) { - clearPreviousConditions(fc) - cond.Status = metav1.ConditionTrue - meta.SetStatusCondition(&fc.Status.Conditions, *cond) - sortConditions(fc) -} diff --git a/pkg/helper/client_helper.go b/pkg/helper/client_helper.go index ee2a0c923..6808f8af9 100644 --- a/pkg/helper/client_helper.go +++ b/pkg/helper/client_helper.go @@ -4,32 +4,44 @@ import ( "context" "reflect" - appsv1 "k8s.io/api/apps/v1" + "k8s.io/apimachinery/pkg/api/errors" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/log" + + flowslatest "github.com/netobserv/network-observability-operator/api/v1beta2" + "github.com/netobserv/network-observability-operator/controllers/constants" ) // Client includes a kube client with some additional helper functions type Client struct { client.Client SetControllerReference func(client.Object) error - SetChanged func(bool) - SetInProgress func(bool) } func UnmanagedClient(cl client.Client) Client { return Client{ Client: cl, SetControllerReference: func(o client.Object) error { return nil }, - SetChanged: func(b bool) {}, - SetInProgress: func(b bool) {}, } } +func NewFlowCollectorClientHelper(ctx context.Context, c client.Client) (*Client, *flowslatest.FlowCollector, error) { + fc, err := getFlowCollector(ctx, c) + if err != nil || fc == nil { + return nil, fc, err + } + return &Client{ + Client: c, + SetControllerReference: func(obj client.Object) error { + return controllerutil.SetControllerReference(fc, obj, c.Scheme()) + }, + }, fc, nil +} + // CreateOwned is an helper function that creates an object, sets owner reference and writes info & errors logs func (c *Client) CreateOwned(ctx context.Context, obj client.Object) error { log := log.FromContext(ctx) - c.SetChanged(true) err := c.SetControllerReference(obj) if err != nil { log.Error(err, "Failed to set controller reference") @@ -68,9 +80,7 @@ func (c *Client) UpdateOwned(ctx context.Context, old, obj client.Object) error log.Error(err, "Failed to get updated resource "+kind, "Namespace", obj.GetNamespace(), "Name", obj.GetName()) return err } - if obj.GetResourceVersion() != old.GetResourceVersion() { - c.SetChanged(true) - } else { + if obj.GetResourceVersion() == old.GetResourceVersion() { log.Info(kind+" not updated", "Namespace", obj.GetNamespace(), "Name", obj.GetName()) } return nil @@ -88,14 +98,19 @@ func (c *Client) UpdateIfOwned(ctx context.Context, old, obj client.Object) erro return c.UpdateOwned(ctx, old, obj) } -func (c *Client) CheckDeploymentInProgress(d *appsv1.Deployment) { - if d.Status.UpdatedReplicas < d.Status.Replicas { - c.SetInProgress(true) - } -} - -func (c *Client) CheckDaemonSetInProgress(ds *appsv1.DaemonSet) { - if ds.Status.UpdatedNumberScheduled < ds.Status.DesiredNumberScheduled { - c.SetInProgress(true) +func getFlowCollector(ctx context.Context, c client.Client) (*flowslatest.FlowCollector, error) { + log := log.FromContext(ctx) + desired := &flowslatest.FlowCollector{} + if err := c.Get(ctx, constants.FlowCollectorName, desired); err != nil { + if errors.IsNotFound(err) { + // Request object not found, could have been deleted after reconcile request. + // Owned objects are automatically garbage collected. For additional cleanup logic use finalizers. + // Return and don't requeue + log.Info("FlowCollector resource not found. Ignoring since object must be deleted") + return nil, nil + } + // Error reading the object - requeue the request. + return nil, err } + return desired, nil } diff --git a/pkg/helper/flowcollector.go b/pkg/helper/flowcollector.go index ce90d55c8..d0fbbe04d 100644 --- a/pkg/helper/flowcollector.go +++ b/pkg/helper/flowcollector.go @@ -37,12 +37,8 @@ func HasKafkaExporter(spec *flowslatest.FlowCollectorSpec) bool { return false } -func HPADisabled(spec *flowslatest.FlowCollectorHPA) bool { - return spec.Status == flowslatest.HPAStatusDisabled -} - func HPAEnabled(spec *flowslatest.FlowCollectorHPA) bool { - return spec.Status == flowslatest.HPAStatusEnabled + return spec != nil && spec.Status == flowslatest.HPAStatusEnabled } func GetRecordTypes(processor *flowslatest.FlowCollectorFLP) []string { @@ -167,3 +163,10 @@ func removeMetricsByPattern(list []string, search string) []string { } return filtered } + +func GetNamespace(spec *flowslatest.FlowCollectorSpec) string { + if spec.Namespace != "" { + return spec.Namespace + } + return constants.DefaultOperatorNamespace +} diff --git a/controllers/operator/config.go b/pkg/manager/config.go similarity index 95% rename from controllers/operator/config.go rename to pkg/manager/config.go index d4227427f..57173bb17 100644 --- a/controllers/operator/config.go +++ b/pkg/manager/config.go @@ -1,6 +1,8 @@ -package operator +package manager -import "errors" +import ( + "errors" +) // Config of the operator. type Config struct { diff --git a/pkg/manager/manager.go b/pkg/manager/manager.go new file mode 100644 index 000000000..86b106af3 --- /dev/null +++ b/pkg/manager/manager.go @@ -0,0 +1,106 @@ +package manager + +import ( + "context" + "fmt" + + "github.com/netobserv/network-observability-operator/pkg/discover" + "github.com/netobserv/network-observability-operator/pkg/manager/status" + "github.com/netobserv/network-observability-operator/pkg/narrowcache" + "k8s.io/client-go/discovery" + "k8s.io/client-go/rest" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/manager" +) + +//+kubebuilder:rbac:groups=apps,resources=deployments;daemonsets,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=core,resources=namespaces;services;serviceaccounts;configmaps;secrets,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=core,resources=endpoints,verbs=get;list;watch +//+kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=clusterrolebindings;clusterroles;rolebindings;roles,verbs=get;list;create;delete;update;watch +//+kubebuilder:rbac:groups=console.openshift.io,resources=consoleplugins,verbs=get;create;delete;update;patch;list;watch +//+kubebuilder:rbac:groups=operator.openshift.io,resources=consoles,verbs=get;update;list;update;watch +//+kubebuilder:rbac:groups=flows.netobserv.io,resources=flowcollectors,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=flows.netobserv.io,resources=flowcollectors/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=flows.netobserv.io,resources=flowcollectors/finalizers,verbs=update +//+kubebuilder:rbac:groups=security.openshift.io,resources=securitycontextconstraints,resourceNames=hostnetwork,verbs=use +//+kubebuilder:rbac:groups=security.openshift.io,resources=securitycontextconstraints,verbs=list;create;update;watch +//+kubebuilder:rbac:groups=apiregistration.k8s.io,resources=apiservices,verbs=list;get;watch +//+kubebuilder:rbac:groups=monitoring.coreos.com,resources=servicemonitors;prometheusrules,verbs=get;create;delete;update;patch;list;watch +//+kubebuilder:rbac:groups=config.openshift.io,resources=clusterversions,verbs=get;list;watch +//+kubebuilder:rbac:groups=loki.grafana.com,resources=network,resourceNames=logs,verbs=get;create +//+kubebuilder:rbac:urls="/metrics",verbs=get + +type Registerer func(context.Context, *Manager) error + +type Manager struct { + manager.Manager + discover.AvailableAPIs + Client client.Client + Status *status.Manager + Config *Config + vendor discover.Vendor +} + +func NewManager( + ctx context.Context, + kcfg *rest.Config, + opcfg *Config, + opts *ctrl.Options, + ctrls []Registerer, +) (*Manager, error) { + + log := log.FromContext(ctx) + log.Info("Creating manager") + + narrowCache := narrowcache.NewConfig(kcfg, narrowcache.ConfigMaps, narrowcache.Secrets) + opts.Client = client.Options{Cache: narrowCache.ControllerRuntimeClientCacheOptions()} + + internalManager, err := ctrl.NewManager(kcfg, *opts) + if err != nil { + return nil, err + } + client, err := narrowCache.CreateClient(internalManager.GetClient()) + if err != nil { + return nil, fmt.Errorf("unable to create narrow cache client: %w", err) + } + + log.Info("Discovering APIs") + dc, err := discovery.NewDiscoveryClientForConfig(kcfg) + if err != nil { + return nil, fmt.Errorf("can't instantiate discovery client: %w", err) + } + permissions := discover.Permissions{Client: dc} + vendor := permissions.Vendor(ctx) + apis, err := discover.NewAvailableAPIs(dc) + if err != nil { + return nil, fmt.Errorf("can't discover available APIs: %w", err) + } + + this := &Manager{ + Manager: internalManager, + AvailableAPIs: *apis, + Status: status.NewManager(), + Client: client, + Config: opcfg, + vendor: vendor, + } + + log.Info("Building controllers") + for _, f := range ctrls { + if err := f(ctx, this); err != nil { + return nil, fmt.Errorf("unable to create controller: %w", err) + } + } + + return this, nil +} + +func (m *Manager) GetClient() client.Client { + return m.Client +} + +func (m *Manager) IsOpenShift() bool { + return m.vendor == discover.VendorOpenShift +} diff --git a/pkg/manager/status/namespace.go b/pkg/manager/status/namespace.go new file mode 100644 index 000000000..4b927c91a --- /dev/null +++ b/pkg/manager/status/namespace.go @@ -0,0 +1,54 @@ +package status + +import ( + "context" + "strings" + + flowslatest "github.com/netobserv/network-observability-operator/api/v1beta2" + "github.com/netobserv/network-observability-operator/controllers/constants" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/client-go/util/retry" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" +) + +func annotation(cpnt ComponentName) string { + return constants.AnnotationDomain + "/" + strings.ToLower(string(cpnt)) + "-namespace" +} + +func GetDeployedNamespace(cpnt ComponentName, fc *flowslatest.FlowCollector) string { + if ns, found := fc.Annotations[annotation(cpnt)]; found { + return ns + } + return fc.Status.Namespace +} + +func SetDeployedNamespace(ctx context.Context, c client.Client, cpnt ComponentName, ns string) error { + log := log.FromContext(ctx) + annot := annotation(cpnt) + log.WithValues(annot, ns).Info("Updating FlowCollector annotation") + + return retry.RetryOnConflict(retry.DefaultBackoff, func() error { + fc := flowslatest.FlowCollector{} + if err := c.Get(ctx, constants.FlowCollectorName, &fc); err != nil { + if errors.IsNotFound(err) { + // ignore: when it's being deleted, there's no point trying to update its status + return nil + } + return err + } + if fc.Annotations == nil { + fc.Annotations = make(map[string]string) + } + fc.Annotations[annot] = ns + return c.Update(ctx, &fc) + }) +} + +func (i *Instance) GetDeployedNamespace(fc *flowslatest.FlowCollector) string { + return GetDeployedNamespace(i.cpnt, fc) +} + +func (i *Instance) SetDeployedNamespace(ctx context.Context, c client.Client, ns string) error { + return SetDeployedNamespace(ctx, c, i.cpnt, ns) +} diff --git a/pkg/manager/status/status_manager.go b/pkg/manager/status/status_manager.go new file mode 100644 index 000000000..5808196bd --- /dev/null +++ b/pkg/manager/status/status_manager.go @@ -0,0 +1,212 @@ +package status + +import ( + "context" + "fmt" + "sync" + + flowslatest "github.com/netobserv/network-observability-operator/api/v1beta2" + "github.com/netobserv/network-observability-operator/controllers/constants" + appsv1 "k8s.io/api/apps/v1" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/util/retry" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" +) + +type ComponentName string + +const ( + FlowCollectorLegacy ComponentName = "FlowCollectorLegacy" + FLPParent ComponentName = "FLPParent" + FLPMonolith ComponentName = "FLPMonolith" + FLPIngestOnly ComponentName = "FLPIngestOnly" + FLPTransformOnly ComponentName = "FLPTransformOnly" + Monitoring ComponentName = "Monitoring" +) + +var allNames = []ComponentName{FlowCollectorLegacy, Monitoring} + +type Manager struct { + statuses sync.Map +} + +func NewManager() *Manager { + s := Manager{} + for _, cpnt := range allNames { + s.statuses.Store(cpnt, ComponentStatus{ + name: cpnt, + status: StatusUnknown, + }) + } + return &s +} + +func (s *Manager) setInProgress(cpnt ComponentName, reason, message string) { + s.statuses.Store(cpnt, ComponentStatus{ + name: cpnt, + status: StatusInProgress, + reason: reason, + message: message, + }) +} + +func (s *Manager) setFailure(cpnt ComponentName, reason, message string) { + s.statuses.Store(cpnt, ComponentStatus{ + name: cpnt, + status: StatusFailure, + reason: reason, + message: message, + }) +} + +func (s *Manager) hasFailure(cpnt ComponentName) bool { + v, _ := s.statuses.Load(cpnt) + return v != nil && v.(ComponentStatus).status == StatusFailure +} + +func (s *Manager) setReady(cpnt ComponentName) { + s.statuses.Store(cpnt, ComponentStatus{ + name: cpnt, + status: StatusReady, + }) +} + +func (s *Manager) setUnknown(cpnt ComponentName) { + s.statuses.Store(cpnt, ComponentStatus{ + name: cpnt, + status: StatusUnknown, + }) +} + +func (s *Manager) setUnused(cpnt ComponentName, message string) { + s.statuses.Store(cpnt, ComponentStatus{ + name: cpnt, + status: StatusUnknown, + reason: "ComponentUnused", + message: message, + }) +} + +func (s *Manager) getConditions() []metav1.Condition { + global := metav1.Condition{ + Type: "Ready", + Status: metav1.ConditionTrue, + Reason: "Ready", + } + conds := []metav1.Condition{} + counters := make(map[Status]int, len(allNames)) + s.statuses.Range(func(_, v any) bool { + status := v.(ComponentStatus) + conds = append(conds, status.toCondition()) + counters[status.status]++ + return true + }) + global.Message = fmt.Sprintf("%d ready components, %d with failure, %d pending", counters[StatusReady], counters[StatusFailure], counters[StatusInProgress]) + if counters[StatusFailure] > 0 { + global.Status = metav1.ConditionFalse + global.Reason = "Failure" + } else if counters[StatusInProgress] > 0 { + global.Status = metav1.ConditionFalse + global.Reason = "Pending" + } + return append([]metav1.Condition{global}, conds...) +} + +func (s *Manager) Sync(ctx context.Context, c client.Client) { + updateStatus(ctx, c, s.getConditions()...) +} + +func updateStatus(ctx context.Context, c client.Client, conditions ...metav1.Condition) { + log := log.FromContext(ctx) + log.Info("Updating FlowCollector status") + + err := retry.RetryOnConflict(retry.DefaultBackoff, func() error { + fc := flowslatest.FlowCollector{} + if err := c.Get(ctx, constants.FlowCollectorName, &fc); err != nil { + if errors.IsNotFound(err) { + // ignore: when it's being deleted, there's no point trying to update its status + return nil + } + return err + } + for _, c := range conditions { + meta.SetStatusCondition(&fc.Status.Conditions, c) + } + return c.Status().Update(ctx, &fc) + }) + + if err != nil { + log.Error(err, "failed to update FlowCollector status") + } +} + +func (s *Manager) ForComponent(cpnt ComponentName) Instance { + return Instance{cpnt: cpnt, s: s} +} + +type Instance struct { + cpnt ComponentName + s *Manager +} + +func (i *Instance) SetReady() { + i.s.setReady(i.cpnt) +} + +func (i *Instance) SetUnknown() { + i.s.setUnknown(i.cpnt) +} + +func (i *Instance) SetUnused(message string) { + i.s.setUnused(i.cpnt, message) +} + +func (i *Instance) CheckDeploymentProgress(d *appsv1.Deployment) { + // TODO (when legacy controller is broken down into individual controllers) + // this should set the status as Ready when replicas match + for _, c := range d.Status.Conditions { + if c.Type == appsv1.DeploymentAvailable { + if c.Status != v1.ConditionTrue { + i.s.setInProgress(i.cpnt, "DeploymentNotReady", fmt.Sprintf("Deployment %s not ready: %d/%d (%s)", d.Name, d.Status.UpdatedReplicas, d.Status.Replicas, c.Message)) + } + return + } + } +} + +func (i *Instance) CheckDaemonSetProgress(ds *appsv1.DaemonSet) { + // TODO (when legacy controller is broken down into individual controllers) + // this should set the status as Ready when replicas match + if ds.Status.UpdatedNumberScheduled < ds.Status.DesiredNumberScheduled { + i.s.setInProgress(i.cpnt, "DaemonSetNotReady", fmt.Sprintf("DaemonSet %s not ready: %d/%d", ds.Name, ds.Status.UpdatedNumberScheduled, ds.Status.DesiredNumberScheduled)) + } +} + +func (i *Instance) SetCreatingDeployment(d *appsv1.Deployment) { + i.s.setInProgress(i.cpnt, "CreatingDeployment", fmt.Sprintf("Creating deployment %s", d.Name)) +} + +func (i *Instance) SetCreatingDaemonSet(ds *appsv1.DaemonSet) { + i.s.setInProgress(i.cpnt, "CreatingDaemonSet", fmt.Sprintf("Creating daemon set %s", ds.Name)) +} + +func (i *Instance) SetFailure(reason, message string) { + i.s.setFailure(i.cpnt, reason, message) +} + +func (i *Instance) Error(reason string, err error) error { + i.SetFailure(reason, err.Error()) + return err +} + +func (i *Instance) HasFailure() bool { + return i.s.hasFailure(i.cpnt) +} + +func (i *Instance) Commit(ctx context.Context, c client.Client) { + i.s.Sync(ctx, c) +} diff --git a/pkg/manager/status/status_manager_test.go b/pkg/manager/status/status_manager_test.go new file mode 100644 index 000000000..558bb46ce --- /dev/null +++ b/pkg/manager/status/status_manager_test.go @@ -0,0 +1,75 @@ +package status + +import ( + "testing" + + "github.com/stretchr/testify/assert" + appsv1 "k8s.io/api/apps/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestStatusWorkflow(t *testing.T) { + s := NewManager() + sl := s.ForComponent(FlowCollectorLegacy) + sm := s.ForComponent(Monitoring) + + sl.SetReady() // temporary until controllers are broken down + sl.SetCreatingDaemonSet(&appsv1.DaemonSet{ObjectMeta: metav1.ObjectMeta{Name: "test"}}) + sm.SetFailure("AnError", "bad one") + + conds := s.getConditions() + assert.Len(t, conds, 3) + assertHasCondition(t, conds, "Ready", "Failure", metav1.ConditionFalse) + assertHasCondition(t, conds, "FlowCollectorLegacyReady", "CreatingDaemonSet", metav1.ConditionFalse) + assertHasCondition(t, conds, "MonitoringReady", "AnError", metav1.ConditionFalse) + + sl.SetReady() // temporary until controllers are broken down + sl.CheckDaemonSetProgress(&appsv1.DaemonSet{ObjectMeta: metav1.ObjectMeta{Name: "test"}, Status: appsv1.DaemonSetStatus{ + DesiredNumberScheduled: 3, + UpdatedNumberScheduled: 1, + }}) + sm.SetUnknown() + + conds = s.getConditions() + assert.Len(t, conds, 3) + assertHasCondition(t, conds, "Ready", "Pending", metav1.ConditionFalse) + assertHasCondition(t, conds, "FlowCollectorLegacyReady", "DaemonSetNotReady", metav1.ConditionFalse) + assertHasCondition(t, conds, "MonitoringReady", "Ready", metav1.ConditionUnknown) + + sl.SetReady() // temporary until controllers are broken down + sl.CheckDaemonSetProgress(&appsv1.DaemonSet{ObjectMeta: metav1.ObjectMeta{Name: "test"}, Status: appsv1.DaemonSetStatus{ + DesiredNumberScheduled: 3, + UpdatedNumberScheduled: 3, + }}) + sm.SetUnused("message") + + conds = s.getConditions() + assert.Len(t, conds, 3) + assertHasCondition(t, conds, "Ready", "Ready", metav1.ConditionTrue) + assertHasCondition(t, conds, "FlowCollectorLegacyReady", "Ready", metav1.ConditionTrue) + assertHasCondition(t, conds, "MonitoringReady", "ComponentUnused", metav1.ConditionUnknown) + + sl.SetReady() // temporary until controllers are broken down + sl.CheckDeploymentProgress(&appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Name: "test"}, Status: appsv1.DeploymentStatus{ + UpdatedReplicas: 2, + Replicas: 2, + }}) + sm.SetReady() + + conds = s.getConditions() + assert.Len(t, conds, 3) + assertHasCondition(t, conds, "Ready", "Ready", metav1.ConditionTrue) + assertHasCondition(t, conds, "FlowCollectorLegacyReady", "Ready", metav1.ConditionTrue) + assertHasCondition(t, conds, "MonitoringReady", "Ready", metav1.ConditionTrue) +} + +func assertHasCondition(t *testing.T, conditions []metav1.Condition, searchType, reason string, value metav1.ConditionStatus) { + for _, c := range conditions { + if c.Type == searchType { + assert.Equal(t, reason, c.Reason, conditions) + assert.Equal(t, value, c.Status, conditions) + return + } + } + assert.Fail(t, "Condition type not found", searchType, conditions) +} diff --git a/pkg/manager/status/statuses.go b/pkg/manager/status/statuses.go new file mode 100644 index 000000000..db4cfd8ea --- /dev/null +++ b/pkg/manager/status/statuses.go @@ -0,0 +1,41 @@ +package status + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type Status string + +const ( + StatusUnknown Status = "Unknown" + StatusInProgress Status = "InProgress" + StatusReady Status = "Ready" + StatusFailure Status = "Failure" +) + +type ComponentStatus struct { + name ComponentName + status Status + reason string + message string +} + +func (s *ComponentStatus) toCondition() metav1.Condition { + c := metav1.Condition{ + Type: string(s.name) + "Ready", + Reason: "Ready", + Message: s.message, + } + if s.reason != "" { + c.Reason = s.reason + } + switch s.status { + case StatusUnknown: + c.Status = metav1.ConditionUnknown + case StatusFailure, StatusInProgress: + c.Status = metav1.ConditionFalse + case StatusReady: + c.Status = metav1.ConditionTrue + } + return c +} diff --git a/pkg/narrowcache/client.go b/pkg/narrowcache/client.go index 4ff3292d2..2048d075e 100644 --- a/pkg/narrowcache/client.go +++ b/pkg/narrowcache/client.go @@ -40,8 +40,6 @@ type handlerOnQueue struct { } func (c *Client) Get(ctx context.Context, key client.ObjectKey, out client.Object, opts ...client.GetOption) error { - rlog := log.FromContext(ctx).WithName("narrowcache") - rlog.WithValues("key", key).Info("Getting object:") gvk, err := c.GroupVersionKindFor(out) if err != nil { return err @@ -60,16 +58,13 @@ func (c *Client) Get(ctx context.Context, key client.ObjectKey, out client.Objec return nil } - rlog.WithValues("GVK", strGVK).Info("GVK not managed here: pass it to down layer") return c.Client.Get(ctx, key, out, opts...) } func (c *Client) getAndCreateWatchIfNeeded(ctx context.Context, info GVKInfo, gvk schema.GroupVersionKind, key client.ObjectKey) (client.Object, string, error) { strGVK := gvk.String() objKey := strGVK + "|" + key.String() - rlog := log.FromContext(ctx).WithName("narrowcache").WithValues("objKey", objKey) - rlog.Info("Managed kind: check cache") c.wmut.RLock() ca := c.watchedObjects[objKey] c.wmut.RUnlock() @@ -78,17 +73,16 @@ func (c *Client) getAndCreateWatchIfNeeded(ctx context.Context, info GVKInfo, gv return nil, objKey, errors.NewNotFound(schema.GroupResource{Group: gvk.Group, Resource: gvk.Kind}, key.Name) } // Return from cache - rlog.Info("Object found in cache, returning") return ca.cached, objKey, nil } // Live query - rlog.Info("Object not found in cache, doing live query") + rlog := log.FromContext(ctx).WithName("narrowcache").WithValues("objKey", objKey) + rlog.Info("Cache miss, doing live query") fetched, err := info.Getter(ctx, c.liveClient, key) if err != nil { return nil, objKey, err } - rlog.Info("Object fetched") // Create watch for later calls w, err := info.Watcher(ctx, c.liveClient, key)