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")