diff --git a/api/flowcollector/v1beta2/flowcollector_types.go b/api/flowcollector/v1beta2/flowcollector_types.go index c42abd47b..7ff64bef4 100644 --- a/api/flowcollector/v1beta2/flowcollector_types.go +++ b/api/flowcollector/v1beta2/flowcollector_types.go @@ -850,6 +850,11 @@ type LokiMicroservicesParams struct { // LokiMonolithParams is the configuration for monolithic Loki (https://grafana.com/docs/loki/latest/fundamentals/architecture/deployment-modes/#monolithic-mode) type LokiMonolithParams struct { + // Set `zeroClick` to `true` to automatically create Loki deployment, service and storage. + // This is useful for development pureposes. Do not use it in production. + //+kubebuilder:default:=true + ZeroClick *bool `json:"zeroClick,omitempty"` + //+kubebuilder:default:="http://loki:3100/" // `url` is the unique address of an existing Loki service that points to both the ingester and the querier. URL string `json:"url,omitempty"` diff --git a/api/flowcollector/v1beta2/helper.go b/api/flowcollector/v1beta2/helper.go index 7b478b059..3b14d9ccc 100644 --- a/api/flowcollector/v1beta2/helper.go +++ b/api/flowcollector/v1beta2/helper.go @@ -46,6 +46,10 @@ func (spec *FlowCollectorSpec) UseLoki() bool { return spec.Loki.Enable == nil || *spec.Loki.Enable } +func (spec *FlowCollectorSpec) UseLokiDev() bool { + return spec.UseLoki() && spec.Loki.Mode == LokiModeMonolithic && *spec.Loki.Monolithic.ZeroClick +} + func (spec *FlowCollectorSpec) UsePrometheus() bool { // nil should fallback to default value, which is "true" return spec.Prometheus.Querier.Enable == nil || *spec.Prometheus.Querier.Enable diff --git a/api/flowcollector/v1beta2/zz_generated.deepcopy.go b/api/flowcollector/v1beta2/zz_generated.deepcopy.go index 612a9c178..3f45f8673 100644 --- a/api/flowcollector/v1beta2/zz_generated.deepcopy.go +++ b/api/flowcollector/v1beta2/zz_generated.deepcopy.go @@ -853,7 +853,7 @@ func (in *FlowCollectorLoki) DeepCopyInto(out *FlowCollectorLoki) { } out.Manual = in.Manual out.Microservices = in.Microservices - out.Monolithic = in.Monolithic + in.Monolithic.DeepCopyInto(&out.Monolithic) out.LokiStack = in.LokiStack if in.ReadTimeout != nil { in, out := &in.ReadTimeout, &out.ReadTimeout @@ -1107,6 +1107,11 @@ func (in *LokiMicroservicesParams) DeepCopy() *LokiMicroservicesParams { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *LokiMonolithParams) DeepCopyInto(out *LokiMonolithParams) { *out = *in + if in.ZeroClick != nil { + in, out := &in.ZeroClick, &out.ZeroClick + *out = new(bool) + **out = **in + } out.TLS = in.TLS } diff --git a/bundle/manifests/flows.netobserv.io_flowcollectors.yaml b/bundle/manifests/flows.netobserv.io_flowcollectors.yaml index 3b01095ba..734f034e7 100644 --- a/bundle/manifests/flows.netobserv.io_flowcollectors.yaml +++ b/bundle/manifests/flows.netobserv.io_flowcollectors.yaml @@ -4209,6 +4209,12 @@ spec: description: '`url` is the unique address of an existing Loki service that points to both the ingester and the querier.' type: string + zeroClick: + default: true + description: |- + Set `zeroClick` to `true` to automatically create Loki deployment, service and storage. + This is useful for development pureposes. Do not use it in production. + type: boolean type: object readTimeout: default: 30s diff --git a/bundle/manifests/netobserv-operator.clusterserviceversion.yaml b/bundle/manifests/netobserv-operator.clusterserviceversion.yaml index 1d19d2513..bff0c90b0 100644 --- a/bundle/manifests/netobserv-operator.clusterserviceversion.yaml +++ b/bundle/manifests/netobserv-operator.clusterserviceversion.yaml @@ -194,16 +194,7 @@ metadata: }, "mode": "Monolithic", "monolithic": { - "tenantID": "netobserv", - "tls": { - "caCert": { - "certFile": "service-ca.crt", - "name": "loki-gateway-ca-bundle", - "type": "configmap" - }, - "enable": false - }, - "url": "http://loki.netobserv.svc:3100/" + "zeroClick": true }, "readTimeout": "30s", "writeBatchSize": 10485760, @@ -596,6 +587,8 @@ spec: path: loki.monolithic.tenantID - displayName: Url path: loki.monolithic.url + - displayName: Zero click + path: loki.monolithic.zeroClick - displayName: Read timeout path: loki.readTimeout - displayName: Namespace @@ -777,6 +770,7 @@ spec: resources: - configmaps - namespaces + - persistentvolumeclaims - secrets - serviceaccounts - services @@ -1057,6 +1051,7 @@ spec: - --flowlogs-pipeline-image=$(RELATED_IMAGE_FLOWLOGS_PIPELINE) - --console-plugin-image=$(RELATED_IMAGE_CONSOLE_PLUGIN) - --console-plugin-compat-image=$(RELATED_IMAGE_CONSOLE_PLUGIN_COMPAT) + - --zero-click-loki-image=$(RELATED_IMAGE_ZERO_CLICK_LOKI) - --namespace=$(NAMESPACE) - --downstream-deployment=$(DOWNSTREAM_DEPLOYMENT) - --profiling-bind-address=$(PROFILING_BIND_ADDRESS) @@ -1073,6 +1068,8 @@ spec: value: quay.io/netobserv/network-observability-console-plugin:v1.9.2-community - name: RELATED_IMAGE_CONSOLE_PLUGIN_COMPAT value: quay.io/netobserv/network-observability-console-plugin-pf4:v1.8.2-community + - name: RELATED_IMAGE_ZERO_CLICK_LOKI + value: grafana/loki:3.5.0 - name: DOWNSTREAM_DEPLOYMENT value: "false" - name: PROFILING_BIND_ADDRESS @@ -1220,6 +1217,8 @@ spec: name: console-plugin - image: quay.io/netobserv/network-observability-console-plugin-pf4:v1.8.2-community name: console-plugin-compat + - image: grafana/loki:3.5.0 + name: zero-click-loki version: 1.9.2-community webhookdefinitions: - admissionReviewVersions: diff --git a/catalog/unreleased/downstream-test-fbc/bundle.yaml b/catalog/unreleased/downstream-test-fbc/bundle.yaml index da0489632..544e2ee59 100644 --- a/catalog/unreleased/downstream-test-fbc/bundle.yaml +++ b/catalog/unreleased/downstream-test-fbc/bundle.yaml @@ -811,6 +811,8 @@ properties: path: loki.microservices.querierUrl - displayName: Tenant id path: loki.microservices.tenantID + - displayName: Zero click + path: loki.monolithic.zeroClick - displayName: Tenant id path: loki.monolithic.tenantID - displayName: Url diff --git a/config/crd/bases/flows.netobserv.io_flowcollectors.yaml b/config/crd/bases/flows.netobserv.io_flowcollectors.yaml index 29da48189..24535cf6b 100644 --- a/config/crd/bases/flows.netobserv.io_flowcollectors.yaml +++ b/config/crd/bases/flows.netobserv.io_flowcollectors.yaml @@ -3861,6 +3861,12 @@ spec: default: http://loki:3100/ description: '`url` is the unique address of an existing Loki service that points to both the ingester and the querier.' type: string + zeroClick: + default: true + description: |- + Set `zeroClick` to `true` to automatically create Loki deployment, service and storage. + This is useful for development pureposes. Do not use it in production. + type: boolean type: object readTimeout: default: 30s diff --git a/config/manager/manager.yaml b/config/manager/manager.yaml index c7d29dc52..297d4d8c9 100644 --- a/config/manager/manager.yaml +++ b/config/manager/manager.yaml @@ -28,6 +28,7 @@ spec: - --flowlogs-pipeline-image=$(RELATED_IMAGE_FLOWLOGS_PIPELINE) - --console-plugin-image=$(RELATED_IMAGE_CONSOLE_PLUGIN) - --console-plugin-compat-image=$(RELATED_IMAGE_CONSOLE_PLUGIN_COMPAT) + - --zero-click-loki-image=$(RELATED_IMAGE_ZERO_CLICK_LOKI) - --namespace=$(NAMESPACE) - --downstream-deployment=$(DOWNSTREAM_DEPLOYMENT) - --profiling-bind-address=$(PROFILING_BIND_ADDRESS) @@ -40,6 +41,8 @@ spec: value: quay.io/netobserv/network-observability-console-plugin:v1.9.2-community - name: RELATED_IMAGE_CONSOLE_PLUGIN_COMPAT value: quay.io/netobserv/network-observability-console-plugin-pf4:v1.8.2-community + - name: RELATED_IMAGE_ZERO_CLICK_LOKI + value: grafana/loki:3.5.0 - name: DOWNSTREAM_DEPLOYMENT value: "false" - name: PROFILING_BIND_ADDRESS diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index dd937b3d6..01c3206bc 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -9,6 +9,7 @@ rules: resources: - configmaps - namespaces + - persistentvolumeclaims - secrets - serviceaccounts - services diff --git a/config/samples/flows_v1beta2_flowcollector.yaml b/config/samples/flows_v1beta2_flowcollector.yaml index 451261884..01dab06ef 100644 --- a/config/samples/flows_v1beta2_flowcollector.yaml +++ b/config/samples/flows_v1beta2_flowcollector.yaml @@ -151,14 +151,7 @@ spec: # Change mode to "LokiStack" to use with the loki operator mode: Monolithic monolithic: - url: 'http://loki.netobserv.svc:3100/' - tenantID: netobserv - tls: - enable: false - caCert: - type: configmap - name: loki-gateway-ca-bundle - certFile: service-ca.crt + zeroClick: true lokiStack: name: loki # Change loki operator instance namespace diff --git a/docs/FlowCollector.md b/docs/FlowCollector.md index 26e19fe99..9fd7fa94b 100644 --- a/docs/FlowCollector.md +++ b/docs/FlowCollector.md @@ -8143,6 +8143,16 @@ It is ignored for other modes. Default: http://loki:3100/
false + + zeroClick + boolean + + Set `zeroClick` to `true` to automatically create Loki deployment, service and storage. +This is useful for development pureposes. Do not use it in production.
+
+ Default: true
+ + false diff --git a/helm/crds/flows.netobserv.io_flowcollectors.yaml b/helm/crds/flows.netobserv.io_flowcollectors.yaml index cd5647770..2ba8f1c55 100644 --- a/helm/crds/flows.netobserv.io_flowcollectors.yaml +++ b/helm/crds/flows.netobserv.io_flowcollectors.yaml @@ -3865,6 +3865,12 @@ spec: default: http://loki:3100/ description: '`url` is the unique address of an existing Loki service that points to both the ingester and the querier.' type: string + zeroClick: + default: true + description: |- + Set `zeroClick` to `true` to automatically create Loki deployment, service and storage. + This is useful for development pureposes. Do not use it in production. + type: boolean type: object readTimeout: default: 30s diff --git a/helm/templates/clusterrole.yaml b/helm/templates/clusterrole.yaml index 0566c6202..23df6de9d 100644 --- a/helm/templates/clusterrole.yaml +++ b/helm/templates/clusterrole.yaml @@ -8,6 +8,7 @@ rules: resources: - configmaps - namespaces + - persistentvolumeclaims - secrets - serviceaccounts - services diff --git a/helm/templates/deployment.yaml b/helm/templates/deployment.yaml index d32407fb9..b1bc33d92 100644 --- a/helm/templates/deployment.yaml +++ b/helm/templates/deployment.yaml @@ -27,6 +27,7 @@ spec: - --flowlogs-pipeline-image=$(RELATED_IMAGE_FLOWLOGS_PIPELINE) - --console-plugin-image=$(RELATED_IMAGE_CONSOLE_PLUGIN) - --console-plugin-compat-image=$(RELATED_IMAGE_CONSOLE_PLUGIN_COMPAT) + - --zero-click-loki-image=$(RELATED_IMAGE_ZERO_CLICK_LOKI) - --namespace=$(NAMESPACE) - --downstream-deployment=$(DOWNSTREAM_DEPLOYMENT) - --profiling-bind-address=$(PROFILING_BIND_ADDRESS) @@ -41,6 +42,8 @@ spec: value: '{{ .Values.flowlogsPipeline.image }}:{{ .Values.flowlogsPipeline.version }}' - name: RELATED_IMAGE_CONSOLE_PLUGIN value: '{{ if .Values.standaloneConsole.enable }}{{ .Values.standaloneConsole.image }}:{{ .Values.standaloneConsole.version }}{{ else }}{{ .Values.consolePlugin.image }}:{{ .Values.consolePlugin.version }}{{ end }}' + - name: RELATED_IMAGE_ZERO_CLICK_LOKI + value: grafana/loki:3.5.0 - name: DOWNSTREAM_DEPLOYMENT value: "false" - name: PROFILING_BIND_ADDRESS diff --git a/internal/controller/constants/constants.go b/internal/controller/constants/constants.go index 8dfa31b3e..c9b8bfcfa 100644 --- a/internal/controller/constants/constants.go +++ b/internal/controller/constants/constants.go @@ -25,6 +25,7 @@ const ( PluginName = "netobserv-plugin" StaticPluginName = "netobserv-plugin-static" PluginShortName = "plugin" + LokiDev = "loki" // EBPFAgentName and other constants for it EBPFAgentName = "netobserv-ebpf-agent" diff --git a/internal/controller/flowcollector_controller.go b/internal/controller/flowcollector_controller.go index 24d1371bf..525d2ce64 100644 --- a/internal/controller/flowcollector_controller.go +++ b/internal/controller/flowcollector_controller.go @@ -17,6 +17,7 @@ import ( flowslatest "github.com/netobserv/network-observability-operator/api/flowcollector/v1beta2" "github.com/netobserv/network-observability-operator/internal/controller/consoleplugin" "github.com/netobserv/network-observability-operator/internal/controller/ebpf" + "github.com/netobserv/network-observability-operator/internal/controller/loki" "github.com/netobserv/network-observability-operator/internal/controller/reconcilers" "github.com/netobserv/network-observability-operator/internal/pkg/cleanup" "github.com/netobserv/network-observability-operator/internal/pkg/helper" @@ -120,8 +121,8 @@ func (r *FlowCollectorReconciler) Reconcile(ctx context.Context, _ ctrl.Request) func (r *FlowCollectorReconciler) reconcile(ctx context.Context, clh *helper.Client, desired *flowslatest.FlowCollector) error { ns := desired.Spec.GetNamespace() previousNamespace := r.status.GetDeployedNamespace(desired) - loki := helper.NewLokiConfig(&desired.Spec.Loki, ns) - reconcilersInfo := r.newCommonInfo(clh, ns, &loki) + lokiConfig := helper.NewLokiConfig(&desired.Spec.Loki, ns) + reconcilersInfo := r.newCommonInfo(clh, ns, &lokiConfig) if err := r.checkFinalizer(ctx, desired); err != nil { return err @@ -162,11 +163,20 @@ func (r *FlowCollectorReconciler) reconcile(ctx context.Context, clh *helper.Cli } // Console plugin - err := cpReconciler.Reconcile(ctx, desired) - if err != nil { + if err := cpReconciler.Reconcile(ctx, desired); err != nil { return r.status.Error("ReconcileConsolePluginFailed", err) } + lokiReconciler := loki.NewReconciler(reconcilersInfo.NewInstance( + map[reconcilers.ImageRef]string{ + reconcilers.MainImage: r.mgr.Config.ZeroClickLokiImage, + }, + r.status, + )) + if err := lokiReconciler.Reconcile(ctx, desired); err != nil { + return r.status.Error("ReconcileLokiFailed", err) + } + return nil } diff --git a/internal/controller/flowcollector_controller_console_test.go b/internal/controller/flowcollector_controller_console_test.go index 420aca527..d509fc651 100644 --- a/internal/controller/flowcollector_controller_console_test.go +++ b/internal/controller/flowcollector_controller_console_test.go @@ -256,6 +256,7 @@ func flowCollectorConsolePluginSpecs() { }) It("Should update the Loki URL in the Console Plugin if it changes in the Spec", func() { updateCR(crKey, func(fc *flowslatest.FlowCollector) { + fc.Spec.Loki.Monolithic.ZeroClick = ptr.To(false) fc.Spec.Loki.Monolithic.URL = "http://loki.namespace:8888" }) Eventually(getConfigMapData(configKey), diff --git a/internal/controller/flowcollector_controller_iso_test.go b/internal/controller/flowcollector_controller_iso_test.go index 698b9ce73..b082d6b0f 100644 --- a/internal/controller/flowcollector_controller_iso_test.go +++ b/internal/controller/flowcollector_controller_iso_test.go @@ -168,9 +168,10 @@ func flowCollectorIsoSpecs() { TLS: defaultTLS, }, Monolithic: flowslatest.LokiMonolithParams{ - URL: "http://loki:3100/", - TenantID: "netobserv", - TLS: defaultTLS, + ZeroClick: ptr.To(false), + URL: "http://loki:3100/", + TenantID: "netobserv", + TLS: defaultTLS, }, LokiStack: flowslatest.LokiStackRef{ Name: "loki", diff --git a/internal/controller/flp/flp_controller_test.go b/internal/controller/flp/flp_controller_test.go index e33469b02..8c4aff3ef 100644 --- a/internal/controller/flp/flp_controller_test.go +++ b/internal/controller/flp/flp_controller_test.go @@ -637,7 +637,8 @@ func ControllerSpecs() { updateCR(crKey, func(fc *flowslatest.FlowCollector) { fc.Spec.Loki.Mode = flowslatest.LokiModeMonolithic fc.Spec.Loki.Monolithic = flowslatest.LokiMonolithParams{ - URL: "http://loki-mono:3100/", + ZeroClick: ptr.To(false), + URL: "http://loki-mono:3100/", TLS: flowslatest.ClientTLS{ Enable: true, CACert: flowslatest.CertificateReference{ diff --git a/internal/controller/loki/config/config.go b/internal/controller/loki/config/config.go new file mode 100644 index 000000000..1da3474d8 --- /dev/null +++ b/internal/controller/loki/config/config.go @@ -0,0 +1,12 @@ +package config + +import ( + _ "embed" +) + +//go:embed local-config.yaml +var rawLocalConfig []byte + +func GetLokiConfigStr() string { + return string(rawLocalConfig) +} diff --git a/internal/controller/loki/config/local-config.yaml b/internal/controller/loki/config/local-config.yaml new file mode 100644 index 000000000..6553689f6 --- /dev/null +++ b/internal/controller/loki/config/local-config.yaml @@ -0,0 +1,92 @@ +auth_enabled: false +server: + http_listen_port: 3100 + grpc_listen_port: 9096 + grpc_server_max_recv_msg_size: 10485760 + http_server_read_timeout: 1m + http_server_write_timeout: 1m + log_level: error +target: all +common: + path_prefix: /loki-store + storage: + filesystem: + chunks_directory: /loki-store/chunks + rules_directory: /loki-store/rules + replication_factor: 1 + ring: + instance_addr: 127.0.0.1 + kvstore: + store: inmemory +compactor: + compaction_interval: 5m + delete_request_store: filesystem + retention_enabled: true + retention_delete_delay: 2h + retention_delete_worker_count: 150 +frontend: + compress_responses: true +ingester: + chunk_encoding: snappy + chunk_retain_period: 1m +query_range: + align_queries_with_step: true + cache_results: true + max_retries: 5 + results_cache: + cache: + embedded_cache: + enabled: true + max_size_mb: 500 + ttl: 24h + compression: 'snappy' + parallelise_shardable_queries: true +query_scheduler: + max_outstanding_requests_per_tenant: 2048 +schema_config: + configs: + - from: 2020-05-15 + store: tsdb + object_store: filesystem + schema: v13 + index: + prefix: index_ + period: 24h +storage_config: + filesystem: + directory: /loki-store/storage + tsdb_shipper: + active_index_directory: /loki-store/index + cache_location: /loki-store/boltdb-cache + cache_ttl: 24h +limits_config: + ingestion_rate_strategy: global + ingestion_rate_mb: 10 + ingestion_burst_size_mb: 10 + max_label_name_length: 1024 + max_label_value_length: 2048 + max_label_names_per_series: 30 + reject_old_samples: true + reject_old_samples_max_age: 15m + creation_grace_period: 10m + max_line_size: 256000 + max_line_size_truncate: false + max_entries_limit_per_query: 10000 + max_streams_per_user: 0 + max_global_streams_per_user: 0 + unordered_writes: true + max_chunks_per_query: 2000000 + max_query_length: 721h + max_query_parallelism: 32 + max_query_series: 10000 + cardinality_limit: 100000 + max_streams_matchers_per_query: 1000 + max_concurrent_tail_requests: 10 + retention_period: 24h + max_cache_freshness_per_query: 5m + max_queriers_per_tenant: 0 + per_stream_rate_limit: 3MB + per_stream_rate_limit_burst: 15MB + max_query_lookback: 0 + min_sharding_lookback: 0s + split_queries_by_interval: 1m \ No newline at end of file diff --git a/internal/controller/loki/loki_objects.go b/internal/controller/loki/loki_objects.go new file mode 100644 index 000000000..93d7f892c --- /dev/null +++ b/internal/controller/loki/loki_objects.go @@ -0,0 +1,193 @@ +package loki + +import ( + "fmt" + "hash/fnv" + "strconv" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/utils/ptr" + + flowslatest "github.com/netobserv/network-observability-operator/api/flowcollector/v1beta2" + cfg "github.com/netobserv/network-observability-operator/internal/controller/loki/config" + + "github.com/netobserv/network-observability-operator/internal/controller/constants" + "github.com/netobserv/network-observability-operator/internal/controller/reconcilers" + "github.com/netobserv/network-observability-operator/internal/pkg/helper" + "github.com/netobserv/network-observability-operator/internal/pkg/volumes" +) + +const ( + configMapName = "loki-config" + configFile = "local-config.yaml" + configVolume = "loki-config" + configPath = "/etc/loki" + port = 3100 + storeVolume = "loki-store" + storePath = "/loki-store" +) + +type builder struct { + info *reconcilers.Instance + labels map[string]string + selector map[string]string + desired *flowslatest.FlowCollectorSpec + volumes volumes.Builder +} + +func newBuilder(info *reconcilers.Instance, desired *flowslatest.FlowCollectorSpec, name string) builder { + return builder{ + info: info, + labels: map[string]string{ + "app": name, + }, + selector: map[string]string{ + "app": name, + }, + desired: desired, + } +} + +func (b *builder) deployment(name, cm string) *appsv1.Deployment { + return &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: b.info.Namespace, + Labels: b.labels, + }, + Spec: appsv1.DeploymentSpec{ + Replicas: ptr.To(int32(1)), + Selector: &metav1.LabelSelector{ + MatchLabels: b.selector, + }, + Template: *b.podTemplate(name, cm), + }, + } +} + +func (b *builder) podTemplate(name, cmDigest string) *corev1.PodTemplateSpec { + annotations := map[string]string{} + volumes := []corev1.Volume{} + volumeMounts := []corev1.VolumeMount{} + + volumes = append(volumes, corev1.Volume{ + Name: storeVolume, + VolumeSource: corev1.VolumeSource{ + PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ + ClaimName: storeVolume, + }, + }, + }) + + volumeMounts = append(volumeMounts, corev1.VolumeMount{ + Name: storeVolume, + MountPath: storePath, + }) + + if cmDigest != "" { + annotations[constants.PodConfigurationDigest] = cmDigest + + volumes = append(volumes, corev1.Volume{ + Name: configVolume, + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: configMapName, + }, + }, + }, + }) + + volumeMounts = append(volumeMounts, corev1.VolumeMount{ + Name: configVolume, + MountPath: configPath, + ReadOnly: true, + }) + } + + return &corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: b.labels, + Annotations: annotations, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{ + Name: name, + Image: b.info.Images[reconcilers.MainImage], + Args: []string{ + fmt.Sprintf("-config.file=%s/%s", configPath, configFile), + }, + VolumeMounts: b.volumes.AppendMounts(volumeMounts), + SecurityContext: helper.ContainerDefaultSecurityContext(), + }}, + Volumes: b.volumes.AppendVolumes(volumes), + }, + } +} + +func (b *builder) service(name string) *corev1.Service { + return &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: b.info.Namespace, + Labels: b.labels, + }, + Spec: corev1.ServiceSpec{ + Selector: b.selector, + Ports: []corev1.ServicePort{{ + Port: port, + Protocol: corev1.ProtocolTCP, + // Some Kubernetes versions might automatically set TargetPort to Port. We need to + // explicitly set it here so the reconcile loop verifies that the owned service + // is equal as the desired service + TargetPort: intstr.FromInt32(port), + }}, + }, + } +} + +// returns a configmap with a digest of its configuration contents, which will be used to +// detect any configuration change +func (b *builder) configMap() (*corev1.ConfigMap, string, error) { + configStr := cfg.GetLokiConfigStr() + configMap := corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: configMapName, + Namespace: b.info.Namespace, + Labels: b.labels, + }, + Data: map[string]string{ + configFile: configStr, + }, + } + hasher := fnv.New64a() + _, err := hasher.Write([]byte(configStr)) + if err != nil { + return nil, "", err + } + digest := strconv.FormatUint(hasher.Sum64(), 36) + return &configMap, digest, nil +} + +func (b *builder) persistentVolumeClaim() *corev1.PersistentVolumeClaim { + return &corev1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: storeVolume, + Namespace: b.info.Namespace, + Labels: b.labels, + }, + Spec: corev1.PersistentVolumeClaimSpec{ + AccessModes: []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce}, + Resources: corev1.VolumeResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceStorage: resource.MustParse("10Gi"), + }, + }, + VolumeMode: ptr.To(corev1.PersistentVolumeFilesystem), + }, + } +} diff --git a/internal/controller/loki/loki_reconciler.go b/internal/controller/loki/loki_reconciler.go new file mode 100644 index 000000000..73d0e9a58 --- /dev/null +++ b/internal/controller/loki/loki_reconciler.go @@ -0,0 +1,131 @@ +package loki + +import ( + "context" + "reflect" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/log" + + flowslatest "github.com/netobserv/network-observability-operator/api/flowcollector/v1beta2" + "github.com/netobserv/network-observability-operator/internal/controller/constants" + "github.com/netobserv/network-observability-operator/internal/controller/reconcilers" + "github.com/netobserv/network-observability-operator/internal/pkg/helper" +) + +// LReconciler reconciles the current console plugin state with the desired configuration +type LReconciler struct { + *reconcilers.Instance + configMap *corev1.ConfigMap + deployment *appsv1.Deployment + pvc *corev1.PersistentVolumeClaim + service *corev1.Service +} + +func NewReconciler(cmn *reconcilers.Instance) LReconciler { + rec := LReconciler{ + Instance: cmn, + configMap: cmn.Managed.NewConfigMap(configMapName), + deployment: cmn.Managed.NewDeployment(constants.LokiDev), + pvc: cmn.Managed.NewPersistentVolumeClaim(storeVolume), + service: cmn.Managed.NewService(constants.LokiDev), + } + return rec +} + +// Reconcile is the reconciler entry point to reconcile the current plugin state with the desired configuration +func (r *LReconciler) Reconcile(ctx context.Context, desired *flowslatest.FlowCollector) error { + l := log.FromContext(ctx).WithName("loki") + ctx = log.IntoContext(ctx, l) + + // Retrieve current owned objects + err := r.Managed.FetchAll(ctx) + if err != nil { + return err + } + + if desired.Spec.UseLokiDev() { + // Create object builder + builder := newBuilder(r.Instance, &desired.Spec, constants.LokiDev) + + cmDigest, err := r.reconcileConfigMap(ctx, &builder) + if err != nil { + return err + } + + if err = r.reconcilePVC(ctx, &builder); err != nil { + return err + } + + if err = r.reconcileDeployment(ctx, &builder, constants.LokiDev, cmDigest); err != nil { + return err + } + + if err = r.reconcileServices(ctx, &builder, constants.LokiDev); err != nil { + return err + } + } else { + // delete any existing owned object + r.Managed.TryDeleteAll(ctx) + } + + return nil +} + +func (r *LReconciler) reconcileConfigMap(ctx context.Context, builder *builder) (string, error) { + newCM, configDigest, err := builder.configMap() + if err != nil { + return "", err + } + if !r.Managed.Exists(r.configMap) { + if err := r.CreateOwned(ctx, newCM); err != nil { + return "", err + } + } else if !reflect.DeepEqual(newCM.Data, r.configMap.Data) { + if err := r.UpdateIfOwned(ctx, r.configMap, newCM); err != nil { + return "", err + } + } + return configDigest, nil +} + +func (r *LReconciler) reconcilePVC(ctx context.Context, builder *builder) error { + pvc := builder.persistentVolumeClaim() + if !r.Managed.Exists(r.pvc) { + if err := r.CreateOwned(ctx, pvc); err != nil { + return err + } + } else if !reflect.DeepEqual(pvc, r.pvc) { + if err := r.UpdateIfOwned(ctx, r.pvc, pvc); err != nil { + return err + } + } + return nil +} + +func (r *LReconciler) reconcileDeployment(ctx context.Context, builder *builder, name string, cm string) error { + report := helper.NewChangeReport("Loki deployment") + defer report.LogIfNeeded(ctx) + + return reconcilers.ReconcileDeployment( + ctx, + r.Instance, + r.deployment, + builder.deployment(name, cm), + name, + 1, + nil, + &report, + ) +} + +func (r *LReconciler) reconcileServices(ctx context.Context, builder *builder, name string) error { + report := helper.NewChangeReport("Loki services") + defer report.LogIfNeeded(ctx) + + if err := r.ReconcileService(ctx, r.service, builder.service(name), &report); err != nil { + return err + } + return nil +} diff --git a/internal/controller/reconcilers/namespaced_objects_manager.go b/internal/controller/reconcilers/namespaced_objects_manager.go index e5dc742d8..45b92fd3f 100644 --- a/internal/controller/reconcilers/namespaced_objects_manager.go +++ b/internal/controller/reconcilers/namespaced_objects_manager.go @@ -54,6 +54,12 @@ func (m *NamespacedObjectManager) NewConfigMap(name string) *corev1.ConfigMap { return &cm } +func (m *NamespacedObjectManager) NewPersistentVolumeClaim(name string) *corev1.PersistentVolumeClaim { + pvc := corev1.PersistentVolumeClaim{} + m.AddManagedObject(name, &pvc) + return &pvc +} + func (m *NamespacedObjectManager) NewDeployment(name string) *appsv1.Deployment { d := appsv1.Deployment{} m.AddManagedObject(name, &d) diff --git a/internal/pkg/helper/loki_config.go b/internal/pkg/helper/loki_config.go index 916bfc6e3..2cb6baf75 100644 --- a/internal/pkg/helper/loki_config.go +++ b/internal/pkg/helper/loki_config.go @@ -52,12 +52,21 @@ func NewLokiConfig(spec *flowslatest.FlowCollectorLoki, namespace string) LokiCo }, } case flowslatest.LokiModeMonolithic: - loki.LokiManualParams = flowslatest.LokiManualParams{ - QuerierURL: spec.Monolithic.URL, - IngesterURL: spec.Monolithic.URL, - StatusURL: spec.Monolithic.URL, - TenantID: spec.Monolithic.TenantID, - TLS: spec.Monolithic.TLS, + if *spec.Monolithic.ZeroClick { + loki.LokiManualParams = flowslatest.LokiManualParams{ + QuerierURL: "http://loki:3100/", + IngesterURL: "http://loki:3100/", + TenantID: "netobserv", + AuthToken: flowslatest.LokiAuthDisabled, + } + } else { + loki.LokiManualParams = flowslatest.LokiManualParams{ + QuerierURL: spec.Monolithic.URL, + IngesterURL: spec.Monolithic.URL, + StatusURL: spec.Monolithic.URL, + TenantID: spec.Monolithic.TenantID, + TLS: spec.Monolithic.TLS, + } } case flowslatest.LokiModeMicroservices: loki.LokiManualParams = flowslatest.LokiManualParams{ diff --git a/internal/pkg/manager/config.go b/internal/pkg/manager/config.go index 56679d33a..086e2729c 100644 --- a/internal/pkg/manager/config.go +++ b/internal/pkg/manager/config.go @@ -6,6 +6,8 @@ import ( // Config of the operator. type Config struct { + // ZeroClickLokiImage is the image of the zero click loki deployment that is managed by the operator + ZeroClickLokiImage string // EBPFAgentImage is the image of the eBPF agent that is managed by the operator EBPFAgentImage string // FlowlogsPipelineImage is the image of the Flowlogs-Pipeline that is managed by the operator diff --git a/internal/pkg/manager/manager.go b/internal/pkg/manager/manager.go index 707c1cc84..cc42ab247 100644 --- a/internal/pkg/manager/manager.go +++ b/internal/pkg/manager/manager.go @@ -17,7 +17,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/manager" ) -//+kubebuilder:rbac:groups=core,resources=namespaces;services;serviceaccounts;configmaps;secrets,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=core,resources=namespaces;services;serviceaccounts;configmaps;persistentvolumeclaims;secrets,verbs=get;list;watch;create;update;patch;delete //+kubebuilder:rbac:groups=core,resources=pods;nodes;endpoints,verbs=get;list;watch //+kubebuilder:rbac:groups=apps,resources=deployments;daemonsets,verbs=get;list;watch;create;update;patch;delete //+kubebuilder:rbac:groups=apps,resources=replicasets,verbs=get;list;watch diff --git a/main.go b/main.go index 7afff61b5..d16caedbf 100644 --- a/main.go +++ b/main.go @@ -114,6 +114,7 @@ func main() { flag.StringVar(&config.ConsolePluginCompatImage, "console-plugin-compat-image", "quay.io/netobserv/network-observability-console-plugin-pf4:main", "A backward compatible image of the Console Plugin (e.g. Patterfly 4 variant)") flag.StringVar(&config.EBPFByteCodeImage, "ebpf-bytecode-image", "quay.io/netobserv/ebpf-bytecode:main", "The EBPF bytecode for the eBPF agent") flag.StringVar(&config.Namespace, "namespace", "netobserv", "Current controller namespace") + flag.StringVar(&config.ZeroClickLokiImage, "zero-click-loki-image", "grafana/loki:3.5.0", "The image of the zero click loki deployment") flag.BoolVar(&config.DownstreamDeployment, "downstream-deployment", false, "Either this deployment is a downstream deployment ot not") flag.BoolVar(&enableHTTP2, "enable-http2", enableHTTP2, "If HTTP/2 should be enabled for the metrics and webhook servers.") flag.BoolVar(&versionFlag, "v", false, "print version")