Skip to content
Draft
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
11 changes: 9 additions & 2 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,11 +107,11 @@ func main() {
// as certificates issued by a trusted Certificate Authority (CA). The primary risk is potentially allowing
// unauthorized access to sensitive metrics data. Consider replacing with CertDir, CertName, and KeyName
// to provide certificates, ensuring the server communicates using trusted and secure certificates.
TLSOpts: tlsOpts,
TLSOpts: tlsOpts,
FilterProvider: filters.WithAuthenticationAndAuthorization,
}

// TODO: enable https for /metrics endpoint by default
// TODO: enable https for /metrics endpoint by default
// if secureMetrics {
// // FilterProvider is used to protect the metrics endpoint with authn/authz.
// // These configurations ensure that only authorized users and service accounts
Expand Down 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
4 changes: 4 additions & 0 deletions config/manager/controller_manager_telemetry.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: manager-telemetry
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
110 changes: 110 additions & 0 deletions internal/controller/telemetry_controller.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/*
Copyright (c) 2018-2022 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"
enterprise "github.com/splunk/splunk-operator/pkg/splunk/enterprise"
ctrl "sigs.k8s.io/controller-runtime"
"time"

"github.com/pkg/errors"
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 (
// TODO: Below two contants are defined at default/kustomizatio.yaml, need to get it programatically?
ConfigMapNamePrefix = "splunk-operator-"
ConfigMapLabelName = "splunk-operator"

telemetryRetryDelay = time.Second * 60
)

// TelemetryReconciler periodically reads all keys under the "telemetry" configmap
// in the Splunk operator namespace and logs all key values.
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")

// 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
}
return ctrl.Result{}, errors.Wrap(err, "could not load telemetry configmap")
}

// Log all key/value pairs. No sorting per your request.
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")
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)
}
64 changes: 64 additions & 0 deletions internal/controller/telemetry_controller_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
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 * 60))
})

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 * 60))
})

// Additional tests for error and success cases can be added here
})

/*
func TestTelemetryController(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Telemetry Controller Suite")
}

*/
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
30 changes: 2 additions & 28 deletions pkg/splunk/enterprise/afwscheduler.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,26 +138,6 @@ func runCustomCommandOnSplunkPods(ctx context.Context, cr splcommon.MetaObject,
return err
}

// Get extension for name of telemetry app
func getTelAppNameExtension(crKind string) (string, error) {
switch crKind {
case "Standalone":
return "stdaln", nil
case "LicenseMaster":
return "lmaster", nil
case "LicenseManager":
return "lmanager", nil
case "SearchHeadCluster":
return "shc", nil
case "ClusterMaster":
return "cmaster", nil
case "ClusterManager":
return "cmanager", nil
default:
return "", errors.New("Invalid CR kind for telemetry app")
}
}

// addTelApp adds a telemetry app
var addTelApp = func(ctx context.Context, podExecClient splutil.PodExecClientImpl, replicas int32, cr splcommon.MetaObject) error {
var err error
Expand All @@ -170,26 +150,20 @@ var addTelApp = func(ctx context.Context, podExecClient splutil.PodExecClientImp
// Create pod exec client
crKind := cr.GetObjectKind().GroupVersionKind().Kind

// Get Tel App Name Extension
appNameExt, err := getTelAppNameExtension(crKind)
if err != nil {
return err
}

// Commands to run on pods
var command1, command2 string

// Handle non SHC scenarios(Standalone, CM, LM)
if crKind != "SearchHeadCluster" {
// Create dir on pods
command1 = fmt.Sprintf(createTelAppNonShcString, appNameExt, appNameExt, telAppConfString, appNameExt, telAppDefMetaConfString, appNameExt)
command1 = fmt.Sprintf(createTelAppNonShcString, telAppConfString, telAppDefMetaConfString)

// App reload
command2 = telAppReloadString

} else {
// Create dir on pods
command1 = fmt.Sprintf(createTelAppShcString, shcAppsLocationOnDeployer, appNameExt, shcAppsLocationOnDeployer, appNameExt, telAppConfString, shcAppsLocationOnDeployer, appNameExt, telAppDefMetaConfString, shcAppsLocationOnDeployer, appNameExt)
command1 = fmt.Sprintf(createTelAppShcString, shcAppsLocationOnDeployer, shcAppsLocationOnDeployer, telAppConfString, shcAppsLocationOnDeployer, telAppDefMetaConfString, shcAppsLocationOnDeployer)

// Bundle push
command2 = fmt.Sprintf(applySHCBundleCmdStr, GetSplunkStatefulsetURL(cr.GetNamespace(), SplunkSearchHead, cr.GetName(), 0, false), "/tmp/status.txt")
Expand Down
Loading
Loading