Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion .github/workflows/pre-release-workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,14 @@ jobs:
replace: "SPLUNK_ENTERPRISE_IMAGE"
include: "config/default/kustomization.yaml"

- name: Update Telemetry Test Value
uses: jacobtomlinson/gha-find-replace@v3
with:
find: '"test"\s*:\s*"[^"]*"'
replace: '"test": "false"'
isRegexp: true
include: 'config/manager/controller_manager_telemetry.yaml'

- name: Reset files before creating Pull Request
run: |
git checkout go.sum
Expand All @@ -249,4 +257,4 @@ jobs:
body: |
### Automated Pull Request for Splunk Operator Release ${{ github.event.inputs.release_version }}
* Changes added to docs/ChangeLog-NEW.md. Please filter and update ChangeLog.md
* Delete ChangeLog-New.md
* Delete ChangeLog-New.md
6 changes: 5 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ deploy: manifests kustomize uninstall ## Deploy controller to the K8s cluster sp
$(SED) "s/value: WATCH_NAMESPACE_VALUE/value: \"${WATCH_NAMESPACE}\"/g" config/${ENVIRONMENT}/kustomization.yaml
$(SED) "s|SPLUNK_ENTERPRISE_IMAGE|${SPLUNK_ENTERPRISE_IMAGE}|g" config/${ENVIRONMENT}/kustomization.yaml
$(SED) "s/value: SPLUNK_GENERAL_TERMS_VALUE/value: \"${SPLUNK_GENERAL_TERMS}\"/g" config/${ENVIRONMENT}/kustomization.yaml
$(SED) 's/\("sokVersion": \)"[^"]*"/\1"$(VERSION)"/' config/manager/controller_manager_telemetry.yaml
cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG}
RELATED_IMAGE_SPLUNK_ENTERPRISE=${SPLUNK_ENTERPRISE_IMAGE} WATCH_NAMESPACE=${WATCH_NAMESPACE} SPLUNK_GENERAL_TERMS=${SPLUNK_GENERAL_TERMS} $(KUSTOMIZE) build config/${ENVIRONMENT} | kubectl apply --server-side --force-conflicts -f -
$(SED) "s/namespace: ${NAMESPACE}/namespace: splunk-operator/g" config/${ENVIRONMENT}/kustomization.yaml
Expand Down Expand Up @@ -354,6 +355,7 @@ run_clair_scan:

# generate artifacts needed to deploy operator, this is current way of doing it, need to fix this
generate-artifacts-namespace: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config.
$(SED) 's/\("sokVersion": \)"[^"]*"/\1"$(VERSION)"/' config/manager/controller_manager_telemetry.yaml
mkdir -p release-${VERSION}
cp config/default/kustomization-namespace.yaml config/default/kustomization.yaml
cp config/rbac/kustomization-namespace.yaml config/rbac/kustomization.yaml
Expand All @@ -369,6 +371,7 @@ generate-artifacts-namespace: manifests kustomize ## Deploy controller to the K8

# generate artifacts needed to deploy operator, this is current way of doing it, need to fix this
generate-artifacts-cluster: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config.
$(SED) 's/\("sokVersion": \)"[^"]*"/\1"$(VERSION)"/' config/manager/controller_manager_telemetry.yaml
mkdir -p release-${VERSION}
cp config/default/kustomization-cluster.yaml config/default/kustomization.yaml
cp config/rbac/kustomization-cluster.yaml config/rbac/kustomization.yaml
Expand Down Expand Up @@ -428,4 +431,5 @@ setup/ginkgo:
build-installer: manifests generate kustomize
mkdir -p dist
cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG}
$(KUSTOMIZE) build config/default > dist/install.yaml
$(KUSTOMIZE) build config/default > dist/install.yaml

