Skip to content
48 changes: 47 additions & 1 deletion docs/AppFramework.md
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,53 @@ Here is a typical App framework configuration in a Custom resource definition:

### appsRepoPollIntervalSeconds

`appsRepoPollIntervalSeconds` helps configure the polling interval(in seconds) to detect addition or modification of apps on the Remote Storage
The App Framework uses the polling interval `appsRepoPollIntervalSeconds` to check for additional apps, or modified apps on the remote object storage. If app framework is enabled, the Splunk Operator creates a namespace scoped configMap named **splunk-\<namespace\>-manual-app-update**, which is used to manually trigger the app updates. When `appsRepoPollIntervalSeconds` is set to `0` for a CR, the App Framework will not perform a check until the configMap `status` field is updated manually. See [Manual initiation of app management](#manual_initiation_of_app_management).


## Manual initiation of app management
You can prevent the App Framework from automatically polling the remote storage for changes. By setting the CR setting `appsRepoPollIntervalSeconds` to `0`, the App Framework polling is disabled, and the configMap is updated with a new `status` field. The App Framework always performs an initial poll of the remote storage, even when the CR is initialized with polling disabled.

The 'status' field defaults to 'off'. When you're ready to initiate an app check using the App Framework, manually update the `status` field in the configMap for that CR type to `on`.
For example, you deployed one Standalone CR with app framework enabled.

```
kubectl get standalone
NAME PHASE DESIRED READY AGE
s1 Ready 1 1 13h
```
As mentioned above, Splunk Operator will create the configMap(assuming `default` namespace) `splunk-default-manual-app-update` with an entry for Standalone CR as below -

```yaml
apiVersion: v1
data:
Standalone: |-
status: off
refCount: 1
kind: ConfigMap
metadata:
creationTimestamp: "2021-08-24T01:04:01Z"
name: splunk-manual-app-update
namespace: default
ownerReferences:
- apiVersion: enterprise.splunk.com/v2
controller: false
kind: Standalone
name: s1
uid: ddb9528f-2e25-49be-acd4-4fadde489849
resourceVersion: "75406013"
selfLink: /api/v1/namespaces/default/configmaps/splunk-manual-app-update
uid: 413c6053-af4f-4cb3-97e0-6dbe7cd17721
```

To trigger manual checking of app/s, update the configMap and set the `status` field to `on` for the Standalone CR as below:

```kubectl patch cm/splunk-default-manual-app-update --type merge -p '{"data":{"Standalone":"status: on\nrefCount: 1"}}'```

The App Framework will perform its checks and updates, and reset the `status` to `off` when it has completed its tasks.

To re-enable the polling, update the CR `appsRepoPollIntervalSeconds` setting to a value greater than 0.

NOTE: All CR's of the same type must have polling enabled, or disabled. For example, if `appsRepoPollIntervalSeconds` is set to '0' for one Standalone CR, all other Standalone CRs must also have polling disabled. Use the `kubectl` command to identify all CR's of the same type before updating the polling interval. We can have unexpected behavior of polling if we have CRs with a mix of polling enabled and disabled.

## Impact of livenessInitialDelaySeconds and readinessInitialDelaySeconds

Expand Down
2 changes: 1 addition & 1 deletion pkg/controller/add_clustermaster.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ func (ctrl ClusterMasterController) GetInstance() splcommon.MetaObject {

// GetWatchTypes returns a list of types owned by the controller that it would like to receive watch events for
func (ctrl ClusterMasterController) GetWatchTypes() []runtime.Object {
return []runtime.Object{&appsv1.StatefulSet{}, &corev1.Secret{}}
return []runtime.Object{&appsv1.StatefulSet{}, &corev1.Secret{}, &corev1.ConfigMap{}}
}

// Reconcile is used to perform an idempotent reconciliation of the custom resource managed by this controller
Expand Down
2 changes: 1 addition & 1 deletion pkg/controller/add_licensemaster.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ func (ctrl LicenseMasterController) GetInstance() splcommon.MetaObject {

// GetWatchTypes returns a list of types owned by the controller that it would like to receive watch events for
func (ctrl LicenseMasterController) GetWatchTypes() []runtime.Object {
return []runtime.Object{&appsv1.StatefulSet{}, &corev1.Secret{}}
return []runtime.Object{&appsv1.StatefulSet{}, &corev1.Secret{}, &corev1.ConfigMap{}}
}

// Reconcile is used to perform an idempotent reconciliation of the custom resource managed by this controller
Expand Down
2 changes: 1 addition & 1 deletion pkg/controller/add_searchheadcluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ func (ctrl SearchHeadClusterController) GetInstance() splcommon.MetaObject {

// GetWatchTypes returns a list of types owned by the controller that it would like to receive watch events for
func (ctrl SearchHeadClusterController) GetWatchTypes() []runtime.Object {
return []runtime.Object{&appsv1.StatefulSet{}, &corev1.Secret{}}
return []runtime.Object{&appsv1.StatefulSet{}, &corev1.Secret{}, &corev1.ConfigMap{}}
}

// Reconcile is used to perform an idempotent reconciliation of the custom resource managed by this controller
Expand Down
2 changes: 1 addition & 1 deletion pkg/controller/add_standalone.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ func (ctrl StandaloneController) GetInstance() splcommon.MetaObject {

// GetWatchTypes returns a list of types owned by the controller that it would like to receive watch events for
func (ctrl StandaloneController) GetWatchTypes() []runtime.Object {
return []runtime.Object{&appsv1.StatefulSet{}, &corev1.Secret{}}
return []runtime.Object{&appsv1.StatefulSet{}, &corev1.Secret{}, &corev1.ConfigMap{}}
}

// Reconcile is used to perform an idempotent reconciliation of the custom resource managed by this controller
Expand Down
2 changes: 2 additions & 0 deletions pkg/splunk/controller/statefulset.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ type DefaultStatefulSetPodManager struct{}

// Update for DefaultStatefulSetPodManager handles all updates for a statefulset of standard pods
func (mgr *DefaultStatefulSetPodManager) Update(client splcommon.ControllerClient, statefulSet *appsv1.StatefulSet, desiredReplicas int32) (splcommon.Phase, error) {

phase, err := ApplyStatefulSet(client, statefulSet)

if err == nil && phase == splcommon.PhaseReady {
phase, err = UpdateStatefulSetPods(client, statefulSet, mgr, desiredReplicas)
}
Expand Down
28 changes: 28 additions & 0 deletions pkg/splunk/enterprise/clustermaster.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (

enterpriseApi "github.com/splunk/splunk-operator/pkg/apis/enterprise/v2"
appsv1 "k8s.io/api/apps/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/reconcile"

splclient "github.com/splunk/splunk-operator/pkg/splunk/client"
Expand Down Expand Up @@ -106,6 +107,16 @@ func ApplyClusterMaster(client splcommon.ControllerClient, cr *enterpriseApi.Clu
return result, err
}

// If this is the last of its kind getting deleted,
// remove the entry for this CR type from configMap or else
// just decrement the refCount for this CR type.
if len(cr.Spec.AppFrameworkConfig.AppSources) != 0 {
err = UpdateOrRemoveEntryFromConfigMap(client, cr, SplunkClusterMaster)
if err != nil {
return result, err
}
}

DeleteOwnerReferencesForResources(client, cr, &cr.Spec.SmartStore)
terminating, err := splctrl.CheckForDeletion(cr, client)
if terminating && err != nil { // don't bother if no error, since it will just be removed immmediately after
Expand Down Expand Up @@ -301,3 +312,20 @@ func PushMasterAppsBundle(c splcommon.ControllerClient, cr *enterpriseApi.Cluste

return splunkClient.BundlePush(true)
}

// helper function to get the list of ClusterMaster types in the current namespace
func getClusterMasterList(c splcommon.ControllerClient, cr splcommon.MetaObject, listOpts []client.ListOption) (int, error) {
scopedLog := log.WithName("getClusterMasterList").WithValues("name", cr.GetName(), "namespace", cr.GetNamespace())

objectList := enterpriseApi.ClusterMasterList{}

err := c.List(context.TODO(), &objectList, listOpts...)
numOfObjects := len(objectList.Items)

if err != nil {
scopedLog.Error(err, "ClusterMaster types not found in namespace", "namsespace", cr.GetNamespace())
return numOfObjects, err
}

return numOfObjects, nil
}
116 changes: 116 additions & 0 deletions pkg/splunk/enterprise/clustermaster_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -441,8 +441,12 @@ func TestAppFrameworkApplyClusterMasterShouldNotFail(t *testing.T) {
Name: "stack1",
Namespace: "test",
},
TypeMeta: metav1.TypeMeta{
Kind: "ClusterMaster",
},
Spec: enterpriseApi.ClusterMasterSpec{
AppFrameworkConfig: enterpriseApi.AppFrameworkSpec{
AppsRepoPollInterval: 60,
VolList: []enterpriseApi.VolumeSpec{
{Name: "msos_s2s3_vol",
Endpoint: "https://s3-eu-west-2.amazonaws.com",
Expand Down Expand Up @@ -494,6 +498,88 @@ func TestAppFrameworkApplyClusterMasterShouldNotFail(t *testing.T) {
}
}

func TestApplyCLusterMasterDeletion(t *testing.T) {
cm := enterpriseApi.ClusterMaster{
ObjectMeta: metav1.ObjectMeta{
Name: "stack1",
Namespace: "test",
},
TypeMeta: metav1.TypeMeta{
Kind: "ClusterMaster",
},
Spec: enterpriseApi.ClusterMasterSpec{
AppFrameworkConfig: enterpriseApi.AppFrameworkSpec{
AppsRepoPollInterval: 0,
VolList: []enterpriseApi.VolumeSpec{
{Name: "msos_s2s3_vol",
Endpoint: "https://s3-eu-west-2.amazonaws.com",
Path: "testbucket-rs-london",
SecretRef: "s3-secret",
Type: "s3",
Provider: "aws"},
},
AppSources: []enterpriseApi.AppSourceSpec{
{Name: "adminApps",
Location: "adminAppsRepo",
AppSourceDefaultSpec: enterpriseApi.AppSourceDefaultSpec{
VolName: "msos_s2s3_vol",
Scope: enterpriseApi.ScopeLocal},
},
{Name: "securityApps",
Location: "securityAppsRepo",
AppSourceDefaultSpec: enterpriseApi.AppSourceDefaultSpec{
VolName: "msos_s2s3_vol",
Scope: enterpriseApi.ScopeLocal},
},
{Name: "authenticationApps",
Location: "authenticationAppsRepo",
AppSourceDefaultSpec: enterpriseApi.AppSourceDefaultSpec{
VolName: "msos_s2s3_vol",
Scope: enterpriseApi.ScopeLocal},
},
},
},
CommonSplunkSpec: enterpriseApi.CommonSplunkSpec{
Mock: true,
},
},
}

c := spltest.NewMockClient()

// Create S3 secret
s3Secret := spltest.GetMockS3SecretKeys("s3-secret")

c.AddObject(&s3Secret)

// Create namespace scoped secret
_, err := splutil.ApplyNamespaceScopedSecretObject(c, "test")
if err != nil {
t.Errorf(err.Error())
}

// test deletion
currentTime := metav1.NewTime(time.Now())
cm.ObjectMeta.DeletionTimestamp = &currentTime
cm.ObjectMeta.Finalizers = []string{"enterprise.splunk.com/delete-pvc"}

pvclist := corev1.PersistentVolumeClaimList{
Items: []corev1.PersistentVolumeClaim{
{
ObjectMeta: metav1.ObjectMeta{
Name: "splunk-pvc-stack1-var",
Namespace: "test",
},
},
},
}
c.ListObj = &pvclist

_, err = ApplyClusterMaster(c, &cm)
if err != nil {
t.Errorf("ApplyClusterMaster should not have returned error here.")
}
}
func TestClusterMasterGetAppsListForAWSS3ClientShouldNotFail(t *testing.T) {
cm := enterpriseApi.ClusterMaster{
ObjectMeta: metav1.ObjectMeta{
Expand Down Expand Up @@ -821,3 +907,33 @@ func TestClusterMasterGetAppsListForAWSS3ClientShouldFail(t *testing.T) {
t.Errorf("GetAppsList should have returned error as we have empty objects in MockAWSS3Client")
}
}

func TestGetClusterMasterList(t *testing.T) {
cm := enterpriseApi.ClusterMaster{}

listOpts := []client.ListOption{
client.InNamespace("test"),
}

client := spltest.NewMockClient()

// Invalid scenario since we haven't added clustermaster to the list yet
numOfObjects, err := getClusterMasterList(client, &cm, listOpts)
if err == nil {
t.Errorf("getNumOfObjects should have returned error as we haven't added standalone to the list yet")
}

cmList := &enterpriseApi.ClusterMasterList{}
cmList.Items = append(cmList.Items, cm)

client.ListObj = cmList

numOfObjects, err = getClusterMasterList(client, &cm, listOpts)
if err != nil {
t.Errorf("getNumOfObjects should not have returned error=%v", err)
}

if numOfObjects != 1 {
t.Errorf("Got wrong number of ClusterMaster objects. Expected=%d, Got=%d", 1, numOfObjects)
}
}
Loading