7 changes: 7 additions & 0 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,13 @@ func main() {
setupLog.Error(err, "unable to create controller", "controller", "Standalone")
os.Exit(1)
}
if err = (&intController.TelemetryReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "Telemetry")
os.Exit(1)
}
//+kubebuilder:scaffold:builder

if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
Expand Down
11 changes: 11 additions & 0 deletions config/manager/controller_manager_telemetry.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: manager-telemetry
data:
status: |
{
"lastTransmission": "",
"test": "true",
"sokVersion": ""
}
1 change: 1 addition & 0 deletions config/manager/kustomization.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
resources:
- manager.yaml
- controller_manager_telemetry.yaml

generatorOptions:
disableNameSuffixHash: true
Expand Down
113 changes: 113 additions & 0 deletions internal/controller/telemetry_controller.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/*
Copyright (c) 2026 Splunk Inc. All rights reserved.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package controller

import (
"context"
"fmt"
enterprise "github.com/splunk/splunk-operator/pkg/splunk/enterprise"
ctrl "sigs.k8s.io/controller-runtime"
"time"

metrics "github.com/splunk/splunk-operator/pkg/splunk/client/metrics"

corev1 "k8s.io/api/core/v1"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"

"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller"
"sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/predicate"
)

const (
// Below two contants are defined at kustomizatio*.yaml
ConfigMapNamePrefix = "splunk-operator-"
ConfigMapLabelName = "splunk-operator"

telemetryRetryDelay = time.Second * 600
)

type TelemetryReconciler struct {
client.Client
Scheme *runtime.Scheme
}

//+kubebuilder:rbac:groups=core,resources=configmaps,verbs=get;list;watch

func (r *TelemetryReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
metrics.ReconcileCounters.With(metrics.GetPrometheusLabels(req, "Telemetry")).Inc()
defer recordInstrumentionData(time.Now(), req, "controller", "Telemetry")

reqLogger := log.FromContext(ctx)
reqLogger = reqLogger.WithValues("telemetry", req.NamespacedName)

reqLogger.Info("Reconciling telemetry")

defer func() {
if rec := recover(); rec != nil {
reqLogger.Error(fmt.Errorf("panic: %v", rec), "Recovered from panic in TelemetryReconciler.Reconcile")
}
}()

// Fetch the ConfigMap
cm := &corev1.ConfigMap{}
err := r.Get(ctx, req.NamespacedName, cm)
if err != nil {
if k8serrors.IsNotFound(err) {
reqLogger.Info("telemetry configmap not found; requeueing", "period(seconds)", int(telemetryRetryDelay/time.Second))
return ctrl.Result{Requeue: true, RequeueAfter: telemetryRetryDelay}, nil
}
reqLogger.Error(err, "could not load telemetry configmap; requeueing", "period(seconds)", int(telemetryRetryDelay/time.Second))
return ctrl.Result{Requeue: true, RequeueAfter: telemetryRetryDelay}, nil
}

if len(cm.Data) == 0 {
reqLogger.Info("telemetry configmap has no data keys")
return ctrl.Result{Requeue: true, RequeueAfter: telemetryRetryDelay}, nil
}

reqLogger.Info("start", "Telemetry configmap version", cm.GetResourceVersion())

result, err := enterprise.ApplyTelemetry(ctx, r.Client, cm)
if err != nil {
reqLogger.Error(err, "Failed to send telemetry")
return ctrl.Result{Requeue: true, RequeueAfter: telemetryRetryDelay}, nil
}
if result.Requeue && result.RequeueAfter != 0 {
reqLogger.Info("Requeued", "period(seconds)", int(result.RequeueAfter/time.Second))
}

return result, err
}

// SetupWithManager sets up the controller with the Manager.
func (r *TelemetryReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&corev1.ConfigMap{}).
WithEventFilter(predicate.NewPredicateFuncs(func(obj client.Object) bool {
labels := obj.GetLabels()
if labels == nil {
return false
}
return obj.GetName() == enterprise.GetTelemetryConfigMapName(ConfigMapNamePrefix) && labels["name"] == ConfigMapLabelName
})).
WithOptions(controller.Options{
MaxConcurrentReconciles: 1,
}).
Complete(r)
}
69 changes: 69 additions & 0 deletions internal/controller/telemetry_controller_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
Copyright (c) 2026 Splunk Inc. All rights reserved.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/package controller

import (
"context"
"time"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/kubernetes/scheme"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
)

var _ = Describe("Telemetry Controller", func() {
var (
ctx context.Context
cmName = "splunk-operator-telemetry"
ns = "test-telemetry-ns"
labels = map[string]string{"name": "splunk-operator"}
)

BeforeEach(func() {
ctx = context.TODO()
})

It("Reconcile returns requeue when ConfigMap not found", func() {
builder := fake.NewClientBuilder().WithScheme(scheme.Scheme)
c := builder.Build()
r := &TelemetryReconciler{Client: c, Scheme: scheme.Scheme}
req := reconcile.Request{NamespacedName: types.NamespacedName{Name: cmName, Namespace: ns}}
result, err := r.Reconcile(ctx, req)
Expect(err).To(BeNil())
Expect(result.Requeue).To(BeTrue())
Expect(result.RequeueAfter).To(Equal(time.Second * 600))
})

It("Reconcile returns requeue when ConfigMap has no data", func() {
cm := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{Name: cmName, Namespace: ns, Labels: labels},
Data: map[string]string{},
}
builder := fake.NewClientBuilder().WithScheme(scheme.Scheme).WithObjects(cm)
c := builder.Build()
r := &TelemetryReconciler{Client: c, Scheme: scheme.Scheme}
req := reconcile.Request{NamespacedName: types.NamespacedName{Name: cmName, Namespace: ns}}
result, err := r.Reconcile(ctx, req)
Expect(err).To(BeNil())
Expect(result.Requeue).To(BeTrue())
Expect(result.RequeueAfter).To(Equal(time.Second * 600))
})

})
44 changes: 44 additions & 0 deletions pkg/splunk/client/enterprise.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package client

import (
"bytes"
"crypto/tls"
"encoding/json"
"fmt"
Expand Down Expand Up @@ -954,6 +955,49 @@ func (c *SplunkClient) SetIdxcSecret(idxcSecret string) error {
return c.Do(request, expectedStatus, nil)
}

type LicenseInfo struct {
ID string `json:"guid"`
Type string `json:"type"`
}

func (c *SplunkClient) GetLicenseInfo() (*LicenseInfo, error) {
apiResponse := struct {
Entry []struct {
Content LicenseInfo `json:"content"`
} `json:"entry"`
}{}
path := "/services/licenser/licenses"
err := c.Get(path, &apiResponse)
if err != nil {
return nil, err
}
if len(apiResponse.Entry) < 1 {
return nil, fmt.Errorf("invalid response from %s%s", c.ManagementURI, path)
}
return &apiResponse.Entry[0].Content, nil
}

type TelemetryResponse struct {
Message string `json:"message"`
MetricValueID string `json:"metricValueId"`
}

func (c *SplunkClient) SendTelemetry(path string, body []byte) (*TelemetryResponse, error) {
endpoint := fmt.Sprintf("%s%s", c.ManagementURI, path)
request, err := http.NewRequest("POST", endpoint, bytes.NewReader(body))
if err != nil {
return nil, err
}
request.Header.Set("Content-Type", "application/json")
expectedStatus := []int{201}
var response TelemetryResponse
err = c.Do(request, expectedStatus, &response)
if err != nil {
return nil, err
}
return &response, nil
}

// RestartSplunk restarts specific Splunk instance
// Can be used for any Splunk Instance
// See https://docs.splunk.com/Documentation/Splunk/latest/RESTREF/RESTsystem#server.2Fcontrol.2Frestart
Expand Down
Loading
Loading