From b5ac52a58fcdaea0f9e4662053a2825fd6879bd8 Mon Sep 17 00:00:00 2001 From: Jay Koby <297441+jkoby@users.noreply.github.com> Date: Wed, 16 Dec 2020 13:11:04 -0500 Subject: [PATCH 01/43] Add date to 0.2.1 Beta in changelog --- docs/ChangeLog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/ChangeLog.md b/docs/ChangeLog.md index a9b41904c..47fba97e6 100644 --- a/docs/ChangeLog.md +++ b/docs/ChangeLog.md @@ -1,6 +1,6 @@ # Splunk Operator for Kubernetes Change Log -## 0.2.1 Beta() +## 0.2.1 Beta(2020-12-15) * This release depends upon changes made concurrently in the Splunk Enterprise container images. You must use the latest splunk/splunk:edge nightly image with it, or alternatively any release version 8.1.0 or later * CSPL-529 - Fixed incorrect deletion of Indexer PVCs upon deletion of ClusterMaster From 5cf8f7437fd78d9340055dd43d35578268a863c6 Mon Sep 17 00:00:00 2001 From: Bill Chung Date: Tue, 1 Dec 2020 16:05:40 +0900 Subject: [PATCH 02/43] fix IndexerCluster clusterMasterRef yaml in doc --- docs/CustomResources.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/CustomResources.md b/docs/CustomResources.md index d43b9332a..3506e9741 100644 --- a/docs/CustomResources.md +++ b/docs/CustomResources.md @@ -244,7 +244,8 @@ metadata: name: example spec: replicas: 3 - clusterMasterRef: example-cm + clusterMasterRef: + name: example-cm ``` Note: `clusterMasterRef` is required field in case of IndexerCluster resource since it will be used to connect the IndexerCluster to ClusterMaster resource. From e937db79ef7d5277472cfc5bc8c53acb7b48452a Mon Sep 17 00:00:00 2001 From: Param Dhanoya Date: Tue, 1 Dec 2020 09:18:31 -0800 Subject: [PATCH 03/43] Updated create eks cluster to use existing VPC --- test/deploy-eks-cluster.sh | 2 +- test/env.sh | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/test/deploy-eks-cluster.sh b/test/deploy-eks-cluster.sh index c379a5653..6d8a3abb3 100755 --- a/test/deploy-eks-cluster.sh +++ b/test/deploy-eks-cluster.sh @@ -20,7 +20,7 @@ function createCluster() { found=$(eksctl get cluster --name "${CLUSTER_NAME}") if [ -z "${found}" ]; then - eksctl create cluster --name=${CLUSTER_NAME} --nodes=${NUM_WORKERS} + eksctl create cluster --name=${CLUSTER_NAME} --nodes=${NUM_WORKERS} --vpc-public-subnets=${VPC_PUBLIC_SUBNET_STRING} --vpc-private-subnets=${VPC_PRIVATE_SUBNET_STRING} if [ $? -ne 0 ]; then echo "Unable to create cluster - ${CLUSTER_NAME}" return 1 diff --git a/test/env.sh b/test/env.sh index 12967ca8a..a9c092238 100644 --- a/test/env.sh +++ b/test/env.sh @@ -8,6 +8,8 @@ : "${NUM_NODES:=2}" : "${COMMIT_HASH:=}" : "${ECR_REGISTRY:=}" +: "${VPC_PUBLIC_SUBNET_STRING:=}" +: "${VPC_PRIVATE_SUBNET_STRING:=}" # Docker registry to use to push the test images to and pull from in the cluster if [ -z "${PRIVATE_REGISTRY}" ]; then From b5045ae684a9579b6f52c0bb94cfecc1c615a998 Mon Sep 17 00:00:00 2001 From: akondur Date: Wed, 9 Dec 2020 22:25:33 -0800 Subject: [PATCH 04/43] Docs fix for pass4Symmkey --- docs/Examples.md | 12 ++++++------ docs/PasswordManagement.md | 16 ++++++++-------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/docs/Examples.md b/docs/Examples.md index 290cf775f..4c865ea20 100644 --- a/docs/Examples.md +++ b/docs/Examples.md @@ -643,7 +643,7 @@ pass4SymmKey for authentication. There are two ways to configure `pass4Symmkey` with an External LM: #### Approach 1 -- Setup the desired plain-text [`pass4Symmkey`](PasswordManagement.md#pass4symmkey) in the global secret object(Note: The `pass4Symmkey` would be stored in a base64 encoded format). For details see [updating global kubernetes secret object](#updating-global-kubernetes-secret-object). +- Setup the desired plain-text [`pass4Symmkey`](PasswordManagement.md#pass4Symmkey) in the global secret object(Note: The `pass4Symmkey` would be stored in a base64 encoded format). For details see [updating global kubernetes secret object](#updating-global-kubernetes-secret-object). - Setup the same plain-text `pass4SymmKey` in the `[general]` section of your LM's `server.conf` file. #### Approach 2 @@ -662,7 +662,7 @@ There are two ways to configure `pass4Symmkey` with an External LM: ``` $SPLUNK_HOME/bin/splunk show-decrypted --value '$7$Sw0A+wvJdTztMcA2Ge7u435XmpTzPqyaq49kUZqn0yfAgwFpwrArM2JjWJ3mUyf/FyHAnCZkE/U=' ``` -- Setup the above decrypted plain-text [`pass4Symmkey`](PasswordManagement.md#pass4symmkey) in the global secret object(Note: The `pass4Symmkey` would be stored in a base64 encoded format). For details see [updating global kubernetes secret object](#updating-global-kubernetes-secret-object) +- Setup the above decrypted plain-text [`pass4Symmkey`](PasswordManagement.md#pass4Symmkey) in the global secret object(Note: The `pass4Symmkey` would be stored in a base64 encoded format). For details see [updating global kubernetes secret object](#updating-global-kubernetes-secret-object) ### Configuring license_master_url: @@ -712,7 +712,7 @@ operator & the external indexer cluster, and configure the `splunk.cluster_maste There are two ways to configure `IDXC pass4Symmkey` with an External Indexer Cluster: #### Approach 1 -- Setup the desired plain-text [`IDXC pass4Symmkey`](PasswordManagement.md#idxc-pass4symmkey) in the global secret object(Note: The `IDXC pass4Symmkey` would be stored in a base64 encoded format). For details see [updating global kubernetes secret object](#updating-global-kubernetes-secret-object). +- Setup the desired plain-text [`IDXC pass4Symmkey`](PasswordManagement.md#idxc-pass4Symmkey) in the global secret object(Note: The `IDXC pass4Symmkey` would be stored in a base64 encoded format). For details see [updating global kubernetes secret object](#updating-global-kubernetes-secret-object). - Setup the same plain-text `IDXC pass4SymmKey` in the `[clustering]` section of your cluster master's and indexers' `server.conf` file. #### Approach 2 @@ -731,7 +731,7 @@ There are two ways to configure `IDXC pass4Symmkey` with an External Indexer Clu ``` $SPLUNK_HOME/bin/splunk show-decrypted --value '$7$Sw0A+wvJdTztMcA2Ge7u435XmpTzPqyaq49kUZqn0yfAgwFpwrArM2JjWJ3mUyf/FyHAnCZkE/U=' ``` -- Setup the above decrypted plain-text [`IDXC pass4Symmkey`](PasswordManagement.md#idxc-pass4symmkey) in the global secret object(Note: The `IDXC pass4Symmkey` would be stored in a base64 encoded format). For details see [updating global kubernetes secret object](#updating-global-kubernetes-secret-object) +- Setup the above decrypted plain-text [`IDXC pass4Symmkey`](PasswordManagement.md#idxc-pass4Symmkey) in the global secret object(Note: The `IDXC pass4Symmkey` would be stored in a base64 encoded format). For details see [updating global kubernetes secret object](#updating-global-kubernetes-secret-object) ### Configuring cluster_master_url: @@ -777,8 +777,8 @@ Use the kubectl command to create the global kubernetes secret object: 2. Gather the password values for the secret tokens you want to configure. To see all available secret tokens defined for the global kubernetes secret object, review [password management](PasswordManagement.md#splunk-secret-tokens-in-the-global-secret-object) 3. Create a kubernetes secret object referencing the namespace. Example: splunk-`-secret. -In the example below, we are creating the global kubernetes secret object, defining the default administrator and pass4symmkey tokens, and passing in the values. -`kubectl create secret generic splunk--secret --from-literal='password=' --from-literal='pass4symmkey='` +In the example below, we are creating the global kubernetes secret object, defining the default administrator and pass4Symmkey tokens, and passing in the values. +`kubectl create secret generic splunk--secret --from-literal='password=' --from-literal='pass4Symmkey='` ### Reading global kubernetes secret object diff --git a/docs/PasswordManagement.md b/docs/PasswordManagement.md index adde67004..7a64b638d 100644 --- a/docs/PasswordManagement.md +++ b/docs/PasswordManagement.md @@ -5,9 +5,9 @@ - [Splunk Secret Tokens in the global secret object](#splunk-secret-tokens-in-the-global-secret-object) - [HEC Token](#hec-token) - [Default administrator password](#default-administrator-password) - - [Pass4symmkey](#pass4symmkey) - - [IDXC Pass4symmkey](#idxc-pass4symmkey) - - [SHC Pass4symmkey](#shc-pass4symmkey) + - [pass4Symmkey](#pass4Symmkey) + - [IDXC pass4Symmkey](#idxc-pass4Symmkey) + - [SHC pass4Symmkey](#shc-pass4Symmkey) - [Information for Splunk Enterprise administrator](#information-for-splunk-enterprise-administrator) - [Secrets on Docker Splunk](#secrets-on-docker-splunk) @@ -42,15 +42,15 @@ The configurable Splunk Secret Tokens include: **Key name in global kubernetes secret object**: `password` **Description**: password refers to the default administrator password for Splunk. -#### Pass4symmkey -**Key name in global kubernetes secret object**: `pass4symmkey` -**Description**: pass4symmkey is an authentication token for inter-communication within Splunk Enterprise. +#### pass4Symmkey +**Key name in global kubernetes secret object**: `pass4Symmkey` +**Description**: pass4Symmkey is an authentication token for inter-communication within Splunk Enterprise. -#### IDXC Pass4symmkey +#### IDXC pass4Symmkey **Key name in global kubernetes secret object**: `idxc.secret` **Description**: idxc.secret is an authentication token for inter-communication specifically for indexer clustering in Splunk Enterprise. -#### SHC Pass4symmkey +#### SHC pass4Symmkey **Key name in global kubernetes secret object**: `shc.secret` **Description**: shc.secret is an authentication token for inter-communication specifically for search head clustering in Splunk Enterprise. From 699c6befe1ad35569bc34893949e8794d2154837 Mon Sep 17 00:00:00 2001 From: Gaurav Gupta Date: Fri, 11 Dec 2020 12:48:36 -0800 Subject: [PATCH 05/43] Initial commit --- pkg/controller/add_indexercluster.go | 2 +- pkg/splunk/enterprise/configuration.go | 25 ++++++++++++++++++++++++- pkg/splunk/enterprise/indexercluster.go | 6 ++++++ 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/pkg/controller/add_indexercluster.go b/pkg/controller/add_indexercluster.go index c5205eb8e..2b6a69ad3 100644 --- a/pkg/controller/add_indexercluster.go +++ b/pkg/controller/add_indexercluster.go @@ -50,7 +50,7 @@ func (ctrl IndexerClusterController) GetInstance() splcommon.MetaObject { // GetWatchTypes returns a list of types owned by the controller that it would like to receive watch events for func (ctrl IndexerClusterController) GetWatchTypes() []runtime.Object { - return []runtime.Object{&appsv1.StatefulSet{}, &corev1.Secret{}} + return []runtime.Object{&appsv1.StatefulSet{}, &corev1.Secret{}, &enterprisev1.ClusterMaster{}} } // Reconcile is used to perform an idempotent reconciliation of the custom resource managed by this controller diff --git a/pkg/splunk/enterprise/configuration.go b/pkg/splunk/enterprise/configuration.go index 48f2a4fce..78014124d 100644 --- a/pkg/splunk/enterprise/configuration.go +++ b/pkg/splunk/enterprise/configuration.go @@ -15,6 +15,7 @@ package enterprise import ( + "context" "fmt" appsv1 "k8s.io/api/apps/v1" @@ -459,6 +460,7 @@ func getSmartstoreConfigMap(client splcommon.ControllerClient, cr splcommon.Meta // updateSplunkPodTemplateWithConfig modifies the podTemplateSpec object based on configuration of the Splunk Enterprise resource. func updateSplunkPodTemplateWithConfig(client splcommon.ControllerClient, podTemplateSpec *corev1.PodTemplateSpec, cr splcommon.MetaObject, spec *enterprisev1.CommonSplunkSpec, instanceType InstanceType, extraEnv []corev1.EnvVar, secretToMount string) { + scopedLog := log.WithName("updateSplunkPodTemplateWithConfig").WithValues("name", cr.GetName(), "namespace", cr.GetNamespace()) // Add custom ports to splunk containers if spec.ServiceTemplate.Spec.Ports != nil { for idx := range podTemplateSpec.Spec.Containers { @@ -510,7 +512,6 @@ func updateSplunkPodTemplateWithConfig(client splcommon.ControllerClient, podTem }, }) - scopedLog := log.WithName("updateSplunkPodTemplateWithConfig").WithValues("name", cr.GetName(), "namespace", cr.GetNamespace()) namespacedName := types.NamespacedName{Namespace: cr.GetNamespace(), Name: configMapName} // We will update the annotation for resource version in the pod template spec @@ -639,7 +640,29 @@ func updateSplunkPodTemplateWithConfig(client splcommon.ControllerClient, podTem if spec.ClusterMasterRef.Namespace != "" { clusterMasterURL = splcommon.GetServiceFQDN(spec.ClusterMasterRef.Namespace, clusterMasterURL) } + //Check if CM is connected to a LicenseMaster + namespacedName := types.NamespacedName{ + Namespace: cr.GetNamespace(), + Name: spec.ClusterMasterRef.Name, + } + masterIdxCluster := &enterprisev1.ClusterMaster{} + err := client.Get(context.TODO(), namespacedName, masterIdxCluster) + if err != nil { + scopedLog.Error(err, "Unable to get ClusterMaster") + } + + if masterIdxCluster.Spec.LicenseMasterRef.Name != "" { + licenseMasterURL := GetSplunkServiceName(SplunkLicenseMaster, masterIdxCluster.Spec.LicenseMasterRef.Name, false) + if masterIdxCluster.Spec.LicenseMasterRef.Namespace != "" { + licenseMasterURL = splcommon.GetServiceFQDN(masterIdxCluster.Spec.LicenseMasterRef.Namespace, licenseMasterURL) + } + env = append(env, corev1.EnvVar{ + Name: "SPLUNK_LICENSE_MASTER_URL", + Value: licenseMasterURL, + }) + } } + if clusterMasterURL != "" { extraEnv = append(extraEnv, corev1.EnvVar{ Name: "SPLUNK_CLUSTER_MASTER_URL", diff --git a/pkg/splunk/enterprise/indexercluster.go b/pkg/splunk/enterprise/indexercluster.go index 30c130763..7854252ea 100644 --- a/pkg/splunk/enterprise/indexercluster.go +++ b/pkg/splunk/enterprise/indexercluster.go @@ -154,6 +154,12 @@ func ApplyIndexerCluster(client splcommon.ControllerClient, cr *enterprisev1.Ind cr.Status.IdxcPasswordChangedSecrets = make(map[string]bool) result.Requeue = false + // set indexer cluster CR as owner reference for clustermaster + err = splctrl.SetStatefulSetOwnerRef(client, cr, namespacedName) + if err != nil { + result.Requeue = true + return result, err + } } return result, nil } From 585f729017b2b65333771484c361c5d1419ec0d1 Mon Sep 17 00:00:00 2001 From: Gaurav Gupta Date: Thu, 17 Dec 2020 12:47:40 -0800 Subject: [PATCH 06/43] Fixed UT failures --- pkg/splunk/enterprise/indexercluster.go | 3 ++- pkg/splunk/enterprise/indexercluster_test.go | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/pkg/splunk/enterprise/indexercluster.go b/pkg/splunk/enterprise/indexercluster.go index f7e294520..3fcb401a9 100644 --- a/pkg/splunk/enterprise/indexercluster.go +++ b/pkg/splunk/enterprise/indexercluster.go @@ -154,7 +154,8 @@ func ApplyIndexerCluster(client splcommon.ControllerClient, cr *enterprisev1.Ind cr.Status.IdxcPasswordChangedSecrets = make(map[string]bool) result.Requeue = false - // set indexer cluster CR as owner reference for clustermaster + // Set indexer cluster CR as owner reference for clustermaster + namespacedName = types.NamespacedName{Namespace: cr.GetNamespace(), Name: GetSplunkStatefulsetName(SplunkClusterMaster, cr.Spec.ClusterMasterRef.Name)} err = splctrl.SetStatefulSetOwnerRef(client, cr, namespacedName) if err != nil { result.Requeue = true diff --git a/pkg/splunk/enterprise/indexercluster_test.go b/pkg/splunk/enterprise/indexercluster_test.go index 770036790..0fef88913 100644 --- a/pkg/splunk/enterprise/indexercluster_test.go +++ b/pkg/splunk/enterprise/indexercluster_test.go @@ -42,6 +42,7 @@ func TestApplyIndexerCluster(t *testing.T) { {MetaName: "*v1.Service-test-splunk-stack1-indexer-service"}, {MetaName: "*v1.Secret-test-splunk-test-secret"}, {MetaName: "*v1.Secret-test-splunk-stack1-indexer-secret-v1"}, + {MetaName: "*v1beta1.ClusterMaster-test-master1"}, {MetaName: "*v1.Secret-test-splunk-test-secret"}, } labels := map[string]string{ @@ -55,7 +56,7 @@ func TestApplyIndexerCluster(t *testing.T) { listmockCall := []spltest.MockFuncCall{ {ListOpts: listOpts}} createCalls := map[string][]spltest.MockFuncCall{"Get": funcCalls, "Create": {funcCalls[0], funcCalls[3], funcCalls[4], funcCalls[6]}, "Update": {funcCalls[0]}, "List": {listmockCall[0]}} - updateCalls := map[string][]spltest.MockFuncCall{"Get": {funcCalls[0], funcCalls[1], funcCalls[2], funcCalls[3], funcCalls[4], funcCalls[5], funcCalls[6], funcCalls[7]}, "List": {listmockCall[0]}} + updateCalls := map[string][]spltest.MockFuncCall{"Get": funcCalls, "List": {listmockCall[0]}} current := enterprisev1.IndexerCluster{ TypeMeta: metav1.TypeMeta{ From 9bc3a76d5b975a782c760cbbb8cf0e4aecc5fc29 Mon Sep 17 00:00:00 2001 From: Gaurav Gupta Date: Tue, 22 Dec 2020 15:51:31 -0800 Subject: [PATCH 07/43] Addressed review comment --- pkg/controller/add_indexercluster.go | 2 +- pkg/splunk/enterprise/indexercluster.go | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/controller/add_indexercluster.go b/pkg/controller/add_indexercluster.go index 2b6a69ad3..c5205eb8e 100644 --- a/pkg/controller/add_indexercluster.go +++ b/pkg/controller/add_indexercluster.go @@ -50,7 +50,7 @@ func (ctrl IndexerClusterController) GetInstance() splcommon.MetaObject { // GetWatchTypes returns a list of types owned by the controller that it would like to receive watch events for func (ctrl IndexerClusterController) GetWatchTypes() []runtime.Object { - return []runtime.Object{&appsv1.StatefulSet{}, &corev1.Secret{}, &enterprisev1.ClusterMaster{}} + return []runtime.Object{&appsv1.StatefulSet{}, &corev1.Secret{}} } // Reconcile is used to perform an idempotent reconciliation of the custom resource managed by this controller diff --git a/pkg/splunk/enterprise/indexercluster.go b/pkg/splunk/enterprise/indexercluster.go index 3fcb401a9..b10f7c7c6 100644 --- a/pkg/splunk/enterprise/indexercluster.go +++ b/pkg/splunk/enterprise/indexercluster.go @@ -155,6 +155,7 @@ func ApplyIndexerCluster(client splcommon.ControllerClient, cr *enterprisev1.Ind result.Requeue = false // Set indexer cluster CR as owner reference for clustermaster + scopedLog.Info("Setting indexer cluster as owner for cluster master") namespacedName = types.NamespacedName{Namespace: cr.GetNamespace(), Name: GetSplunkStatefulsetName(SplunkClusterMaster, cr.Spec.ClusterMasterRef.Name)} err = splctrl.SetStatefulSetOwnerRef(client, cr, namespacedName) if err != nil { From ab35bb7ca6205f67444215bb814f6fde1b6266d7 Mon Sep 17 00:00:00 2001 From: Jeff Rybczynski Date: Wed, 16 Dec 2020 14:14:30 -0800 Subject: [PATCH 08/43] CSPL-663 Optimize deployment of Splunk apps on SHC Add a new parameter defaultsUrlApps to install apps listed here only on Standalone, SHC Deployer, and IDC CM. This parameter would be ignored on individual SHs and indexers in a clustered environment as the install of app there would be handled by bundle push from the deployer or CM. --- ...erprise.splunk.com_clustermasters_crd.yaml | 4 +++ ...erprise.splunk.com_licensemasters_crd.yaml | 4 +++ ...ise.splunk.com_searchheadclusters_crd.yaml | 4 +++ ...enterprise.splunk.com_standalones_crd.yaml | 4 +++ docs/Examples.md | 26 +++++++++++++++++++ pkg/apis/enterprise/v1beta1/common_types.go | 6 +++++ pkg/splunk/enterprise/clustermaster_test.go | 4 +++ pkg/splunk/enterprise/configuration.go | 8 ++++++ pkg/splunk/enterprise/indexercluster_test.go | 4 +++ pkg/splunk/enterprise/licensemaster_test.go | 4 +++ .../enterprise/searchheadcluster_test.go | 9 +++++++ pkg/splunk/enterprise/standalone_test.go | 3 +++ 12 files changed, 80 insertions(+) diff --git a/deploy/crds/enterprise.splunk.com_clustermasters_crd.yaml b/deploy/crds/enterprise.splunk.com_clustermasters_crd.yaml index 5c9b2adc7..d02058084 100644 --- a/deploy/crds/enterprise.splunk.com_clustermasters_crd.yaml +++ b/deploy/crds/enterprise.splunk.com_clustermasters_crd.yaml @@ -659,6 +659,10 @@ spec: description: Full path or URL for one or more default.yml files, separated by commas type: string + defaultsUrlApps: + description: Full path or URL for one or more default.yml files for installing apps, separated + by commas + type: string ephemeralStorage: description: If true, ephemeral (emptyDir) storage will be used for /opt/splunk/etc and /opt/splunk/var volumes diff --git a/deploy/crds/enterprise.splunk.com_licensemasters_crd.yaml b/deploy/crds/enterprise.splunk.com_licensemasters_crd.yaml index 5b2239343..89200ca61 100644 --- a/deploy/crds/enterprise.splunk.com_licensemasters_crd.yaml +++ b/deploy/crds/enterprise.splunk.com_licensemasters_crd.yaml @@ -664,6 +664,10 @@ spec: description: Full path or URL for one or more default.yml files, separated by commas type: string + defaultsUrlApps: + description: Full path or URL for one or more default.yml files for installing apps, separated + by commas + type: string ephemeralStorage: description: If true, ephemeral (emptyDir) storage will be used for /opt/splunk/etc and /opt/splunk/var volumes diff --git a/deploy/crds/enterprise.splunk.com_searchheadclusters_crd.yaml b/deploy/crds/enterprise.splunk.com_searchheadclusters_crd.yaml index 77d75f9aa..b189409b8 100644 --- a/deploy/crds/enterprise.splunk.com_searchheadclusters_crd.yaml +++ b/deploy/crds/enterprise.splunk.com_searchheadclusters_crd.yaml @@ -681,6 +681,10 @@ spec: description: Full path or URL for one or more default.yml files, separated by commas type: string + defaultsUrlApps: + description: Full path or URL for one or more default.yml files for installing apps, separated + by commas + type: string ephemeralStorage: description: If true, ephemeral (emptyDir) storage will be used for /opt/splunk/etc and /opt/splunk/var volumes diff --git a/deploy/crds/enterprise.splunk.com_standalones_crd.yaml b/deploy/crds/enterprise.splunk.com_standalones_crd.yaml index 044014cd5..293f7bdfe 100644 --- a/deploy/crds/enterprise.splunk.com_standalones_crd.yaml +++ b/deploy/crds/enterprise.splunk.com_standalones_crd.yaml @@ -674,6 +674,10 @@ spec: description: Full path or URL for one or more default.yml files, separated by commas type: string + defaultsUrlApps: + description: Full path or URL for one or more default.yml files for installing apps, separated + by commas + type: string ephemeralStorage: description: If true, ephemeral (emptyDir) storage will be used for /opt/splunk/etc and /opt/splunk/var volumes diff --git a/docs/Examples.md b/docs/Examples.md index 604ebfff8..7aed57522 100644 --- a/docs/Examples.md +++ b/docs/Examples.md @@ -504,6 +504,32 @@ You can also install apps hosted remotely using URLs: - "https://example.com/splunk-apps/app3.tgz" ``` +Also these application configuration parameters can be placed in a `defaults.yml` +file and use the `defaultsUrlApps` parameter. The `defaultsUrlApps` parameter +is specific for applicastion installation and will install the apps in the +correct instances as per the deployment. + +Unlike `defaultsUrl` which is applied at every instance created by the CR, the +`defaultsUrlApps` will be applied on instances that will **not** get the application +installed via a bundle push. Search head and indexer cluster members will not have +the `defaultsUrlApps` parameter applied. This means: + + - For Standalone & License Master, these applications will be installed as normal. + - For SearchHeadClusters, these applications will only be installed on the SHC Deployer +and pushed to the members via SH Bundle push. + - For IndexerClusters, these applications will only be installed on the ClusterMaster +and pushed to the indexers in the cluster via CM Bundle push. + +For application installation the preferred method will be through the `defaultsUrlApps` +while other ansible defaults can be still be installed via `defaultsUrl`. For backwards +compatibility applications could be installed via `defaultsUrl` though this is not +recommended. Both options can be used in conjunction: + +```yaml + defaultsUrl : "http://myco.com/splunk/generic.yml" + defaultsUrlApps: "/mnt/defaults/apps.yml" +``` + ## Using Apps for Splunk Configuration diff --git a/pkg/apis/enterprise/v1beta1/common_types.go b/pkg/apis/enterprise/v1beta1/common_types.go index 4728f358b..d3f161eff 100644 --- a/pkg/apis/enterprise/v1beta1/common_types.go +++ b/pkg/apis/enterprise/v1beta1/common_types.go @@ -58,6 +58,12 @@ type CommonSplunkSpec struct { // Full path or URL for one or more default.yml files, separated by commas DefaultsURL string `json:"defaultsUrl"` + // Full path or URL for one or more default.yml files specific to App install, separated by commas + // This is meant as a temporary fix until the SHC Deployer has its own CRD. The defaults listed here will + // only be installed on a standalone, or a SHC Deployer or IDC CM to be pushed to SH/IDXs in a cluster. + // This parameter is ignored on individual SHs or IDXs within a cluster + DefaultsURLApps string `json:"defaultsUrlApps"` + // Full path or URL for a Splunk Enterprise license file LicenseURL string `json:"licenseUrl"` diff --git a/pkg/splunk/enterprise/clustermaster_test.go b/pkg/splunk/enterprise/clustermaster_test.go index 2a8d377e7..ca5bc7830 100644 --- a/pkg/splunk/enterprise/clustermaster_test.go +++ b/pkg/splunk/enterprise/clustermaster_test.go @@ -124,6 +124,10 @@ func TestGetClusterMasterStatefulSet(t *testing.T) { cr.Spec.LicenseMasterRef.Name = "" cr.Spec.LicenseURL = "/mnt/splunk.lic" test(`{"kind":"StatefulSet","apiVersion":"apps/v1","metadata":{"name":"splunk-stack1-cluster-master","namespace":"test","creationTimestamp":null,"ownerReferences":[{"apiVersion":"","kind":"","name":"stack1","uid":"","controller":true}]},"spec":{"replicas":1,"selector":{"matchLabels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-cluster-master","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"cluster-master","app.kubernetes.io/part-of":"splunk-stack1-indexer"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-cluster-master","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"cluster-master","app.kubernetes.io/part-of":"splunk-stack1-indexer"},"annotations":{"traffic.sidecar.istio.io/excludeOutboundPorts":"8089,8191,9997,7777,9000,17000,17500,19000","traffic.sidecar.istio.io/includeInboundPorts":"8000"}},"spec":{"volumes":[{"name":"mnt-splunk-secrets","secret":{"secretName":"splunk-stack1-cluster-master-secret-v1","defaultMode":420}}],"containers":[{"name":"splunk","image":"splunk/splunk","ports":[{"name":"splunkweb","containerPort":8000,"protocol":"TCP"},{"name":"splunkd","containerPort":8089,"protocol":"TCP"}],"env":[{"name":"SPLUNK_HOME","value":"/opt/splunk"},{"name":"SPLUNK_START_ARGS","value":"--accept-license"},{"name":"SPLUNK_DEFAULTS_URL","value":"/mnt/splunk-secrets/default.yml"},{"name":"SPLUNK_HOME_OWNERSHIP_ENFORCEMENT","value":"false"},{"name":"SPLUNK_ROLE","value":"splunk_cluster_master"},{"name":"SPLUNK_DECLARATIVE_ADMIN_PASSWORD","value":"true"},{"name":"SPLUNK_LICENSE_URI","value":"/mnt/splunk.lic"},{"name":"SPLUNK_CLUSTER_MASTER_URL","value":"localhost"}],"resources":{"limits":{"cpu":"4","memory":"8Gi"},"requests":{"cpu":"100m","memory":"512Mi"}},"volumeMounts":[{"name":"pvc-etc","mountPath":"/opt/splunk/etc"},{"name":"pvc-var","mountPath":"/opt/splunk/var"},{"name":"mnt-splunk-secrets","mountPath":"/mnt/splunk-secrets"}],"livenessProbe":{"exec":{"command":["/sbin/checkstate.sh"]},"initialDelaySeconds":300,"timeoutSeconds":30,"periodSeconds":30},"readinessProbe":{"exec":{"command":["/bin/grep","started","/opt/container_artifact/splunk-container.state"]},"initialDelaySeconds":10,"timeoutSeconds":5,"periodSeconds":5},"imagePullPolicy":"IfNotPresent"}],"securityContext":{"runAsUser":41812,"fsGroup":41812},"affinity":{"podAntiAffinity":{"preferredDuringSchedulingIgnoredDuringExecution":[{"weight":100,"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app.kubernetes.io/instance","operator":"In","values":["splunk-stack1-cluster-master"]}]},"topologyKey":"kubernetes.io/hostname"}}]}},"schedulerName":"default-scheduler"}},"volumeClaimTemplates":[{"metadata":{"name":"pvc-etc","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-cluster-master","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"cluster-master","app.kubernetes.io/part-of":"splunk-stack1-indexer"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"10Gi"}}},"status":{}},{"metadata":{"name":"pvc-var","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-cluster-master","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"cluster-master","app.kubernetes.io/part-of":"splunk-stack1-indexer"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"100Gi"}}},"status":{}}],"serviceName":"splunk-stack1-cluster-master-headless","podManagementPolicy":"Parallel","updateStrategy":{"type":"OnDelete"}},"status":{"replicas":0}}`) + + cr.Spec.DefaultsURLApps = "/mnt/apps/apps.yml" + test(`{"kind":"StatefulSet","apiVersion":"apps/v1","metadata":{"name":"splunk-stack1-cluster-master","namespace":"test","creationTimestamp":null,"ownerReferences":[{"apiVersion":"","kind":"","name":"stack1","uid":"","controller":true}]},"spec":{"replicas":1,"selector":{"matchLabels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-cluster-master","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"cluster-master","app.kubernetes.io/part-of":"splunk-stack1-indexer"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-cluster-master","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"cluster-master","app.kubernetes.io/part-of":"splunk-stack1-indexer"},"annotations":{"traffic.sidecar.istio.io/excludeOutboundPorts":"8089,8191,9997,7777,9000,17000,17500,19000","traffic.sidecar.istio.io/includeInboundPorts":"8000"}},"spec":{"volumes":[{"name":"mnt-splunk-secrets","secret":{"secretName":"splunk-stack1-cluster-master-secret-v1","defaultMode":420}}],"containers":[{"name":"splunk","image":"splunk/splunk","ports":[{"name":"splunkweb","containerPort":8000,"protocol":"TCP"},{"name":"splunkd","containerPort":8089,"protocol":"TCP"}],"env":[{"name":"SPLUNK_HOME","value":"/opt/splunk"},{"name":"SPLUNK_START_ARGS","value":"--accept-license"},{"name":"SPLUNK_DEFAULTS_URL","value":"/mnt/apps/apps.yml,/mnt/splunk-secrets/default.yml"},{"name":"SPLUNK_HOME_OWNERSHIP_ENFORCEMENT","value":"false"},{"name":"SPLUNK_ROLE","value":"splunk_cluster_master"},{"name":"SPLUNK_DECLARATIVE_ADMIN_PASSWORD","value":"true"},{"name":"SPLUNK_LICENSE_URI","value":"/mnt/splunk.lic"},{"name":"SPLUNK_CLUSTER_MASTER_URL","value":"localhost"}],"resources":{"limits":{"cpu":"4","memory":"8Gi"},"requests":{"cpu":"100m","memory":"512Mi"}},"volumeMounts":[{"name":"pvc-etc","mountPath":"/opt/splunk/etc"},{"name":"pvc-var","mountPath":"/opt/splunk/var"},{"name":"mnt-splunk-secrets","mountPath":"/mnt/splunk-secrets"}],"livenessProbe":{"exec":{"command":["/sbin/checkstate.sh"]},"initialDelaySeconds":300,"timeoutSeconds":30,"periodSeconds":30},"readinessProbe":{"exec":{"command":["/bin/grep","started","/opt/container_artifact/splunk-container.state"]},"initialDelaySeconds":10,"timeoutSeconds":5,"periodSeconds":5},"imagePullPolicy":"IfNotPresent"}],"securityContext":{"runAsUser":41812,"fsGroup":41812},"affinity":{"podAntiAffinity":{"preferredDuringSchedulingIgnoredDuringExecution":[{"weight":100,"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app.kubernetes.io/instance","operator":"In","values":["splunk-stack1-cluster-master"]}]},"topologyKey":"kubernetes.io/hostname"}}]}},"schedulerName":"default-scheduler"}},"volumeClaimTemplates":[{"metadata":{"name":"pvc-etc","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-cluster-master","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"cluster-master","app.kubernetes.io/part-of":"splunk-stack1-indexer"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"10Gi"}}},"status":{}},{"metadata":{"name":"pvc-var","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-cluster-master","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"cluster-master","app.kubernetes.io/part-of":"splunk-stack1-indexer"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"100Gi"}}},"status":{}}],"serviceName":"splunk-stack1-cluster-master-headless","podManagementPolicy":"Parallel","updateStrategy":{"type":"OnDelete"}},"status":{"replicas":0}}`) + } func TestApplyClusterMasterWithSmartstore(t *testing.T) { diff --git a/pkg/splunk/enterprise/configuration.go b/pkg/splunk/enterprise/configuration.go index 48f2a4fce..f660c67e9 100644 --- a/pkg/splunk/enterprise/configuration.go +++ b/pkg/splunk/enterprise/configuration.go @@ -590,6 +590,14 @@ func updateSplunkPodTemplateWithConfig(client splcommon.ControllerClient, podTem // prepare defaults variable splunkDefaults := "/mnt/splunk-secrets/default.yml" + // Check for apps defaults and add it to only the standalone or deployer/cm instances + if spec.DefaultsURLApps != "" && + (instanceType == SplunkDeployer || + instanceType == SplunkStandalone || + instanceType == SplunkClusterMaster || + instanceType == SplunkLicenseMaster) { + splunkDefaults = fmt.Sprintf("%s,%s", spec.DefaultsURLApps, splunkDefaults) + } if spec.DefaultsURL != "" { splunkDefaults = fmt.Sprintf("%s,%s", spec.DefaultsURL, splunkDefaults) } diff --git a/pkg/splunk/enterprise/indexercluster_test.go b/pkg/splunk/enterprise/indexercluster_test.go index 770036790..bfd9e681c 100644 --- a/pkg/splunk/enterprise/indexercluster_test.go +++ b/pkg/splunk/enterprise/indexercluster_test.go @@ -1031,6 +1031,10 @@ func TestGetIndexerStatefulSet(t *testing.T) { cr.Spec.ServiceTemplate.Spec.Ports = []corev1.ServicePort{{Name: "user-defined", Port: 32000, Protocol: "UDP"}} test(`{"kind":"StatefulSet","apiVersion":"apps/v1","metadata":{"name":"splunk-stack1-indexer","namespace":"test","creationTimestamp":null,"ownerReferences":[{"apiVersion":"","kind":"","name":"stack1","uid":"","controller":true}]},"spec":{"replicas":1,"selector":{"matchLabels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-master1-indexer"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-master1-indexer"},"annotations":{"traffic.sidecar.istio.io/excludeOutboundPorts":"8089,8191,9997,7777,9000,17000,17500,19000","traffic.sidecar.istio.io/includeInboundPorts":"8000,8088"}},"spec":{"volumes":[{"name":"mnt-splunk-secrets","secret":{"secretName":"splunk-stack1-indexer-secret-v1","defaultMode":420}}],"containers":[{"name":"splunk","image":"splunk/splunk","ports":[{"name":"splunkweb","containerPort":8000,"protocol":"TCP"},{"name":"hec","containerPort":8088,"protocol":"TCP"},{"name":"splunkd","containerPort":8089,"protocol":"TCP"},{"name":"s2s","containerPort":9997,"protocol":"TCP"},{"name":"user-defined","containerPort":32000,"protocol":"UDP"}],"env":[{"name":"SPLUNK_HOME","value":"/opt/splunk"},{"name":"SPLUNK_START_ARGS","value":"--accept-license"},{"name":"SPLUNK_DEFAULTS_URL","value":"/mnt/splunk-secrets/default.yml"},{"name":"SPLUNK_HOME_OWNERSHIP_ENFORCEMENT","value":"false"},{"name":"SPLUNK_ROLE","value":"splunk_indexer"},{"name":"SPLUNK_DECLARATIVE_ADMIN_PASSWORD","value":"true"},{"name":"SPLUNK_INDEXER_URL","value":"splunk-stack1-indexer-0.splunk-stack1-indexer-headless.test.svc.cluster.local"},{"name":"SPLUNK_CLUSTER_MASTER_URL","value":"splunk-master1-cluster-master-service"}],"resources":{"limits":{"cpu":"4","memory":"8Gi"},"requests":{"cpu":"100m","memory":"512Mi"}},"volumeMounts":[{"name":"pvc-etc","mountPath":"/opt/splunk/etc"},{"name":"pvc-var","mountPath":"/opt/splunk/var"},{"name":"mnt-splunk-secrets","mountPath":"/mnt/splunk-secrets"}],"livenessProbe":{"exec":{"command":["/sbin/checkstate.sh"]},"initialDelaySeconds":300,"timeoutSeconds":30,"periodSeconds":30},"readinessProbe":{"exec":{"command":["/bin/grep","started","/opt/container_artifact/splunk-container.state"]},"initialDelaySeconds":10,"timeoutSeconds":5,"periodSeconds":5},"imagePullPolicy":"IfNotPresent"}],"securityContext":{"runAsUser":41812,"fsGroup":41812},"affinity":{"podAntiAffinity":{"preferredDuringSchedulingIgnoredDuringExecution":[{"weight":100,"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app.kubernetes.io/instance","operator":"In","values":["splunk-stack1-indexer"]}]},"topologyKey":"kubernetes.io/hostname"}}]}},"schedulerName":"default-scheduler"}},"volumeClaimTemplates":[{"metadata":{"name":"pvc-etc","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-master1-indexer"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"10Gi"}}},"status":{}},{"metadata":{"name":"pvc-var","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-master1-indexer"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"100Gi"}}},"status":{}}],"serviceName":"splunk-stack1-indexer-headless","podManagementPolicy":"Parallel","updateStrategy":{"type":"OnDelete"}},"status":{"replicas":0}}`) + // Block moving DefaultsURLApps to SPLUNK_DEFAULTS_URL for indexer cluster member + cr.Spec.DefaultsURLApps = "/mnt/apps/apps.yml" + test(`{"kind":"StatefulSet","apiVersion":"apps/v1","metadata":{"name":"splunk-stack1-indexer","namespace":"test","creationTimestamp":null,"ownerReferences":[{"apiVersion":"","kind":"","name":"stack1","uid":"","controller":true}]},"spec":{"replicas":1,"selector":{"matchLabels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-master1-indexer"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-master1-indexer"},"annotations":{"traffic.sidecar.istio.io/excludeOutboundPorts":"8089,8191,9997,7777,9000,17000,17500,19000","traffic.sidecar.istio.io/includeInboundPorts":"8000,8088"}},"spec":{"volumes":[{"name":"mnt-splunk-secrets","secret":{"secretName":"splunk-stack1-indexer-secret-v1","defaultMode":420}}],"containers":[{"name":"splunk","image":"splunk/splunk","ports":[{"name":"splunkweb","containerPort":8000,"protocol":"TCP"},{"name":"hec","containerPort":8088,"protocol":"TCP"},{"name":"splunkd","containerPort":8089,"protocol":"TCP"},{"name":"s2s","containerPort":9997,"protocol":"TCP"},{"name":"user-defined","containerPort":32000,"protocol":"UDP"}],"env":[{"name":"SPLUNK_HOME","value":"/opt/splunk"},{"name":"SPLUNK_START_ARGS","value":"--accept-license"},{"name":"SPLUNK_DEFAULTS_URL","value":"/mnt/splunk-secrets/default.yml"},{"name":"SPLUNK_HOME_OWNERSHIP_ENFORCEMENT","value":"false"},{"name":"SPLUNK_ROLE","value":"splunk_indexer"},{"name":"SPLUNK_DECLARATIVE_ADMIN_PASSWORD","value":"true"},{"name":"SPLUNK_INDEXER_URL","value":"splunk-stack1-indexer-0.splunk-stack1-indexer-headless.test.svc.cluster.local"},{"name":"SPLUNK_CLUSTER_MASTER_URL","value":"splunk-master1-cluster-master-service"}],"resources":{"limits":{"cpu":"4","memory":"8Gi"},"requests":{"cpu":"100m","memory":"512Mi"}},"volumeMounts":[{"name":"pvc-etc","mountPath":"/opt/splunk/etc"},{"name":"pvc-var","mountPath":"/opt/splunk/var"},{"name":"mnt-splunk-secrets","mountPath":"/mnt/splunk-secrets"}],"livenessProbe":{"exec":{"command":["/sbin/checkstate.sh"]},"initialDelaySeconds":300,"timeoutSeconds":30,"periodSeconds":30},"readinessProbe":{"exec":{"command":["/bin/grep","started","/opt/container_artifact/splunk-container.state"]},"initialDelaySeconds":10,"timeoutSeconds":5,"periodSeconds":5},"imagePullPolicy":"IfNotPresent"}],"securityContext":{"runAsUser":41812,"fsGroup":41812},"affinity":{"podAntiAffinity":{"preferredDuringSchedulingIgnoredDuringExecution":[{"weight":100,"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app.kubernetes.io/instance","operator":"In","values":["splunk-stack1-indexer"]}]},"topologyKey":"kubernetes.io/hostname"}}]}},"schedulerName":"default-scheduler"}},"volumeClaimTemplates":[{"metadata":{"name":"pvc-etc","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-master1-indexer"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"10Gi"}}},"status":{}},{"metadata":{"name":"pvc-var","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-master1-indexer"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"100Gi"}}},"status":{}}],"serviceName":"splunk-stack1-indexer-headless","podManagementPolicy":"Parallel","updateStrategy":{"type":"OnDelete"}},"status":{"replicas":0}}`) + cr.Spec.ClusterMasterRef.Namespace = "other" if err := validateIndexerClusterSpec(&cr); err == nil { t.Errorf("validateIndexerClusterSpec() error expected on multisite IndexerCluster referencing a cluster master located in a different namespace") diff --git a/pkg/splunk/enterprise/licensemaster_test.go b/pkg/splunk/enterprise/licensemaster_test.go index 772f154b3..2302a946c 100644 --- a/pkg/splunk/enterprise/licensemaster_test.go +++ b/pkg/splunk/enterprise/licensemaster_test.go @@ -104,4 +104,8 @@ func TestGetLicenseMasterStatefulSet(t *testing.T) { cr.Spec.LicenseURL = "/mnt/splunk.lic" test(`{"kind":"StatefulSet","apiVersion":"apps/v1","metadata":{"name":"splunk-stack1-license-master","namespace":"test","creationTimestamp":null,"ownerReferences":[{"apiVersion":"","kind":"","name":"stack1","uid":"","controller":true}]},"spec":{"replicas":1,"selector":{"matchLabels":{"app.kubernetes.io/component":"license-master","app.kubernetes.io/instance":"splunk-stack1-license-master","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"license-master","app.kubernetes.io/part-of":"splunk-stack1-license-master"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app.kubernetes.io/component":"license-master","app.kubernetes.io/instance":"splunk-stack1-license-master","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"license-master","app.kubernetes.io/part-of":"splunk-stack1-license-master"},"annotations":{"traffic.sidecar.istio.io/excludeOutboundPorts":"8089,8191,9997,7777,9000,17000,17500,19000","traffic.sidecar.istio.io/includeInboundPorts":"8000"}},"spec":{"volumes":[{"name":"mnt-splunk-secrets","secret":{"secretName":"splunk-stack1-license-master-secret-v1","defaultMode":420}}],"containers":[{"name":"splunk","image":"splunk/splunk","ports":[{"name":"splunkweb","containerPort":8000,"protocol":"TCP"},{"name":"splunkd","containerPort":8089,"protocol":"TCP"}],"env":[{"name":"SPLUNK_HOME","value":"/opt/splunk"},{"name":"SPLUNK_START_ARGS","value":"--accept-license"},{"name":"SPLUNK_DEFAULTS_URL","value":"/mnt/splunk-secrets/default.yml"},{"name":"SPLUNK_HOME_OWNERSHIP_ENFORCEMENT","value":"false"},{"name":"SPLUNK_ROLE","value":"splunk_license_master"},{"name":"SPLUNK_DECLARATIVE_ADMIN_PASSWORD","value":"true"},{"name":"SPLUNK_LICENSE_URI","value":"/mnt/splunk.lic"}],"resources":{"limits":{"cpu":"4","memory":"8Gi"},"requests":{"cpu":"100m","memory":"512Mi"}},"volumeMounts":[{"name":"pvc-etc","mountPath":"/opt/splunk/etc"},{"name":"pvc-var","mountPath":"/opt/splunk/var"},{"name":"mnt-splunk-secrets","mountPath":"/mnt/splunk-secrets"}],"livenessProbe":{"exec":{"command":["/sbin/checkstate.sh"]},"initialDelaySeconds":300,"timeoutSeconds":30,"periodSeconds":30},"readinessProbe":{"exec":{"command":["/bin/grep","started","/opt/container_artifact/splunk-container.state"]},"initialDelaySeconds":10,"timeoutSeconds":5,"periodSeconds":5},"imagePullPolicy":"IfNotPresent"}],"securityContext":{"runAsUser":41812,"fsGroup":41812},"affinity":{"podAntiAffinity":{"preferredDuringSchedulingIgnoredDuringExecution":[{"weight":100,"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app.kubernetes.io/instance","operator":"In","values":["splunk-stack1-license-master"]}]},"topologyKey":"kubernetes.io/hostname"}}]}},"schedulerName":"default-scheduler"}},"volumeClaimTemplates":[{"metadata":{"name":"pvc-etc","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"license-master","app.kubernetes.io/instance":"splunk-stack1-license-master","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"license-master","app.kubernetes.io/part-of":"splunk-stack1-license-master"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"10Gi"}}},"status":{}},{"metadata":{"name":"pvc-var","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"license-master","app.kubernetes.io/instance":"splunk-stack1-license-master","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"license-master","app.kubernetes.io/part-of":"splunk-stack1-license-master"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"100Gi"}}},"status":{}}],"serviceName":"splunk-stack1-license-master-headless","podManagementPolicy":"Parallel","updateStrategy":{"type":"OnDelete"}},"status":{"replicas":0}}`) + + // Allow installing apps via DefaultsURLApps for Licence Master + cr.Spec.DefaultsURLApps = "/mnt/apps/apps.yml" + test(`{"kind":"StatefulSet","apiVersion":"apps/v1","metadata":{"name":"splunk-stack1-license-master","namespace":"test","creationTimestamp":null,"ownerReferences":[{"apiVersion":"","kind":"","name":"stack1","uid":"","controller":true}]},"spec":{"replicas":1,"selector":{"matchLabels":{"app.kubernetes.io/component":"license-master","app.kubernetes.io/instance":"splunk-stack1-license-master","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"license-master","app.kubernetes.io/part-of":"splunk-stack1-license-master"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app.kubernetes.io/component":"license-master","app.kubernetes.io/instance":"splunk-stack1-license-master","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"license-master","app.kubernetes.io/part-of":"splunk-stack1-license-master"},"annotations":{"traffic.sidecar.istio.io/excludeOutboundPorts":"8089,8191,9997,7777,9000,17000,17500,19000","traffic.sidecar.istio.io/includeInboundPorts":"8000"}},"spec":{"volumes":[{"name":"mnt-splunk-secrets","secret":{"secretName":"splunk-stack1-license-master-secret-v1","defaultMode":420}}],"containers":[{"name":"splunk","image":"splunk/splunk","ports":[{"name":"splunkweb","containerPort":8000,"protocol":"TCP"},{"name":"splunkd","containerPort":8089,"protocol":"TCP"}],"env":[{"name":"SPLUNK_HOME","value":"/opt/splunk"},{"name":"SPLUNK_START_ARGS","value":"--accept-license"},{"name":"SPLUNK_DEFAULTS_URL","value":"/mnt/apps/apps.yml,/mnt/splunk-secrets/default.yml"},{"name":"SPLUNK_HOME_OWNERSHIP_ENFORCEMENT","value":"false"},{"name":"SPLUNK_ROLE","value":"splunk_license_master"},{"name":"SPLUNK_DECLARATIVE_ADMIN_PASSWORD","value":"true"},{"name":"SPLUNK_LICENSE_URI","value":"/mnt/splunk.lic"}],"resources":{"limits":{"cpu":"4","memory":"8Gi"},"requests":{"cpu":"100m","memory":"512Mi"}},"volumeMounts":[{"name":"pvc-etc","mountPath":"/opt/splunk/etc"},{"name":"pvc-var","mountPath":"/opt/splunk/var"},{"name":"mnt-splunk-secrets","mountPath":"/mnt/splunk-secrets"}],"livenessProbe":{"exec":{"command":["/sbin/checkstate.sh"]},"initialDelaySeconds":300,"timeoutSeconds":30,"periodSeconds":30},"readinessProbe":{"exec":{"command":["/bin/grep","started","/opt/container_artifact/splunk-container.state"]},"initialDelaySeconds":10,"timeoutSeconds":5,"periodSeconds":5},"imagePullPolicy":"IfNotPresent"}],"securityContext":{"runAsUser":41812,"fsGroup":41812},"affinity":{"podAntiAffinity":{"preferredDuringSchedulingIgnoredDuringExecution":[{"weight":100,"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app.kubernetes.io/instance","operator":"In","values":["splunk-stack1-license-master"]}]},"topologyKey":"kubernetes.io/hostname"}}]}},"schedulerName":"default-scheduler"}},"volumeClaimTemplates":[{"metadata":{"name":"pvc-etc","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"license-master","app.kubernetes.io/instance":"splunk-stack1-license-master","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"license-master","app.kubernetes.io/part-of":"splunk-stack1-license-master"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"10Gi"}}},"status":{}},{"metadata":{"name":"pvc-var","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"license-master","app.kubernetes.io/instance":"splunk-stack1-license-master","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"license-master","app.kubernetes.io/part-of":"splunk-stack1-license-master"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"100Gi"}}},"status":{}}],"serviceName":"splunk-stack1-license-master-headless","podManagementPolicy":"Parallel","updateStrategy":{"type":"OnDelete"}},"status":{"replicas":0}}`) } diff --git a/pkg/splunk/enterprise/searchheadcluster_test.go b/pkg/splunk/enterprise/searchheadcluster_test.go index cf48380ad..65ab8b0f8 100644 --- a/pkg/splunk/enterprise/searchheadcluster_test.go +++ b/pkg/splunk/enterprise/searchheadcluster_test.go @@ -558,6 +558,10 @@ func TestGetSearchHeadStatefulSet(t *testing.T) { cr.Spec.ClusterMasterRef.Namespace = "test2" test(`{"kind":"StatefulSet","apiVersion":"apps/v1","metadata":{"name":"splunk-stack1-search-head","namespace":"test","creationTimestamp":null,"ownerReferences":[{"apiVersion":"","kind":"","name":"stack1","uid":"","controller":true}]},"spec":{"replicas":6,"selector":{"matchLabels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-search-head","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"search-head","app.kubernetes.io/part-of":"splunk-stack1-search-head"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-search-head","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"search-head","app.kubernetes.io/part-of":"splunk-stack1-search-head"},"annotations":{"traffic.sidecar.istio.io/excludeOutboundPorts":"8089,8191,9997,7777,9000,17000,17500,19000","traffic.sidecar.istio.io/includeInboundPorts":"8000"}},"spec":{"volumes":[{"name":"mnt-splunk-secrets","secret":{"secretName":"splunk-stack1-search-head-secret-v1","defaultMode":420}},{"name":"mnt-splunk-jdk","emptyDir":{}},{"name":"mnt-splunk-spark","emptyDir":{}}],"initContainers":[{"name":"init","image":"splunk/spark","command":["bash","-c","cp -r /opt/jdk /mnt \u0026\u0026 cp -r /opt/spark /mnt"],"resources":{"limits":{"cpu":"1","memory":"512Mi"},"requests":{"cpu":"250m","memory":"128Mi"}},"volumeMounts":[{"name":"mnt-splunk-jdk","mountPath":"/mnt/jdk"},{"name":"mnt-splunk-spark","mountPath":"/mnt/spark"}],"imagePullPolicy":"IfNotPresent"}],"containers":[{"name":"splunk","image":"splunk/splunk","ports":[{"name":"splunkweb","containerPort":8000,"protocol":"TCP"},{"name":"splunkd","containerPort":8089,"protocol":"TCP"},{"name":"dfsmaster","containerPort":9000,"protocol":"TCP"},{"name":"dfccontrol","containerPort":17000,"protocol":"TCP"},{"name":"datareceive","containerPort":19000,"protocol":"TCP"}],"env":[{"name":"SPLUNK_HOME","value":"/opt/splunk"},{"name":"SPLUNK_START_ARGS","value":"--accept-license"},{"name":"SPLUNK_DEFAULTS_URL","value":"/mnt/splunk-secrets/default.yml"},{"name":"SPLUNK_HOME_OWNERSHIP_ENFORCEMENT","value":"false"},{"name":"SPLUNK_ROLE","value":"splunk_search_head"},{"name":"SPLUNK_DECLARATIVE_ADMIN_PASSWORD","value":"true"},{"name":"SPLUNK_SEARCH_HEAD_URL","value":"splunk-stack1-search-head-0.splunk-stack1-search-head-headless.test.svc.cluster.local,splunk-stack1-search-head-1.splunk-stack1-search-head-headless.test.svc.cluster.local,splunk-stack1-search-head-2.splunk-stack1-search-head-headless.test.svc.cluster.local,splunk-stack1-search-head-3.splunk-stack1-search-head-headless.test.svc.cluster.local,splunk-stack1-search-head-4.splunk-stack1-search-head-headless.test.svc.cluster.local,splunk-stack1-search-head-5.splunk-stack1-search-head-headless.test.svc.cluster.local"},{"name":"SPLUNK_SEARCH_HEAD_CAPTAIN_URL","value":"splunk-stack1-search-head-0.splunk-stack1-search-head-headless.test.svc.cluster.local"},{"name":"SPLUNK_DEPLOYER_URL","value":"splunk-stack1-deployer-service"},{"name":"SPLUNK_CLUSTER_MASTER_URL","value":"splunk-stack1-cluster-master-service.test2.svc.cluster.local"},{"name":"SPLUNK_ENABLE_DFS","value":"true"},{"name":"SPARK_MASTER_HOST","value":"splunk-stack1-spark-master-service"},{"name":"SPARK_MASTER_WEBUI_PORT","value":"8009"},{"name":"SPARK_HOME","value":"/mnt/splunk-spark"},{"name":"JAVA_HOME","value":"/mnt/splunk-jdk"},{"name":"SPLUNK_DFW_NUM_SLOTS_ENABLED","value":"true"}],"resources":{"limits":{"cpu":"4","memory":"8Gi"},"requests":{"cpu":"100m","memory":"512Mi"}},"volumeMounts":[{"name":"pvc-etc","mountPath":"/opt/splunk/etc"},{"name":"pvc-var","mountPath":"/opt/splunk/var"},{"name":"mnt-splunk-secrets","mountPath":"/mnt/splunk-secrets"},{"name":"mnt-splunk-jdk","mountPath":"/mnt/splunk-jdk"},{"name":"mnt-splunk-spark","mountPath":"/mnt/splunk-spark"}],"livenessProbe":{"exec":{"command":["/sbin/checkstate.sh"]},"initialDelaySeconds":300,"timeoutSeconds":30,"periodSeconds":30},"readinessProbe":{"exec":{"command":["/bin/grep","started","/opt/container_artifact/splunk-container.state"]},"initialDelaySeconds":10,"timeoutSeconds":5,"periodSeconds":5},"imagePullPolicy":"IfNotPresent"}],"securityContext":{"runAsUser":41812,"fsGroup":41812},"affinity":{"podAntiAffinity":{"preferredDuringSchedulingIgnoredDuringExecution":[{"weight":100,"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app.kubernetes.io/instance","operator":"In","values":["splunk-stack1-search-head"]}]},"topologyKey":"kubernetes.io/hostname"}}]}},"schedulerName":"default-scheduler"}},"volumeClaimTemplates":[{"metadata":{"name":"pvc-etc","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-search-head","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"search-head","app.kubernetes.io/part-of":"splunk-stack1-search-head"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"10Gi"}}},"status":{}},{"metadata":{"name":"pvc-var","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-search-head","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"search-head","app.kubernetes.io/part-of":"splunk-stack1-search-head"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"100Gi"}}},"status":{}}],"serviceName":"splunk-stack1-search-head-headless","podManagementPolicy":"Parallel","updateStrategy":{"type":"OnDelete"}},"status":{"replicas":0}}`) + // DefaultsURLApps should not be passed to SPLUNK_DEFAULTS_URL for SHCMember. These apps will be pushed via the SHCDeployer + cr.Spec.DefaultsURLApps = "/mnt/apps/apps.yml" + test(`{"kind":"StatefulSet","apiVersion":"apps/v1","metadata":{"name":"splunk-stack1-search-head","namespace":"test","creationTimestamp":null,"ownerReferences":[{"apiVersion":"","kind":"","name":"stack1","uid":"","controller":true}]},"spec":{"replicas":6,"selector":{"matchLabels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-search-head","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"search-head","app.kubernetes.io/part-of":"splunk-stack1-search-head"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-search-head","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"search-head","app.kubernetes.io/part-of":"splunk-stack1-search-head"},"annotations":{"traffic.sidecar.istio.io/excludeOutboundPorts":"8089,8191,9997,7777,9000,17000,17500,19000","traffic.sidecar.istio.io/includeInboundPorts":"8000"}},"spec":{"volumes":[{"name":"mnt-splunk-secrets","secret":{"secretName":"splunk-stack1-search-head-secret-v1","defaultMode":420}},{"name":"mnt-splunk-jdk","emptyDir":{}},{"name":"mnt-splunk-spark","emptyDir":{}}],"initContainers":[{"name":"init","image":"splunk/spark","command":["bash","-c","cp -r /opt/jdk /mnt \u0026\u0026 cp -r /opt/spark /mnt"],"resources":{"limits":{"cpu":"1","memory":"512Mi"},"requests":{"cpu":"250m","memory":"128Mi"}},"volumeMounts":[{"name":"mnt-splunk-jdk","mountPath":"/mnt/jdk"},{"name":"mnt-splunk-spark","mountPath":"/mnt/spark"}],"imagePullPolicy":"IfNotPresent"}],"containers":[{"name":"splunk","image":"splunk/splunk","ports":[{"name":"splunkweb","containerPort":8000,"protocol":"TCP"},{"name":"splunkd","containerPort":8089,"protocol":"TCP"},{"name":"dfsmaster","containerPort":9000,"protocol":"TCP"},{"name":"dfccontrol","containerPort":17000,"protocol":"TCP"},{"name":"datareceive","containerPort":19000,"protocol":"TCP"}],"env":[{"name":"SPLUNK_HOME","value":"/opt/splunk"},{"name":"SPLUNK_START_ARGS","value":"--accept-license"},{"name":"SPLUNK_DEFAULTS_URL","value":"/mnt/splunk-secrets/default.yml"},{"name":"SPLUNK_HOME_OWNERSHIP_ENFORCEMENT","value":"false"},{"name":"SPLUNK_ROLE","value":"splunk_search_head"},{"name":"SPLUNK_DECLARATIVE_ADMIN_PASSWORD","value":"true"},{"name":"SPLUNK_SEARCH_HEAD_URL","value":"splunk-stack1-search-head-0.splunk-stack1-search-head-headless.test.svc.cluster.local,splunk-stack1-search-head-1.splunk-stack1-search-head-headless.test.svc.cluster.local,splunk-stack1-search-head-2.splunk-stack1-search-head-headless.test.svc.cluster.local,splunk-stack1-search-head-3.splunk-stack1-search-head-headless.test.svc.cluster.local,splunk-stack1-search-head-4.splunk-stack1-search-head-headless.test.svc.cluster.local,splunk-stack1-search-head-5.splunk-stack1-search-head-headless.test.svc.cluster.local"},{"name":"SPLUNK_SEARCH_HEAD_CAPTAIN_URL","value":"splunk-stack1-search-head-0.splunk-stack1-search-head-headless.test.svc.cluster.local"},{"name":"SPLUNK_DEPLOYER_URL","value":"splunk-stack1-deployer-service"},{"name":"SPLUNK_CLUSTER_MASTER_URL","value":"splunk-stack1-cluster-master-service.test2.svc.cluster.local"},{"name":"SPLUNK_ENABLE_DFS","value":"true"},{"name":"SPARK_MASTER_HOST","value":"splunk-stack1-spark-master-service"},{"name":"SPARK_MASTER_WEBUI_PORT","value":"8009"},{"name":"SPARK_HOME","value":"/mnt/splunk-spark"},{"name":"JAVA_HOME","value":"/mnt/splunk-jdk"},{"name":"SPLUNK_DFW_NUM_SLOTS_ENABLED","value":"true"}],"resources":{"limits":{"cpu":"4","memory":"8Gi"},"requests":{"cpu":"100m","memory":"512Mi"}},"volumeMounts":[{"name":"pvc-etc","mountPath":"/opt/splunk/etc"},{"name":"pvc-var","mountPath":"/opt/splunk/var"},{"name":"mnt-splunk-secrets","mountPath":"/mnt/splunk-secrets"},{"name":"mnt-splunk-jdk","mountPath":"/mnt/splunk-jdk"},{"name":"mnt-splunk-spark","mountPath":"/mnt/splunk-spark"}],"livenessProbe":{"exec":{"command":["/sbin/checkstate.sh"]},"initialDelaySeconds":300,"timeoutSeconds":30,"periodSeconds":30},"readinessProbe":{"exec":{"command":["/bin/grep","started","/opt/container_artifact/splunk-container.state"]},"initialDelaySeconds":10,"timeoutSeconds":5,"periodSeconds":5},"imagePullPolicy":"IfNotPresent"}],"securityContext":{"runAsUser":41812,"fsGroup":41812},"affinity":{"podAntiAffinity":{"preferredDuringSchedulingIgnoredDuringExecution":[{"weight":100,"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app.kubernetes.io/instance","operator":"In","values":["splunk-stack1-search-head"]}]},"topologyKey":"kubernetes.io/hostname"}}]}},"schedulerName":"default-scheduler"}},"volumeClaimTemplates":[{"metadata":{"name":"pvc-etc","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-search-head","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"search-head","app.kubernetes.io/part-of":"splunk-stack1-search-head"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"10Gi"}}},"status":{}},{"metadata":{"name":"pvc-var","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-search-head","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"search-head","app.kubernetes.io/part-of":"splunk-stack1-search-head"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"100Gi"}}},"status":{}}],"serviceName":"splunk-stack1-search-head-headless","podManagementPolicy":"Parallel","updateStrategy":{"type":"OnDelete"}},"status":{"replicas":0}}`) + // Define additional service port in CR and verified the statefulset has the new port cr.Spec.ServiceTemplate.Spec.Ports = []corev1.ServicePort{{Name: "user-defined", Port: 32000, Protocol: "UDP"}} test(`{"kind":"StatefulSet","apiVersion":"apps/v1","metadata":{"name":"splunk-stack1-search-head","namespace":"test","creationTimestamp":null,"ownerReferences":[{"apiVersion":"","kind":"","name":"stack1","uid":"","controller":true}]},"spec":{"replicas":6,"selector":{"matchLabels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-search-head","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"search-head","app.kubernetes.io/part-of":"splunk-stack1-search-head"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-search-head","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"search-head","app.kubernetes.io/part-of":"splunk-stack1-search-head"},"annotations":{"traffic.sidecar.istio.io/excludeOutboundPorts":"8089,8191,9997,7777,9000,17000,17500,19000","traffic.sidecar.istio.io/includeInboundPorts":"8000"}},"spec":{"volumes":[{"name":"mnt-splunk-secrets","secret":{"secretName":"splunk-stack1-search-head-secret-v1","defaultMode":420}},{"name":"mnt-splunk-jdk","emptyDir":{}},{"name":"mnt-splunk-spark","emptyDir":{}}],"initContainers":[{"name":"init","image":"splunk/spark","command":["bash","-c","cp -r /opt/jdk /mnt \u0026\u0026 cp -r /opt/spark /mnt"],"resources":{"limits":{"cpu":"1","memory":"512Mi"},"requests":{"cpu":"250m","memory":"128Mi"}},"volumeMounts":[{"name":"mnt-splunk-jdk","mountPath":"/mnt/jdk"},{"name":"mnt-splunk-spark","mountPath":"/mnt/spark"}],"imagePullPolicy":"IfNotPresent"}],"containers":[{"name":"splunk","image":"splunk/splunk","ports":[{"name":"splunkweb","containerPort":8000,"protocol":"TCP"},{"name":"splunkd","containerPort":8089,"protocol":"TCP"},{"name":"dfsmaster","containerPort":9000,"protocol":"TCP"},{"name":"dfccontrol","containerPort":17000,"protocol":"TCP"},{"name":"datareceive","containerPort":19000,"protocol":"TCP"},{"name":"user-defined","containerPort":32000,"protocol":"UDP"}],"env":[{"name":"SPLUNK_HOME","value":"/opt/splunk"},{"name":"SPLUNK_START_ARGS","value":"--accept-license"},{"name":"SPLUNK_DEFAULTS_URL","value":"/mnt/splunk-secrets/default.yml"},{"name":"SPLUNK_HOME_OWNERSHIP_ENFORCEMENT","value":"false"},{"name":"SPLUNK_ROLE","value":"splunk_search_head"},{"name":"SPLUNK_DECLARATIVE_ADMIN_PASSWORD","value":"true"},{"name":"SPLUNK_SEARCH_HEAD_URL","value":"splunk-stack1-search-head-0.splunk-stack1-search-head-headless.test.svc.cluster.local,splunk-stack1-search-head-1.splunk-stack1-search-head-headless.test.svc.cluster.local,splunk-stack1-search-head-2.splunk-stack1-search-head-headless.test.svc.cluster.local,splunk-stack1-search-head-3.splunk-stack1-search-head-headless.test.svc.cluster.local,splunk-stack1-search-head-4.splunk-stack1-search-head-headless.test.svc.cluster.local,splunk-stack1-search-head-5.splunk-stack1-search-head-headless.test.svc.cluster.local"},{"name":"SPLUNK_SEARCH_HEAD_CAPTAIN_URL","value":"splunk-stack1-search-head-0.splunk-stack1-search-head-headless.test.svc.cluster.local"},{"name":"SPLUNK_DEPLOYER_URL","value":"splunk-stack1-deployer-service"},{"name":"SPLUNK_CLUSTER_MASTER_URL","value":"splunk-stack1-cluster-master-service.test2.svc.cluster.local"},{"name":"SPLUNK_ENABLE_DFS","value":"true"},{"name":"SPARK_MASTER_HOST","value":"splunk-stack1-spark-master-service"},{"name":"SPARK_MASTER_WEBUI_PORT","value":"8009"},{"name":"SPARK_HOME","value":"/mnt/splunk-spark"},{"name":"JAVA_HOME","value":"/mnt/splunk-jdk"},{"name":"SPLUNK_DFW_NUM_SLOTS_ENABLED","value":"true"}],"resources":{"limits":{"cpu":"4","memory":"8Gi"},"requests":{"cpu":"100m","memory":"512Mi"}},"volumeMounts":[{"name":"pvc-etc","mountPath":"/opt/splunk/etc"},{"name":"pvc-var","mountPath":"/opt/splunk/var"},{"name":"mnt-splunk-secrets","mountPath":"/mnt/splunk-secrets"},{"name":"mnt-splunk-jdk","mountPath":"/mnt/splunk-jdk"},{"name":"mnt-splunk-spark","mountPath":"/mnt/splunk-spark"}],"livenessProbe":{"exec":{"command":["/sbin/checkstate.sh"]},"initialDelaySeconds":300,"timeoutSeconds":30,"periodSeconds":30},"readinessProbe":{"exec":{"command":["/bin/grep","started","/opt/container_artifact/splunk-container.state"]},"initialDelaySeconds":10,"timeoutSeconds":5,"periodSeconds":5},"imagePullPolicy":"IfNotPresent"}],"securityContext":{"runAsUser":41812,"fsGroup":41812},"affinity":{"podAntiAffinity":{"preferredDuringSchedulingIgnoredDuringExecution":[{"weight":100,"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app.kubernetes.io/instance","operator":"In","values":["splunk-stack1-search-head"]}]},"topologyKey":"kubernetes.io/hostname"}}]}},"schedulerName":"default-scheduler"}},"volumeClaimTemplates":[{"metadata":{"name":"pvc-etc","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-search-head","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"search-head","app.kubernetes.io/part-of":"splunk-stack1-search-head"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"10Gi"}}},"status":{}},{"metadata":{"name":"pvc-var","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-search-head","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"search-head","app.kubernetes.io/part-of":"splunk-stack1-search-head"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"100Gi"}}},"status":{}}],"serviceName":"splunk-stack1-search-head-headless","podManagementPolicy":"Parallel","updateStrategy":{"type":"OnDelete"}},"status":{"replicas":0}}`) @@ -589,4 +593,9 @@ func TestGetDeployerStatefulSet(t *testing.T) { cr.Spec.Replicas = 3 test(`{"kind":"StatefulSet","apiVersion":"apps/v1","metadata":{"name":"splunk-stack1-deployer","namespace":"test","creationTimestamp":null,"ownerReferences":[{"apiVersion":"","kind":"","name":"stack1","uid":"","controller":true}]},"spec":{"replicas":1,"selector":{"matchLabels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-deployer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"deployer","app.kubernetes.io/part-of":"splunk-stack1-search-head"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-deployer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"deployer","app.kubernetes.io/part-of":"splunk-stack1-search-head"},"annotations":{"traffic.sidecar.istio.io/excludeOutboundPorts":"8089,8191,9997,7777,9000,17000,17500,19000","traffic.sidecar.istio.io/includeInboundPorts":"8000"}},"spec":{"volumes":[{"name":"mnt-splunk-secrets","secret":{"secretName":"splunk-stack1-deployer-secret-v1","defaultMode":420}}],"containers":[{"name":"splunk","image":"splunk/splunk","ports":[{"name":"splunkweb","containerPort":8000,"protocol":"TCP"},{"name":"splunkd","containerPort":8089,"protocol":"TCP"}],"env":[{"name":"SPLUNK_HOME","value":"/opt/splunk"},{"name":"SPLUNK_START_ARGS","value":"--accept-license"},{"name":"SPLUNK_DEFAULTS_URL","value":"/mnt/splunk-secrets/default.yml"},{"name":"SPLUNK_HOME_OWNERSHIP_ENFORCEMENT","value":"false"},{"name":"SPLUNK_ROLE","value":"splunk_deployer"},{"name":"SPLUNK_DECLARATIVE_ADMIN_PASSWORD","value":"true"},{"name":"SPLUNK_SEARCH_HEAD_URL","value":"splunk-stack1-search-head-0.splunk-stack1-search-head-headless.test.svc.cluster.local,splunk-stack1-search-head-1.splunk-stack1-search-head-headless.test.svc.cluster.local,splunk-stack1-search-head-2.splunk-stack1-search-head-headless.test.svc.cluster.local"},{"name":"SPLUNK_SEARCH_HEAD_CAPTAIN_URL","value":"splunk-stack1-search-head-0.splunk-stack1-search-head-headless.test.svc.cluster.local"}],"resources":{"limits":{"cpu":"4","memory":"8Gi"},"requests":{"cpu":"100m","memory":"512Mi"}},"volumeMounts":[{"name":"pvc-etc","mountPath":"/opt/splunk/etc"},{"name":"pvc-var","mountPath":"/opt/splunk/var"},{"name":"mnt-splunk-secrets","mountPath":"/mnt/splunk-secrets"}],"livenessProbe":{"exec":{"command":["/sbin/checkstate.sh"]},"initialDelaySeconds":300,"timeoutSeconds":30,"periodSeconds":30},"readinessProbe":{"exec":{"command":["/bin/grep","started","/opt/container_artifact/splunk-container.state"]},"initialDelaySeconds":10,"timeoutSeconds":5,"periodSeconds":5},"imagePullPolicy":"IfNotPresent"}],"securityContext":{"runAsUser":41812,"fsGroup":41812},"affinity":{"podAntiAffinity":{"preferredDuringSchedulingIgnoredDuringExecution":[{"weight":100,"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app.kubernetes.io/instance","operator":"In","values":["splunk-stack1-deployer"]}]},"topologyKey":"kubernetes.io/hostname"}}]}},"schedulerName":"default-scheduler"}},"volumeClaimTemplates":[{"metadata":{"name":"pvc-etc","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-deployer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"deployer","app.kubernetes.io/part-of":"splunk-stack1-search-head"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"10Gi"}}},"status":{}},{"metadata":{"name":"pvc-var","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-deployer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"deployer","app.kubernetes.io/part-of":"splunk-stack1-search-head"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"100Gi"}}},"status":{}}],"serviceName":"splunk-stack1-deployer-headless","podManagementPolicy":"Parallel","updateStrategy":{"type":"OnDelete"}},"status":{"replicas":0}}`) + + // Allow installation of apps via DefaultsURLApps on the SHCDeployer + cr.Spec.DefaultsURLApps = "/mnt/apps/apps.yml" + test(`{"kind":"StatefulSet","apiVersion":"apps/v1","metadata":{"name":"splunk-stack1-deployer","namespace":"test","creationTimestamp":null,"ownerReferences":[{"apiVersion":"","kind":"","name":"stack1","uid":"","controller":true}]},"spec":{"replicas":1,"selector":{"matchLabels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-deployer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"deployer","app.kubernetes.io/part-of":"splunk-stack1-search-head"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-deployer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"deployer","app.kubernetes.io/part-of":"splunk-stack1-search-head"},"annotations":{"traffic.sidecar.istio.io/excludeOutboundPorts":"8089,8191,9997,7777,9000,17000,17500,19000","traffic.sidecar.istio.io/includeInboundPorts":"8000"}},"spec":{"volumes":[{"name":"mnt-splunk-secrets","secret":{"secretName":"splunk-stack1-deployer-secret-v1","defaultMode":420}}],"containers":[{"name":"splunk","image":"splunk/splunk","ports":[{"name":"splunkweb","containerPort":8000,"protocol":"TCP"},{"name":"splunkd","containerPort":8089,"protocol":"TCP"}],"env":[{"name":"SPLUNK_HOME","value":"/opt/splunk"},{"name":"SPLUNK_START_ARGS","value":"--accept-license"},{"name":"SPLUNK_DEFAULTS_URL","value":"/mnt/apps/apps.yml,/mnt/splunk-secrets/default.yml"},{"name":"SPLUNK_HOME_OWNERSHIP_ENFORCEMENT","value":"false"},{"name":"SPLUNK_ROLE","value":"splunk_deployer"},{"name":"SPLUNK_DECLARATIVE_ADMIN_PASSWORD","value":"true"},{"name":"SPLUNK_SEARCH_HEAD_URL","value":"splunk-stack1-search-head-0.splunk-stack1-search-head-headless.test.svc.cluster.local,splunk-stack1-search-head-1.splunk-stack1-search-head-headless.test.svc.cluster.local,splunk-stack1-search-head-2.splunk-stack1-search-head-headless.test.svc.cluster.local"},{"name":"SPLUNK_SEARCH_HEAD_CAPTAIN_URL","value":"splunk-stack1-search-head-0.splunk-stack1-search-head-headless.test.svc.cluster.local"}],"resources":{"limits":{"cpu":"4","memory":"8Gi"},"requests":{"cpu":"100m","memory":"512Mi"}},"volumeMounts":[{"name":"pvc-etc","mountPath":"/opt/splunk/etc"},{"name":"pvc-var","mountPath":"/opt/splunk/var"},{"name":"mnt-splunk-secrets","mountPath":"/mnt/splunk-secrets"}],"livenessProbe":{"exec":{"command":["/sbin/checkstate.sh"]},"initialDelaySeconds":300,"timeoutSeconds":30,"periodSeconds":30},"readinessProbe":{"exec":{"command":["/bin/grep","started","/opt/container_artifact/splunk-container.state"]},"initialDelaySeconds":10,"timeoutSeconds":5,"periodSeconds":5},"imagePullPolicy":"IfNotPresent"}],"securityContext":{"runAsUser":41812,"fsGroup":41812},"affinity":{"podAntiAffinity":{"preferredDuringSchedulingIgnoredDuringExecution":[{"weight":100,"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app.kubernetes.io/instance","operator":"In","values":["splunk-stack1-deployer"]}]},"topologyKey":"kubernetes.io/hostname"}}]}},"schedulerName":"default-scheduler"}},"volumeClaimTemplates":[{"metadata":{"name":"pvc-etc","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-deployer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"deployer","app.kubernetes.io/part-of":"splunk-stack1-search-head"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"10Gi"}}},"status":{}},{"metadata":{"name":"pvc-var","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-deployer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"deployer","app.kubernetes.io/part-of":"splunk-stack1-search-head"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"100Gi"}}},"status":{}}],"serviceName":"splunk-stack1-deployer-headless","podManagementPolicy":"Parallel","updateStrategy":{"type":"OnDelete"}},"status":{"replicas":0}}`) + } diff --git a/pkg/splunk/enterprise/standalone_test.go b/pkg/splunk/enterprise/standalone_test.go index 717af19b0..ba2a4e57d 100644 --- a/pkg/splunk/enterprise/standalone_test.go +++ b/pkg/splunk/enterprise/standalone_test.go @@ -216,6 +216,9 @@ func TestGetStandaloneStatefulSet(t *testing.T) { {Name: "defaults"}, } test(`{"kind":"StatefulSet","apiVersion":"apps/v1","metadata":{"name":"splunk-stack1-standalone","namespace":"test","creationTimestamp":null,"ownerReferences":[{"apiVersion":"","kind":"","name":"stack1","uid":"","controller":true}]},"spec":{"replicas":1,"selector":{"matchLabels":{"app.kubernetes.io/component":"standalone","app.kubernetes.io/instance":"splunk-stack1-standalone","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"standalone","app.kubernetes.io/part-of":"splunk-stack1-standalone"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app.kubernetes.io/component":"standalone","app.kubernetes.io/instance":"splunk-stack1-standalone","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"standalone","app.kubernetes.io/part-of":"splunk-stack1-standalone"},"annotations":{"traffic.sidecar.istio.io/excludeOutboundPorts":"8089,8191,9997,7777,9000,17000,17500,19000","traffic.sidecar.istio.io/includeInboundPorts":"8000,8088"}},"spec":{"volumes":[{"name":"defaults"},{"name":"mnt-splunk-secrets","secret":{"secretName":"splunk-stack1-standalone-secret-v1","defaultMode":420}},{"name":"mnt-splunk-defaults","configMap":{"name":"splunk-stack1-standalone-defaults","defaultMode":420}},{"name":"mnt-splunk-jdk","emptyDir":{}},{"name":"mnt-splunk-spark","emptyDir":{}}],"initContainers":[{"name":"init","image":"splunk/spark","command":["bash","-c","cp -r /opt/jdk /mnt \u0026\u0026 cp -r /opt/spark /mnt"],"resources":{"limits":{"cpu":"1","memory":"512Mi"},"requests":{"cpu":"250m","memory":"128Mi"}},"volumeMounts":[{"name":"mnt-splunk-jdk","mountPath":"/mnt/jdk"},{"name":"mnt-splunk-spark","mountPath":"/mnt/spark"}],"imagePullPolicy":"IfNotPresent"}],"containers":[{"name":"splunk","image":"splunk/splunk","ports":[{"name":"splunkweb","containerPort":8000,"protocol":"TCP"},{"name":"hec","containerPort":8088,"protocol":"TCP"},{"name":"splunkd","containerPort":8089,"protocol":"TCP"},{"name":"dfsmaster","containerPort":9000,"protocol":"TCP"},{"name":"s2s","containerPort":9997,"protocol":"TCP"},{"name":"dfccontrol","containerPort":17000,"protocol":"TCP"},{"name":"datareceive","containerPort":19000,"protocol":"TCP"}],"env":[{"name":"SPLUNK_HOME","value":"/opt/splunk"},{"name":"SPLUNK_START_ARGS","value":"--accept-license"},{"name":"SPLUNK_DEFAULTS_URL","value":"/mnt/splunk-defaults/default.yml,/mnt/defaults/defaults.yml,/mnt/splunk-secrets/default.yml"},{"name":"SPLUNK_HOME_OWNERSHIP_ENFORCEMENT","value":"false"},{"name":"SPLUNK_ROLE","value":"splunk_search_head"},{"name":"SPLUNK_DECLARATIVE_ADMIN_PASSWORD","value":"true"},{"name":"SPLUNK_CLUSTER_MASTER_URL","value":"splunk-stack2-cluster-master-service"},{"name":"SPLUNK_ENABLE_DFS","value":"true"},{"name":"SPARK_MASTER_HOST","value":"splunk-stack1-spark-master-service"},{"name":"SPARK_MASTER_WEBUI_PORT","value":"8009"},{"name":"SPARK_HOME","value":"/mnt/splunk-spark"},{"name":"JAVA_HOME","value":"/mnt/splunk-jdk"},{"name":"SPLUNK_DFW_NUM_SLOTS_ENABLED","value":"false"}],"resources":{"limits":{"cpu":"4","memory":"8Gi"},"requests":{"cpu":"100m","memory":"512Mi"}},"volumeMounts":[{"name":"pvc-etc","mountPath":"/opt/splunk/etc"},{"name":"pvc-var","mountPath":"/opt/splunk/var"},{"name":"defaults","mountPath":"/mnt/defaults"},{"name":"mnt-splunk-secrets","mountPath":"/mnt/splunk-secrets"},{"name":"mnt-splunk-defaults","mountPath":"/mnt/splunk-defaults"},{"name":"mnt-splunk-jdk","mountPath":"/mnt/splunk-jdk"},{"name":"mnt-splunk-spark","mountPath":"/mnt/splunk-spark"}],"livenessProbe":{"exec":{"command":["/sbin/checkstate.sh"]},"initialDelaySeconds":300,"timeoutSeconds":30,"periodSeconds":30},"readinessProbe":{"exec":{"command":["/bin/grep","started","/opt/container_artifact/splunk-container.state"]},"initialDelaySeconds":10,"timeoutSeconds":5,"periodSeconds":5},"imagePullPolicy":"IfNotPresent"}],"securityContext":{"runAsUser":41812,"fsGroup":41812},"affinity":{"podAntiAffinity":{"preferredDuringSchedulingIgnoredDuringExecution":[{"weight":100,"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app.kubernetes.io/instance","operator":"In","values":["splunk-stack1-standalone"]}]},"topologyKey":"kubernetes.io/hostname"}}]}},"schedulerName":"custom-scheduler"}},"volumeClaimTemplates":[{"metadata":{"name":"pvc-etc","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"standalone","app.kubernetes.io/instance":"splunk-stack1-standalone","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"standalone","app.kubernetes.io/part-of":"splunk-stack1-standalone"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"10Gi"}},"storageClassName":"gp2"},"status":{}},{"metadata":{"name":"pvc-var","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"standalone","app.kubernetes.io/instance":"splunk-stack1-standalone","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"standalone","app.kubernetes.io/part-of":"splunk-stack1-standalone"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"100Gi"}},"storageClassName":"gp2"},"status":{}}],"serviceName":"splunk-stack1-standalone-headless","podManagementPolicy":"Parallel","updateStrategy":{"type":"OnDelete"}},"status":{"replicas":0}}`) + + cr.Spec.DefaultsURLApps = "/mnt/apps/apps.yml" + test(`{"kind":"StatefulSet","apiVersion":"apps/v1","metadata":{"name":"splunk-stack1-standalone","namespace":"test","creationTimestamp":null,"ownerReferences":[{"apiVersion":"","kind":"","name":"stack1","uid":"","controller":true}]},"spec":{"replicas":1,"selector":{"matchLabels":{"app.kubernetes.io/component":"standalone","app.kubernetes.io/instance":"splunk-stack1-standalone","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"standalone","app.kubernetes.io/part-of":"splunk-stack1-standalone"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app.kubernetes.io/component":"standalone","app.kubernetes.io/instance":"splunk-stack1-standalone","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"standalone","app.kubernetes.io/part-of":"splunk-stack1-standalone"},"annotations":{"traffic.sidecar.istio.io/excludeOutboundPorts":"8089,8191,9997,7777,9000,17000,17500,19000","traffic.sidecar.istio.io/includeInboundPorts":"8000,8088"}},"spec":{"volumes":[{"name":"defaults"},{"name":"mnt-splunk-secrets","secret":{"secretName":"splunk-stack1-standalone-secret-v1","defaultMode":420}},{"name":"mnt-splunk-defaults","configMap":{"name":"splunk-stack1-standalone-defaults","defaultMode":420}},{"name":"mnt-splunk-jdk","emptyDir":{}},{"name":"mnt-splunk-spark","emptyDir":{}}],"initContainers":[{"name":"init","image":"splunk/spark","command":["bash","-c","cp -r /opt/jdk /mnt \u0026\u0026 cp -r /opt/spark /mnt"],"resources":{"limits":{"cpu":"1","memory":"512Mi"},"requests":{"cpu":"250m","memory":"128Mi"}},"volumeMounts":[{"name":"mnt-splunk-jdk","mountPath":"/mnt/jdk"},{"name":"mnt-splunk-spark","mountPath":"/mnt/spark"}],"imagePullPolicy":"IfNotPresent"}],"containers":[{"name":"splunk","image":"splunk/splunk","ports":[{"name":"splunkweb","containerPort":8000,"protocol":"TCP"},{"name":"hec","containerPort":8088,"protocol":"TCP"},{"name":"splunkd","containerPort":8089,"protocol":"TCP"},{"name":"dfsmaster","containerPort":9000,"protocol":"TCP"},{"name":"s2s","containerPort":9997,"protocol":"TCP"},{"name":"dfccontrol","containerPort":17000,"protocol":"TCP"},{"name":"datareceive","containerPort":19000,"protocol":"TCP"}],"env":[{"name":"SPLUNK_HOME","value":"/opt/splunk"},{"name":"SPLUNK_START_ARGS","value":"--accept-license"},{"name":"SPLUNK_DEFAULTS_URL","value":"/mnt/splunk-defaults/default.yml,/mnt/defaults/defaults.yml,/mnt/apps/apps.yml,/mnt/splunk-secrets/default.yml"},{"name":"SPLUNK_HOME_OWNERSHIP_ENFORCEMENT","value":"false"},{"name":"SPLUNK_ROLE","value":"splunk_search_head"},{"name":"SPLUNK_DECLARATIVE_ADMIN_PASSWORD","value":"true"},{"name":"SPLUNK_CLUSTER_MASTER_URL","value":"splunk-stack2-cluster-master-service"},{"name":"SPLUNK_ENABLE_DFS","value":"true"},{"name":"SPARK_MASTER_HOST","value":"splunk-stack1-spark-master-service"},{"name":"SPARK_MASTER_WEBUI_PORT","value":"8009"},{"name":"SPARK_HOME","value":"/mnt/splunk-spark"},{"name":"JAVA_HOME","value":"/mnt/splunk-jdk"},{"name":"SPLUNK_DFW_NUM_SLOTS_ENABLED","value":"false"}],"resources":{"limits":{"cpu":"4","memory":"8Gi"},"requests":{"cpu":"100m","memory":"512Mi"}},"volumeMounts":[{"name":"pvc-etc","mountPath":"/opt/splunk/etc"},{"name":"pvc-var","mountPath":"/opt/splunk/var"},{"name":"defaults","mountPath":"/mnt/defaults"},{"name":"mnt-splunk-secrets","mountPath":"/mnt/splunk-secrets"},{"name":"mnt-splunk-defaults","mountPath":"/mnt/splunk-defaults"},{"name":"mnt-splunk-jdk","mountPath":"/mnt/splunk-jdk"},{"name":"mnt-splunk-spark","mountPath":"/mnt/splunk-spark"}],"livenessProbe":{"exec":{"command":["/sbin/checkstate.sh"]},"initialDelaySeconds":300,"timeoutSeconds":30,"periodSeconds":30},"readinessProbe":{"exec":{"command":["/bin/grep","started","/opt/container_artifact/splunk-container.state"]},"initialDelaySeconds":10,"timeoutSeconds":5,"periodSeconds":5},"imagePullPolicy":"IfNotPresent"}],"securityContext":{"runAsUser":41812,"fsGroup":41812},"affinity":{"podAntiAffinity":{"preferredDuringSchedulingIgnoredDuringExecution":[{"weight":100,"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app.kubernetes.io/instance","operator":"In","values":["splunk-stack1-standalone"]}]},"topologyKey":"kubernetes.io/hostname"}}]}},"schedulerName":"custom-scheduler"}},"volumeClaimTemplates":[{"metadata":{"name":"pvc-etc","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"standalone","app.kubernetes.io/instance":"splunk-stack1-standalone","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"standalone","app.kubernetes.io/part-of":"splunk-stack1-standalone"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"10Gi"}},"storageClassName":"gp2"},"status":{}},{"metadata":{"name":"pvc-var","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"standalone","app.kubernetes.io/instance":"splunk-stack1-standalone","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"standalone","app.kubernetes.io/part-of":"splunk-stack1-standalone"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"100Gi"}},"storageClassName":"gp2"},"status":{}}],"serviceName":"splunk-stack1-standalone-headless","podManagementPolicy":"Parallel","updateStrategy":{"type":"OnDelete"}},"status":{"replicas":0}}`) } func TestApplyStandaloneSmartstoreKeyChangeDetection(t *testing.T) { From 1bb09542a286e7eb5b3d37c71e7f5485e67bc465 Mon Sep 17 00:00:00 2001 From: Param Dhanoya Date: Wed, 9 Dec 2020 09:44:42 -0800 Subject: [PATCH 09/43] CSPL-620: add test to verify disabled SH dont exist on CM --- test/delete_cr/deletecr_suite_test.go | 47 ++++ test/delete_cr/deletecr_test.go | 63 +++++ .../ingest_search/ingest_search_suite_test.go | 2 +- .../monitoring_console_test.go | 9 +- test/run-tests.sh | 3 +- test/smoke/smoke_test.go | 223 +----------------- test/testenv/cmutil.go | 70 ++++++ test/testenv/search_head_cluster_utils.go | 36 +++ test/testenv/verificationutils.go | 124 +++++++++- 9 files changed, 359 insertions(+), 218 deletions(-) create mode 100644 test/delete_cr/deletecr_suite_test.go create mode 100644 test/delete_cr/deletecr_test.go create mode 100644 test/testenv/search_head_cluster_utils.go diff --git a/test/delete_cr/deletecr_suite_test.go b/test/delete_cr/deletecr_suite_test.go new file mode 100644 index 000000000..5db1d7596 --- /dev/null +++ b/test/delete_cr/deletecr_suite_test.go @@ -0,0 +1,47 @@ +package deletecr + +import ( + "testing" + "time" + + . "github.com/onsi/ginkgo" + "github.com/onsi/ginkgo/reporters" + . "github.com/onsi/gomega" + + "github.com/splunk/splunk-operator/test/testenv" +) + +const ( + // PollInterval specifies the polling interval + PollInterval = 5 * time.Second + + // ConsistentPollInterval is the interval to use to consistently check a state is stable + ConsistentPollInterval = 200 * time.Millisecond + ConsistentDuration = 2000 * time.Millisecond +) + +var ( + testenvInstance *testenv.TestEnv + testSuiteName = "deletecr-" + testenv.RandomDNSName(2) +) + +// TestBasic is the main entry point +func TestBasic(t *testing.T) { + + RegisterFailHandler(Fail) + + junitReporter := reporters.NewJUnitReporter(testSuiteName + "_junit.xml") + RunSpecsWithDefaultAndCustomReporters(t, "Running "+testSuiteName, []Reporter{junitReporter}) +} + +var _ = BeforeSuite(func() { + var err error + testenvInstance, err = testenv.NewDefaultTestEnv(testSuiteName) + Expect(err).ToNot(HaveOccurred()) +}) + +var _ = AfterSuite(func() { + if testenvInstance != nil { + Expect(testenvInstance.Teardown()).ToNot(HaveOccurred()) + } +}) diff --git a/test/delete_cr/deletecr_test.go b/test/delete_cr/deletecr_test.go new file mode 100644 index 000000000..7a72192b6 --- /dev/null +++ b/test/delete_cr/deletecr_test.go @@ -0,0 +1,63 @@ +package deletecr + +import ( + "os/exec" + "strings" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "github.com/splunk/splunk-operator/test/testenv" +) + +func dumpGetPods(ns string) { + output, _ := exec.Command("kubectl", "get", "pod", "-n", ns).Output() + for _, line := range strings.Split(string(output), "\n") { + testenvInstance.Log.Info(line) + } +} + +var _ = Describe("DeleteCR test", func() { + + var deployment *testenv.Deployment + + BeforeEach(func() { + var err error + deployment, err = testenvInstance.NewDeployment(testenv.RandomDNSName(3)) + Expect(err).To(Succeed(), "Unable to create deployment") + }) + + AfterEach(func() { + // When a test spec failed, skip the teardown so we can troubleshoot. + if CurrentGinkgoTestDescription().Failed { + testenvInstance.SkipTeardown = true + } + if deployment != nil { + deployment.Teardown() + } + }) + + Context("Multisite cluster deployment (M13 - Multisite indexer cluster, Search head cluster)", func() { + It("can deploy indexers and search head cluster", func() { + + err := deployment.DeploySingleSiteCluster(deployment.GetName(), 3) + Expect(err).To(Succeed(), "Unable to deploy cluster") + + // Ensure that the cluster-master goes to Ready phase + testenv.ClusterMasterReady(deployment, testenvInstance) + + // Ensure the indexers of all sites go to Ready phase + testenv.SingleSiteIndexersReady(deployment, testenvInstance) + + // Ensure search head cluster go to Ready phase + testenv.SearchHeadClusterReady(deployment, testenvInstance) + + // Verify MC Pod is Ready + testenv.MCPodReady(testenvInstance.GetName(), deployment) + + // Verify no SH in disconnected status is present on CM + testenv.VerifyNoDisconnectedSHPresentOnCM(deployment, testenvInstance) + + }) + }) +}) diff --git a/test/ingest_search/ingest_search_suite_test.go b/test/ingest_search/ingest_search_suite_test.go index ff71d88e6..f1d9f8bf9 100644 --- a/test/ingest_search/ingest_search_suite_test.go +++ b/test/ingest_search/ingest_search_suite_test.go @@ -22,7 +22,7 @@ const ( var ( testenvInstance *testenv.TestEnv - testSuiteName = "mc-" + testenv.RandomDNSName(2) + testSuiteName = "ingestsearch-" + testenv.RandomDNSName(2) ) // TestBasic is the main entry point diff --git a/test/monitoring_console/monitoring_console_test.go b/test/monitoring_console/monitoring_console_test.go index fc08eb78c..c34aa17b6 100644 --- a/test/monitoring_console/monitoring_console_test.go +++ b/test/monitoring_console/monitoring_console_test.go @@ -36,8 +36,7 @@ var _ = Describe("Montioring Console test", func() { } }) - // Disabling this test because of failure. The manual test passes. - XContext("Standalone deployment (S1)", func() { + Context("Standalone deployment (S1)", func() { It("can deploy a MC with standalone instance and update MC with new standalone deployment", func() { standaloneOneName := deployment.GetName() @@ -98,7 +97,7 @@ var _ = Describe("Montioring Console test", func() { }) }) - XContext("Standalone deployment with Scale up", func() { + Context("Standalone deployment with Scale up", func() { It("can deploy a MC with standalone instance and update MC when standalone is scaled up", func() { standalone, err := deployment.DeployStandalone(deployment.GetName()) @@ -168,7 +167,7 @@ var _ = Describe("Montioring Console test", func() { }) }) - XContext("SearchHeadCluster deployment with Scale Up", func() { + Context("SearchHeadCluster deployment with Scale Up", func() { It("MC can configure SHC instances after scale up in a namespace", func() { _, err := deployment.DeploySearchHeadCluster(deployment.GetName(), "", "", "") @@ -232,7 +231,7 @@ var _ = Describe("Montioring Console test", func() { }) }) - XContext("SearchHeadCluster and Standalone", func() { + Context("SearchHeadCluster and Standalone", func() { It("MC can configure SHC and Standalone instances in a namespace", func() { _, err := deployment.DeploySearchHeadCluster(deployment.GetName(), "", "", "") diff --git a/test/run-tests.sh b/test/run-tests.sh index e7eb2460f..6123fd8cc 100755 --- a/test/run-tests.sh +++ b/test/run-tests.sh @@ -77,4 +77,5 @@ fi echo "Running test using number of nodes: ${NUM_NODES}" echo "Running test using these images: ${PRIVATE_SPLUNK_OPERATOR_IMAGE} and ${PRIVATE_SPLUNK_ENTERPRISE_IMAGE}..." # run Ginkgo -ginkgo -v -progress -r -stream -nodes=${NUM_NODES} -skipPackage=example,monitoringconsoletest ${topdir}/test -- -commit-hash=${COMMIT_HASH} -operator-image=${PRIVATE_SPLUNK_OPERATOR_IMAGE} -splunk-image=${PRIVATE_SPLUNK_ENTERPRISE_IMAGE} +# Running only smoke test cases. To run different test packages add/remove path from skipPackage argument +ginkgo -v -progress -r -stream -nodes=${NUM_NODES} --skipPackage=example,ingest_search,monitoring_console,delete_cr ${topdir}/test -- -commit-hash=${COMMIT_HASH} -operator-image=${PRIVATE_SPLUNK_OPERATOR_IMAGE} -splunk-image=${PRIVATE_SPLUNK_ENTERPRISE_IMAGE} diff --git a/test/smoke/smoke_test.go b/test/smoke/smoke_test.go index f1f75b132..2b0906997 100644 --- a/test/smoke/smoke_test.go +++ b/test/smoke/smoke_test.go @@ -1,16 +1,12 @@ package smoke import ( - "encoding/json" - "fmt" "os/exec" "strings" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" - enterprisev1 "github.com/splunk/splunk-operator/pkg/apis/enterprise/v1beta1" - splcommon "github.com/splunk/splunk-operator/pkg/splunk/common" "github.com/splunk/splunk-operator/test/testenv" ) @@ -47,22 +43,8 @@ var _ = Describe("Smoke test", func() { standalone, err := deployment.DeployStandalone(deployment.GetName()) Expect(err).To(Succeed(), "Unable to deploy standalone instance ") - Eventually(func() splcommon.Phase { - err = deployment.GetInstance(deployment.GetName(), standalone) - if err != nil { - return splcommon.PhaseError - } - testenvInstance.Log.Info("Waiting for standalone instance status to be ready", "instance", standalone.ObjectMeta.Name, "Phase", standalone.Status.Phase) - dumpGetPods(testenvInstance.GetName()) - - return standalone.Status.Phase - }, deployment.GetTimeout(), PollInterval).Should(Equal(splcommon.PhaseReady)) - - // In a steady state, we should stay in Ready and not flip-flop around - Consistently(func() splcommon.Phase { - _ = deployment.GetInstance(deployment.GetName(), standalone) - return standalone.Status.Phase - }, ConsistentDuration, ConsistentPollInterval).Should(Equal(splcommon.PhaseReady)) + // Verify standalone goes to ready state + testenv.StandaloneReady(deployment, deployment.GetName(), standalone, testenvInstance) // Verify MC Pod is Ready testenv.MCPodReady(testenvInstance.GetName(), deployment) @@ -77,61 +59,13 @@ var _ = Describe("Smoke test", func() { Expect(err).To(Succeed(), "Unable to deploy cluster") // Ensure that the cluster-master goes to Ready phase - cm := &enterprisev1.ClusterMaster{} - Eventually(func() splcommon.Phase { - err := deployment.GetInstance(deployment.GetName(), cm) - if err != nil { - return splcommon.PhaseError - } - testenvInstance.Log.Info("Waiting for cluster-master instance status to be ready", "instance", cm.ObjectMeta.Name, "Phase", cm.Status.Phase) - dumpGetPods(testenvInstance.GetName()) - // Test ClusterMaster Phase to see if its ready - return cm.Status.Phase - }, deployment.GetTimeout(), PollInterval).Should(Equal(splcommon.PhaseReady)) - - // In a steady state, cluster-master should stay in Ready and not flip-flop around - Consistently(func() splcommon.Phase { - _ = deployment.GetInstance(deployment.GetName(), cm) - return cm.Status.Phase - }, ConsistentDuration, ConsistentPollInterval).Should(Equal(splcommon.PhaseReady)) + testenv.ClusterMasterReady(deployment, testenvInstance) // Ensure indexers go to Ready phase - idc := &enterprisev1.IndexerCluster{} - instanceName := fmt.Sprintf("%s-idxc", deployment.GetName()) - Eventually(func() splcommon.Phase { - err := deployment.GetInstance(instanceName, idc) - if err != nil { - return splcommon.PhaseError - } - testenvInstance.Log.Info("Waiting for indexer instance's status to be ready", "instance", instanceName, "Phase", idc.Status.Phase) - dumpGetPods(testenvInstance.GetName()) - return idc.Status.Phase - }, deployment.GetTimeout(), PollInterval).Should(Equal(splcommon.PhaseReady)) - - // In a steady state, we should stay in Ready and not flip-flop around - Consistently(func() splcommon.Phase { - _ = deployment.GetInstance(instanceName, idc) - return idc.Status.Phase - }, ConsistentDuration, ConsistentPollInterval).Should(Equal(splcommon.PhaseReady)) + testenv.SingleSiteIndexersReady(deployment, testenvInstance) // Ensure search head cluster go to Ready phase - shc := &enterprisev1.SearchHeadCluster{} - instanceName = fmt.Sprintf("%s-shc", deployment.GetName()) - Eventually(func() splcommon.Phase { - err := deployment.GetInstance(instanceName, shc) - if err != nil { - return splcommon.PhaseError - } - testenvInstance.Log.Info("Waiting for search head cluster STATUS to be ready", "instance", shc.ObjectMeta.Name, "Phase", shc.Status.Phase) - dumpGetPods(testenvInstance.GetName()) - return shc.Status.Phase - }, deployment.GetTimeout(), PollInterval).Should(Equal(splcommon.PhaseReady)) - - // In a steady state, we should stay in Ready and not flip-flop around - Consistently(func() splcommon.Phase { - _ = deployment.GetInstance(deployment.GetName(), shc) - return shc.Status.Phase - }, ConsistentDuration, ConsistentPollInterval).Should(Equal(splcommon.PhaseReady)) + testenv.SearchHeadClusterReady(deployment, testenvInstance) // Verify MC Pod is Ready testenv.MCPodReady(testenvInstance.GetName(), deployment) @@ -149,89 +83,16 @@ var _ = Describe("Smoke test", func() { Expect(err).To(Succeed(), "Unable to deploy cluster") // Ensure that the cluster-master goes to Ready phase - cm := &enterprisev1.ClusterMaster{} - Eventually(func() splcommon.Phase { - err := deployment.GetInstance(deployment.GetName(), cm) - if err != nil { - return splcommon.PhaseError - } - testenvInstance.Log.Info("Waiting for cluster-master instance status to be ready", "instance", cm.ObjectMeta.Name, "Phase", cm.Status.Phase) - dumpGetPods(testenvInstance.GetName()) - // Test ClusterMaster Phase to see if its ready - return cm.Status.Phase - }, deployment.GetTimeout(), PollInterval).Should(Equal(splcommon.PhaseReady)) - - // In a steady state, cluster-master should stay in Ready and not flip-flop around - Consistently(func() splcommon.Phase { - _ = deployment.GetInstance(deployment.GetName(), cm) - return cm.Status.Phase - }, ConsistentDuration, ConsistentPollInterval).Should(Equal(splcommon.PhaseReady)) + testenv.ClusterMasterReady(deployment, testenvInstance) // Ensure the indexers of all sites go to Ready phase - siteIndexerMap := map[string][]string{} - for site := 1; site <= siteCount; site++ { - siteName := fmt.Sprintf("site%d", site) - instanceName := fmt.Sprintf("%s-%s", deployment.GetName(), siteName) - siteIndexerMap[siteName] = []string{fmt.Sprintf("splunk-%s-indexer-0", instanceName)} - // Ensure indexers go to Ready phase - idc := &enterprisev1.IndexerCluster{} - Eventually(func() splcommon.Phase { - err := deployment.GetInstance(instanceName, idc) - if err != nil { - return splcommon.PhaseError - } - testenvInstance.Log.Info("Waiting for indexer site instance status to be ready", "instance", instanceName, "Phase", idc.Status.Phase) - dumpGetPods(testenvInstance.GetName()) - return idc.Status.Phase - }, deployment.GetTimeout(), PollInterval).Should(Equal(splcommon.PhaseReady)) - - // In a steady state, we should stay in Ready and not flip-flop around - Consistently(func() splcommon.Phase { - _ = deployment.GetInstance(instanceName, idc) - return idc.Status.Phase - }, ConsistentDuration, ConsistentPollInterval).Should(Equal(splcommon.PhaseReady)) - } + testenv.IndexersReady(deployment, testenvInstance, siteCount) // Ensure cluster configured as multisite - Eventually(func() map[string][]string { - podName := fmt.Sprintf("splunk-%s-cluster-master-0", deployment.GetName()) - stdin := "curl -ks -u admin:$(cat /mnt/splunk-secrets/password) https://localhost:8089/services/cluster/master/sites?output_mode=json" - command := []string{"/bin/sh"} - stdout, stderr, err := deployment.PodExecCommand(podName, command, stdin, false) - if err != nil { - testenvInstance.Log.Error(err, "Failed to execute command on pod", "pod", podName, "command", command) - return map[string][]string{} - } - testenvInstance.Log.Info("Command executed on pod", "pod", podName, "command", command, "stdin", stdin, "stdout", stdout, "stderr", stderr) - siteIndexerResponse := ClusterMasterSitesResponse{} - json.Unmarshal([]byte(stdout), &siteIndexerResponse) - siteIndexerStatus := map[string][]string{} - for _, site := range siteIndexerResponse.Entries { - siteIndexerStatus[site.Name] = []string{} - for _, peer := range site.Content.Peers { - siteIndexerStatus[site.Name] = append(siteIndexerStatus[site.Name], peer.ServerName) - } - } - return siteIndexerStatus - }, deployment.GetTimeout(), PollInterval).Should(Equal(siteIndexerMap)) - - shc := &enterprisev1.SearchHeadCluster{} - instanceName := fmt.Sprintf("%s-shc", deployment.GetName()) + testenv.IndexerClusterMultisiteStatus(deployment, testenvInstance, siteCount) + // Ensure search head cluster go to Ready phase - Eventually(func() splcommon.Phase { - err := deployment.GetInstance(instanceName, shc) - if err != nil { - return splcommon.PhaseError - } - testenvInstance.Log.Info("Waiting for search head cluster STATUS to be ready", "instance", shc.ObjectMeta.Name, "Phase", shc.Status.Phase) - return shc.Status.Phase - }, deployment.GetTimeout(), PollInterval).Should(Equal(splcommon.PhaseReady)) - - // In a steady state, we should stay in Ready and not flip-flop around - Consistently(func() splcommon.Phase { - _ = deployment.GetInstance(deployment.GetName(), shc) - return shc.Status.Phase - }, ConsistentDuration, ConsistentPollInterval).Should(Equal(splcommon.PhaseReady)) + testenv.SearchHeadClusterReady(deployment, testenvInstance) // Verify MC Pod is Ready testenv.MCPodReady(testenvInstance.GetName(), deployment) @@ -249,71 +110,13 @@ var _ = Describe("Smoke test", func() { Expect(err).To(Succeed(), "Unable to deploy cluster") // Ensure that the cluster-master goes to Ready phase - cm := &enterprisev1.ClusterMaster{} - Eventually(func() splcommon.Phase { - err := deployment.GetInstance(deployment.GetName(), cm) - if err != nil { - return splcommon.PhaseError - } - testenvInstance.Log.Info("Waiting for cluster-master instance status to be ready", "instance", cm.ObjectMeta.Name, "Phase", cm.Status.Phase) - dumpGetPods(testenvInstance.GetName()) - // Test ClusterMaster Phase to see if its ready - return cm.Status.Phase - }, deployment.GetTimeout(), PollInterval).Should(Equal(splcommon.PhaseReady)) - - // In a steady state, cluster-master should stay in Ready and not flip-flop around - Consistently(func() splcommon.Phase { - _ = deployment.GetInstance(deployment.GetName(), cm) - return cm.Status.Phase - }, ConsistentDuration, ConsistentPollInterval).Should(Equal(splcommon.PhaseReady)) + testenv.ClusterMasterReady(deployment, testenvInstance) // Ensure the indexers of all sites go to Ready phase - siteIndexerMap := map[string][]string{} - for site := 1; site <= siteCount; site++ { - siteName := fmt.Sprintf("site%d", site) - instanceName := fmt.Sprintf("%s-%s", deployment.GetName(), siteName) - siteIndexerMap[siteName] = []string{fmt.Sprintf("splunk-%s-indexer-0", instanceName)} - // Ensure indexers go to Ready phase - idc := &enterprisev1.IndexerCluster{} - Eventually(func() splcommon.Phase { - err := deployment.GetInstance(instanceName, idc) - if err != nil { - return splcommon.PhaseError - } - testenvInstance.Log.Info("Waiting for indexer site instance status to be ready", "instance", instanceName, "Phase", idc.Status.Phase) - dumpGetPods(testenvInstance.GetName()) - return idc.Status.Phase - }, deployment.GetTimeout(), PollInterval).Should(Equal(splcommon.PhaseReady)) - - // In a steady state, we should stay in Ready and not flip-flop around - Consistently(func() splcommon.Phase { - _ = deployment.GetInstance(instanceName, idc) - return idc.Status.Phase - }, ConsistentDuration, ConsistentPollInterval).Should(Equal(splcommon.PhaseReady)) - } + testenv.IndexersReady(deployment, testenvInstance, siteCount) // Ensure cluster configured as multisite - Eventually(func() map[string][]string { - podName := fmt.Sprintf("splunk-%s-cluster-master-0", deployment.GetName()) - stdin := "curl -ks -u admin:$(cat /mnt/splunk-secrets/password) https://localhost:8089/services/cluster/master/sites?output_mode=json" - command := []string{"/bin/sh"} - stdout, stderr, err := deployment.PodExecCommand(podName, command, stdin, false) - if err != nil { - testenvInstance.Log.Error(err, "Failed to execute command on pod", "pod", podName, "command", command) - return map[string][]string{} - } - testenvInstance.Log.Info("Command executed on pod", "pod", podName, "command", command, "stdin", stdin, "stdout", stdout, "stderr", stderr) - siteIndexerResponse := ClusterMasterSitesResponse{} - json.Unmarshal([]byte(stdout), &siteIndexerResponse) - siteIndexerStatus := map[string][]string{} - for _, site := range siteIndexerResponse.Entries { - siteIndexerStatus[site.Name] = []string{} - for _, peer := range site.Content.Peers { - siteIndexerStatus[site.Name] = append(siteIndexerStatus[site.Name], peer.ServerName) - } - } - return siteIndexerStatus - }, deployment.GetTimeout(), PollInterval).Should(Equal(siteIndexerMap)) + testenv.IndexerClusterMultisiteStatus(deployment, testenvInstance, siteCount) // Verify MC Pod is Ready testenv.MCPodReady(testenvInstance.GetName(), deployment) diff --git a/test/testenv/cmutil.go b/test/testenv/cmutil.go index d1b4fa568..d848160b8 100644 --- a/test/testenv/cmutil.go +++ b/test/testenv/cmutil.go @@ -58,3 +58,73 @@ func CheckRFSF(deployment *Deployment) bool { } return rfMet && sfMet } + +// ClusterMasterSearchHeadResponse /services/cluster/master/searchhead response +type ClusterMasterSearchHeadResponse struct { + Entries []ClusterMasterSearchHeadEntry `json:"entry"` +} + +// ClusterMasterSearchHeadEntry represents a single search head +type ClusterMasterSearchHeadEntry struct { + Name string `json:"name"` + Content ClusterMasterSearchHeadContent `json:"content"` +} + +// ClusterMasterSearchHeadContent represents detailed information about a search head +type ClusterMasterSearchHeadContent struct { + EaiACL interface{} `json:"eai:acl"` + HostPortPair string `json:"host_port_pair"` + Label string `json:"label"` + Site string `json:"site"` + Status string `json:"status"` +} + +// CheckSearchHeadRemoved check if search head is removed from Indexer Cluster +func CheckSearchHeadRemoved(deployment *Deployment) bool { + //code to execute + podName := fmt.Sprintf("splunk-%s-cluster-master-0", deployment.GetName()) + stdin := "curl -ks -u admin:$(cat /mnt/splunk-secrets/password) https://localhost:8089/services/cluster/master/searchheads?output_mode=json" + command := []string{"/bin/sh"} + stdout, stderr, err := deployment.PodExecCommand(podName, command, stdin, false) + if err != nil { + logf.Log.Error(err, "Failed to execute command on pod", "pod", podName, "command", command) + return false + } + logf.Log.Info("Command executed on pod", "pod", podName, "command", command, "stdin", stdin, "stdout", stdout, "stderr", stderr) + restResponse := ClusterMasterSearchHeadResponse{} + err = json.Unmarshal([]byte(stdout), &restResponse) + if err != nil { + logf.Log.Error(err, "Failed to parse cluster searchheads") + return false + } + searchHeadRemoved := true + for _, entry := range restResponse.Entries { + logf.Log.Info("Search Found", "Search Head", entry.Content.Label, "Status", entry.Content.Status) + if entry.Content.Status == "Disconnected" { + searchHeadRemoved = false + } + } + return searchHeadRemoved +} + +// ClusterMasterSitesResponse is a representation of the sites managed by a Splunk cluster-master +// Endpoint: /services/cluster/master/sites +type ClusterMasterSitesResponse struct { + Entries []ClusterMasterSitesEntry `json:"entry"` +} + +// ClusterMasterSitesEntry represents a site of an indexer cluster with its metadata +type ClusterMasterSitesEntry struct { + Name string `json:"name"` + Content ClusterMasterSitesContent `json:"content"` +} + +// ClusterMasterSitesContent represents detailed information about a site +type ClusterMasterSitesContent struct { + Peers map[string]ClusterMasterSitesPeer `json:"peers"` +} + +// ClusterMasterSitesPeer reprensents an indexer peer member of a site +type ClusterMasterSitesPeer struct { + ServerName string `json:"server_name"` +} diff --git a/test/testenv/search_head_cluster_utils.go b/test/testenv/search_head_cluster_utils.go new file mode 100644 index 000000000..55938b483 --- /dev/null +++ b/test/testenv/search_head_cluster_utils.go @@ -0,0 +1,36 @@ +package testenv + +import ( + "fmt" + "os/exec" + "strings" + + logf "sigs.k8s.io/controller-runtime/pkg/log" +) + +// DeleteSHC delete Search Head Cluster in given namespace +func DeleteSHC(ns string) { + output, err := exec.Command("kubectl", "delete", "shc", "-n", ns, "--all").Output() + if err != nil { + cmd := fmt.Sprintf("kubectl delete shc -n %s --all", ns) + logf.Log.Error(err, "Failed to execute command", "command", cmd) + } else { + logf.Log.Info("SHC deleted", "Namespace", ns, "stdout", output) + } +} + +// SHCInNamespace returns true if SHC is present in namespace +func SHCInNamespace(ns string) bool { + output, err := exec.Command("kubectl", "get", "searchheadcluster", "-n", ns).Output() + deleted := true + if err != nil { + cmd := fmt.Sprintf("kubectl get shc -n %s", ns) + logf.Log.Error(err, "Failed to execute command", "command", cmd) + return deleted + } + logf.Log.Info("Output of command", "Output", string(output)) + if strings.Contains(string(output), "No resources found in default namespace") { + deleted = false + } + return deleted +} diff --git a/test/testenv/verificationutils.go b/test/testenv/verificationutils.go index 327b3f86d..794845606 100644 --- a/test/testenv/verificationutils.go +++ b/test/testenv/verificationutils.go @@ -1,6 +1,8 @@ package testenv import ( + "encoding/json" + "fmt" gomega "github.com/onsi/gomega" enterprisev1 "github.com/splunk/splunk-operator/pkg/apis/enterprise/v1beta1" @@ -29,8 +31,9 @@ func StandaloneReady(deployment *Deployment, deploymentName string, standalone * // SearchHeadClusterReady verify SHC is in READY status and does not flip-flop func SearchHeadClusterReady(deployment *Deployment, testenvInstance *TestEnv) { shc := &enterprisev1.SearchHeadCluster{} + instanceName := fmt.Sprintf("%s-shc", deployment.GetName()) gomega.Eventually(func() splcommon.Phase { - err := deployment.GetInstance(deployment.GetName(), shc) + err := deployment.GetInstance(instanceName, shc) if err != nil { return splcommon.PhaseError } @@ -46,6 +49,107 @@ func SearchHeadClusterReady(deployment *Deployment, testenvInstance *TestEnv) { }, ConsistentDuration, ConsistentPollInterval).Should(gomega.Equal(splcommon.PhaseReady)) } +// SingleSiteIndexersReady verify single site indexers go to ready state +func SingleSiteIndexersReady(deployment *Deployment, testenvInstance *TestEnv) { + idc := &enterprisev1.IndexerCluster{} + instanceName := fmt.Sprintf("%s-idxc", deployment.GetName()) + gomega.Eventually(func() splcommon.Phase { + err := deployment.GetInstance(instanceName, idc) + if err != nil { + return splcommon.PhaseError + } + testenvInstance.Log.Info("Waiting for indexer instance's status to be ready", "instance", instanceName, "Phase", idc.Status.Phase) + DumpGetPods(testenvInstance.GetName()) + return idc.Status.Phase + }, deployment.GetTimeout(), PollInterval).Should(gomega.Equal(splcommon.PhaseReady)) + + // In a steady state, we should stay in Ready and not flip-flop around + gomega.Consistently(func() splcommon.Phase { + _ = deployment.GetInstance(instanceName, idc) + return idc.Status.Phase + }, ConsistentDuration, ConsistentPollInterval).Should(gomega.Equal(splcommon.PhaseReady)) +} + +// ClusterMasterReady verify Cluster Master Instance is in ready status +func ClusterMasterReady(deployment *Deployment, testenvInstance *TestEnv) { + // Ensure that the cluster-master goes to Ready phase + cm := &enterprisev1.ClusterMaster{} + gomega.Eventually(func() splcommon.Phase { + err := deployment.GetInstance(deployment.GetName(), cm) + if err != nil { + return splcommon.PhaseError + } + testenvInstance.Log.Info("Waiting for cluster-master instance status to be ready", "instance", cm.ObjectMeta.Name, "Phase", cm.Status.Phase) + DumpGetPods(testenvInstance.GetName()) + // Test ClusterMaster Phase to see if its ready + return cm.Status.Phase + }, deployment.GetTimeout(), PollInterval).Should(gomega.Equal(splcommon.PhaseReady)) + + // In a steady state, cluster-master should stay in Ready and not flip-flop around + gomega.Consistently(func() splcommon.Phase { + _ = deployment.GetInstance(deployment.GetName(), cm) + return cm.Status.Phase + }, ConsistentDuration, ConsistentPollInterval).Should(gomega.Equal(splcommon.PhaseReady)) +} + +// IndexersReady verify indexers of all sites go to ready state +func IndexersReady(deployment *Deployment, testenvInstance *TestEnv, siteCount int) { + siteIndexerMap := map[string][]string{} + for site := 1; site <= siteCount; site++ { + siteName := fmt.Sprintf("site%d", site) + instanceName := fmt.Sprintf("%s-%s", deployment.GetName(), siteName) + siteIndexerMap[siteName] = []string{fmt.Sprintf("splunk-%s-indexer-0", instanceName)} + // Ensure indexers go to Ready phase + idc := &enterprisev1.IndexerCluster{} + gomega.Eventually(func() splcommon.Phase { + err := deployment.GetInstance(instanceName, idc) + if err != nil { + return splcommon.PhaseError + } + testenvInstance.Log.Info("Waiting for indexer site instance status to be ready", "instance", instanceName, "Phase", idc.Status.Phase) + DumpGetPods(testenvInstance.GetName()) + return idc.Status.Phase + }, deployment.GetTimeout(), PollInterval).Should(gomega.Equal(splcommon.PhaseReady)) + + // In a steady state, we should stay in Ready and not flip-flop around + gomega.Consistently(func() splcommon.Phase { + _ = deployment.GetInstance(instanceName, idc) + return idc.Status.Phase + }, ConsistentDuration, ConsistentPollInterval).Should(gomega.Equal(splcommon.PhaseReady)) + } +} + +// IndexerClusterMultisiteStatus verify indexer Cluster is configured as multisite +func IndexerClusterMultisiteStatus(deployment *Deployment, testenvInstance *TestEnv, siteCount int) { + siteIndexerMap := map[string][]string{} + for site := 1; site <= siteCount; site++ { + siteName := fmt.Sprintf("site%d", site) + instanceName := fmt.Sprintf("%s-%s", deployment.GetName(), siteName) + siteIndexerMap[siteName] = []string{fmt.Sprintf("splunk-%s-indexer-0", instanceName)} + } + gomega.Eventually(func() map[string][]string { + podName := fmt.Sprintf("splunk-%s-cluster-master-0", deployment.GetName()) + stdin := "curl -ks -u admin:$(cat /mnt/splunk-secrets/password) https://localhost:8089/services/cluster/master/sites?output_mode=json" + command := []string{"/bin/sh"} + stdout, stderr, err := deployment.PodExecCommand(podName, command, stdin, false) + if err != nil { + testenvInstance.Log.Error(err, "Failed to execute command on pod", "pod", podName, "command", command) + return map[string][]string{} + } + testenvInstance.Log.Info("Command executed on pod", "pod", podName, "command", command, "stdin", stdin, "stdout", stdout, "stderr", stderr) + siteIndexerResponse := ClusterMasterSitesResponse{} + json.Unmarshal([]byte(stdout), &siteIndexerResponse) + siteIndexerStatus := map[string][]string{} + for _, site := range siteIndexerResponse.Entries { + siteIndexerStatus[site.Name] = []string{} + for _, peer := range site.Content.Peers { + siteIndexerStatus[site.Name] = append(siteIndexerStatus[site.Name], peer.ServerName) + } + } + return siteIndexerStatus + }, deployment.GetTimeout(), PollInterval).Should(gomega.Equal(siteIndexerMap)) +} + // VerifyRFSFMet verify RF SF is met on cluster masterr func VerifyRFSFMet(deployment *Deployment, testenvInstance *TestEnv) { gomega.Eventually(func() bool { @@ -54,3 +158,21 @@ func VerifyRFSFMet(deployment *Deployment, testenvInstance *TestEnv) { return rfSfStatus }, deployment.GetTimeout(), PollInterval).Should(gomega.Equal(true)) } + +// VerifyNoDisconnectedSHPresentOnCM is present on cluster master +func VerifyNoDisconnectedSHPresentOnCM(deployment *Deployment, testenvInstance *TestEnv) { + gomega.Eventually(func() bool { + shStatus := CheckSearchHeadRemoved(deployment) + testenvInstance.Log.Info("Verifying no SH in DISCONNECTED state present on CM", "Status", shStatus) + return shStatus + }, deployment.GetTimeout(), PollInterval).Should(gomega.Equal(true)) +} + +// VerifyNoSHCInNamespace verify no SHC is present in namespace +func VerifyNoSHCInNamespace(deployment *Deployment, testenvInstance *TestEnv) { + gomega.Eventually(func() bool { + shcStatus := SHCInNamespace(testenvInstance.GetName()) + testenvInstance.Log.Info("Verifying no SHC is present in namespace", "Status", shcStatus) + return shcStatus + }, deployment.GetTimeout(), PollInterval).Should(gomega.Equal(false)) +} From e05bb118111adbc582c6751b3ca40c055ad1d74d Mon Sep 17 00:00:00 2001 From: akondur Date: Tue, 5 Jan 2021 10:41:46 -0800 Subject: [PATCH 10/43] Adding API version in multisite examples docs --- docs/MultisiteExamples.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/MultisiteExamples.md b/docs/MultisiteExamples.md index cb4d15639..b4713cb75 100644 --- a/docs/MultisiteExamples.md +++ b/docs/MultisiteExamples.md @@ -49,6 +49,7 @@ Note: the image version is defined in these resources as this allows to control ```yaml cat < Date: Tue, 5 Jan 2021 17:48:36 -0800 Subject: [PATCH 11/43] Storage class revamp --- ...erprise.splunk.com_clustermasters_crd.yaml | 51 +++-- ...rprise.splunk.com_indexerclusters_crd.yaml | 49 +++-- ...erprise.splunk.com_licensemasters_crd.yaml | 51 +++-- ...ise.splunk.com_searchheadclusters_crd.yaml | 51 +++-- ...enterprise.splunk.com_standalones_crd.yaml | 51 +++-- ...erprise.splunk.com_clustermasters_crd.yaml | 49 +++-- ...rprise.splunk.com_indexerclusters_crd.yaml | 49 +++-- ...erprise.splunk.com_licensemasters_crd.yaml | 49 +++-- ...ise.splunk.com_searchheadclusters_crd.yaml | 49 +++-- ...enterprise.splunk.com_standalones_crd.yaml | 49 +++-- docs/StorageClass.md | 52 +++-- go.mod | 1 + go.sum | 13 ++ pkg/apis/enterprise/v1beta1/common_types.go | 26 ++- .../v1beta1/zz_generated.deepcopy.go | 18 ++ pkg/splunk/common/names.go | 21 ++ pkg/splunk/enterprise/clustermaster_test.go | 4 +- pkg/splunk/enterprise/configuration.go | 193 ++++++++++-------- pkg/splunk/enterprise/configuration_test.go | 69 +++++++ pkg/splunk/enterprise/indexercluster_test.go | 2 +- pkg/splunk/enterprise/standalone_test.go | 9 +- 21 files changed, 637 insertions(+), 269 deletions(-) diff --git a/deploy/crds/enterprise.splunk.com_clustermasters_crd.yaml b/deploy/crds/enterprise.splunk.com_clustermasters_crd.yaml index d02058084..64d70f3c2 100644 --- a/deploy/crds/enterprise.splunk.com_clustermasters_crd.yaml +++ b/deploy/crds/enterprise.splunk.com_clustermasters_crd.yaml @@ -660,17 +660,27 @@ spec: by commas type: string defaultsUrlApps: - description: Full path or URL for one or more default.yml files for installing apps, separated - by commas - type: string - ephemeralStorage: - description: If true, ephemeral (emptyDir) storage will be used for - /opt/splunk/etc and /opt/splunk/var volumes - type: boolean - etcStorage: - description: Storage capacity to request for /opt/splunk/etc persistent - volume claims (default=”1Gi”) + description: Full path or URL for one or more default.yml files specific + to App install, separated by commas This is meant as a temporary fix + until the SHC Deployer has its own CRD. The defaults listed here + will only be installed on a standalone, or a SHC Deployer or IDC CM + to be pushed to SH/IDXs in a cluster. This parameter is ignored on + individual SHs or IDXs within a cluster type: string + etcVolumeStorageConfig: + description: Storage configuration for /opt/splunk/etc volume + properties: + ephemeralStorage: + description: If true, ephemeral (emptyDir) storage will be used + type: boolean + storageCapacity: + description: Storage capacity to request persistent volume claims + (default=”10Gi” for etc and "100Gi" for var) + type: string + storageClassName: + description: Name of StorageClass to use for persistent volume claims + type: string + type: object image: description: Image to use for Splunk pod containers (overrides RELATED_IMAGE_SPLUNK_ENTERPRISE environment variables) @@ -1091,9 +1101,6 @@ spec: to setup the soft links from ../master-apps/splunk-operator/local/ to /mnt/splunk-operator/local/ type: string - storageClassName: - description: Name of StorageClass to use for persistent volume claims - type: string tolerations: description: Pod's tolerations for Kubernetes node's taint items: @@ -1134,10 +1141,20 @@ spec: type: string type: object type: array - varStorage: - description: Storage capacity to request for /opt/splunk/var persistent - volume claims (default=”50Gi”) - type: string + varVolumeStorageConfig: + description: Storage configuration for /opt/splunk/var volume + properties: + ephemeralStorage: + description: If true, ephemeral (emptyDir) storage will be used + type: boolean + storageCapacity: + description: Storage capacity to request persistent volume claims + (default=”10Gi” for etc and "100Gi" for var) + type: string + storageClassName: + description: Name of StorageClass to use for persistent volume claims + type: string + type: object volumes: description: List of one or more Kubernetes volumes. These will be mounted in all pod containers as as /mnt/ diff --git a/deploy/crds/enterprise.splunk.com_indexerclusters_crd.yaml b/deploy/crds/enterprise.splunk.com_indexerclusters_crd.yaml index 5b0f94156..9f350717e 100644 --- a/deploy/crds/enterprise.splunk.com_indexerclusters_crd.yaml +++ b/deploy/crds/enterprise.splunk.com_indexerclusters_crd.yaml @@ -681,14 +681,28 @@ spec: description: Full path or URL for one or more default.yml files, separated by commas type: string - ephemeralStorage: - description: If true, ephemeral (emptyDir) storage will be used for - /opt/splunk/etc and /opt/splunk/var volumes - type: boolean - etcStorage: - description: Storage capacity to request for /opt/splunk/etc persistent - volume claims (default=”1Gi”) + defaultsUrlApps: + description: Full path or URL for one or more default.yml files specific + to App install, separated by commas This is meant as a temporary fix + until the SHC Deployer has its own CRD. The defaults listed here + will only be installed on a standalone, or a SHC Deployer or IDC CM + to be pushed to SH/IDXs in a cluster. This parameter is ignored on + individual SHs or IDXs within a cluster type: string + etcVolumeStorageConfig: + description: Storage configuration for /opt/splunk/etc volume + properties: + ephemeralStorage: + description: If true, ephemeral (emptyDir) storage will be used + type: boolean + storageCapacity: + description: Storage capacity to request persistent volume claims + (default=”10Gi” for etc and "100Gi" for var) + type: string + storageClassName: + description: Name of StorageClass to use for persistent volume claims + type: string + type: object image: description: Image to use for Splunk pod containers (overrides RELATED_IMAGE_SPLUNK_ENTERPRISE environment variables) @@ -1005,9 +1019,6 @@ spec: type: object type: object type: object - storageClassName: - description: Name of StorageClass to use for persistent volume claims - type: string tolerations: description: Pod's tolerations for Kubernetes node's taint items: @@ -1048,10 +1059,20 @@ spec: type: string type: object type: array - varStorage: - description: Storage capacity to request for /opt/splunk/var persistent - volume claims (default=”50Gi”) - type: string + varVolumeStorageConfig: + description: Storage configuration for /opt/splunk/var volume + properties: + ephemeralStorage: + description: If true, ephemeral (emptyDir) storage will be used + type: boolean + storageCapacity: + description: Storage capacity to request persistent volume claims + (default=”10Gi” for etc and "100Gi" for var) + type: string + storageClassName: + description: Name of StorageClass to use for persistent volume claims + type: string + type: object volumes: description: List of one or more Kubernetes volumes. These will be mounted in all pod containers as as /mnt/ diff --git a/deploy/crds/enterprise.splunk.com_licensemasters_crd.yaml b/deploy/crds/enterprise.splunk.com_licensemasters_crd.yaml index 89200ca61..4101001f5 100644 --- a/deploy/crds/enterprise.splunk.com_licensemasters_crd.yaml +++ b/deploy/crds/enterprise.splunk.com_licensemasters_crd.yaml @@ -665,17 +665,27 @@ spec: by commas type: string defaultsUrlApps: - description: Full path or URL for one or more default.yml files for installing apps, separated - by commas - type: string - ephemeralStorage: - description: If true, ephemeral (emptyDir) storage will be used for - /opt/splunk/etc and /opt/splunk/var volumes - type: boolean - etcStorage: - description: Storage capacity to request for /opt/splunk/etc persistent - volume claims (default=”1Gi”) + description: Full path or URL for one or more default.yml files specific + to App install, separated by commas This is meant as a temporary fix + until the SHC Deployer has its own CRD. The defaults listed here + will only be installed on a standalone, or a SHC Deployer or IDC CM + to be pushed to SH/IDXs in a cluster. This parameter is ignored on + individual SHs or IDXs within a cluster type: string + etcVolumeStorageConfig: + description: Storage configuration for /opt/splunk/etc volume + properties: + ephemeralStorage: + description: If true, ephemeral (emptyDir) storage will be used + type: boolean + storageCapacity: + description: Storage capacity to request persistent volume claims + (default=”10Gi” for etc and "100Gi" for var) + type: string + storageClassName: + description: Name of StorageClass to use for persistent volume claims + type: string + type: object image: description: Image to use for Splunk pod containers (overrides RELATED_IMAGE_SPLUNK_ENTERPRISE environment variables) @@ -987,9 +997,6 @@ spec: type: object type: object type: object - storageClassName: - description: Name of StorageClass to use for persistent volume claims - type: string tolerations: description: Pod's tolerations for Kubernetes node's taint items: @@ -1030,10 +1037,20 @@ spec: type: string type: object type: array - varStorage: - description: Storage capacity to request for /opt/splunk/var persistent - volume claims (default=”50Gi”) - type: string + varVolumeStorageConfig: + description: Storage configuration for /opt/splunk/var volume + properties: + ephemeralStorage: + description: If true, ephemeral (emptyDir) storage will be used + type: boolean + storageCapacity: + description: Storage capacity to request persistent volume claims + (default=”10Gi” for etc and "100Gi" for var) + type: string + storageClassName: + description: Name of StorageClass to use for persistent volume claims + type: string + type: object volumes: description: List of one or more Kubernetes volumes. These will be mounted in all pod containers as as /mnt/ diff --git a/deploy/crds/enterprise.splunk.com_searchheadclusters_crd.yaml b/deploy/crds/enterprise.splunk.com_searchheadclusters_crd.yaml index b189409b8..3be124ef3 100644 --- a/deploy/crds/enterprise.splunk.com_searchheadclusters_crd.yaml +++ b/deploy/crds/enterprise.splunk.com_searchheadclusters_crd.yaml @@ -682,17 +682,27 @@ spec: by commas type: string defaultsUrlApps: - description: Full path or URL for one or more default.yml files for installing apps, separated - by commas - type: string - ephemeralStorage: - description: If true, ephemeral (emptyDir) storage will be used for - /opt/splunk/etc and /opt/splunk/var volumes - type: boolean - etcStorage: - description: Storage capacity to request for /opt/splunk/etc persistent - volume claims (default=”1Gi”) + description: Full path or URL for one or more default.yml files specific + to App install, separated by commas This is meant as a temporary fix + until the SHC Deployer has its own CRD. The defaults listed here + will only be installed on a standalone, or a SHC Deployer or IDC CM + to be pushed to SH/IDXs in a cluster. This parameter is ignored on + individual SHs or IDXs within a cluster type: string + etcVolumeStorageConfig: + description: Storage configuration for /opt/splunk/etc volume + properties: + ephemeralStorage: + description: If true, ephemeral (emptyDir) storage will be used + type: boolean + storageCapacity: + description: Storage capacity to request persistent volume claims + (default=”10Gi” for etc and "100Gi" for var) + type: string + storageClassName: + description: Name of StorageClass to use for persistent volume claims + type: string + type: object image: description: Image to use for Splunk pod containers (overrides RELATED_IMAGE_SPLUNK_ENTERPRISE environment variables) @@ -1051,9 +1061,6 @@ spec: description: 'UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids' type: string type: object - storageClassName: - description: Name of StorageClass to use for persistent volume claims - type: string tolerations: description: Pod's tolerations for Kubernetes node's taint items: @@ -1094,10 +1101,20 @@ spec: type: string type: object type: array - varStorage: - description: Storage capacity to request for /opt/splunk/var persistent - volume claims (default=”50Gi”) - type: string + varVolumeStorageConfig: + description: Storage configuration for /opt/splunk/var volume + properties: + ephemeralStorage: + description: If true, ephemeral (emptyDir) storage will be used + type: boolean + storageCapacity: + description: Storage capacity to request persistent volume claims + (default=”10Gi” for etc and "100Gi" for var) + type: string + storageClassName: + description: Name of StorageClass to use for persistent volume claims + type: string + type: object volumes: description: List of one or more Kubernetes volumes. These will be mounted in all pod containers as as /mnt/ diff --git a/deploy/crds/enterprise.splunk.com_standalones_crd.yaml b/deploy/crds/enterprise.splunk.com_standalones_crd.yaml index 293f7bdfe..0ec4085a6 100644 --- a/deploy/crds/enterprise.splunk.com_standalones_crd.yaml +++ b/deploy/crds/enterprise.splunk.com_standalones_crd.yaml @@ -675,17 +675,27 @@ spec: by commas type: string defaultsUrlApps: - description: Full path or URL for one or more default.yml files for installing apps, separated - by commas - type: string - ephemeralStorage: - description: If true, ephemeral (emptyDir) storage will be used for - /opt/splunk/etc and /opt/splunk/var volumes - type: boolean - etcStorage: - description: Storage capacity to request for /opt/splunk/etc persistent - volume claims (default=”1Gi”) + description: Full path or URL for one or more default.yml files specific + to App install, separated by commas This is meant as a temporary fix + until the SHC Deployer has its own CRD. The defaults listed here + will only be installed on a standalone, or a SHC Deployer or IDC CM + to be pushed to SH/IDXs in a cluster. This parameter is ignored on + individual SHs or IDXs within a cluster type: string + etcVolumeStorageConfig: + description: Storage configuration for /opt/splunk/etc volume + properties: + ephemeralStorage: + description: If true, ephemeral (emptyDir) storage will be used + type: boolean + storageCapacity: + description: Storage capacity to request persistent volume claims + (default=”10Gi” for etc and "100Gi" for var) + type: string + storageClassName: + description: Name of StorageClass to use for persistent volume claims + type: string + type: object image: description: Image to use for Splunk pod containers (overrides RELATED_IMAGE_SPLUNK_ENTERPRISE environment variables) @@ -1146,9 +1156,6 @@ spec: description: 'UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids' type: string type: object - storageClassName: - description: Name of StorageClass to use for persistent volume claims - type: string tolerations: description: Pod's tolerations for Kubernetes node's taint items: @@ -1189,10 +1196,20 @@ spec: type: string type: object type: array - varStorage: - description: Storage capacity to request for /opt/splunk/var persistent - volume claims (default=”50Gi”) - type: string + varVolumeStorageConfig: + description: Storage configuration for /opt/splunk/var volume + properties: + ephemeralStorage: + description: If true, ephemeral (emptyDir) storage will be used + type: boolean + storageCapacity: + description: Storage capacity to request persistent volume claims + (default=”10Gi” for etc and "100Gi" for var) + type: string + storageClassName: + description: Name of StorageClass to use for persistent volume claims + type: string + type: object volumes: description: List of one or more Kubernetes volumes. These will be mounted in all pod containers as as /mnt/ diff --git a/deploy/olm-catalog/splunk/0.2.1/enterprise.splunk.com_clustermasters_crd.yaml b/deploy/olm-catalog/splunk/0.2.1/enterprise.splunk.com_clustermasters_crd.yaml index 5c9b2adc7..64d70f3c2 100644 --- a/deploy/olm-catalog/splunk/0.2.1/enterprise.splunk.com_clustermasters_crd.yaml +++ b/deploy/olm-catalog/splunk/0.2.1/enterprise.splunk.com_clustermasters_crd.yaml @@ -659,14 +659,28 @@ spec: description: Full path or URL for one or more default.yml files, separated by commas type: string - ephemeralStorage: - description: If true, ephemeral (emptyDir) storage will be used for - /opt/splunk/etc and /opt/splunk/var volumes - type: boolean - etcStorage: - description: Storage capacity to request for /opt/splunk/etc persistent - volume claims (default=”1Gi”) + defaultsUrlApps: + description: Full path or URL for one or more default.yml files specific + to App install, separated by commas This is meant as a temporary fix + until the SHC Deployer has its own CRD. The defaults listed here + will only be installed on a standalone, or a SHC Deployer or IDC CM + to be pushed to SH/IDXs in a cluster. This parameter is ignored on + individual SHs or IDXs within a cluster type: string + etcVolumeStorageConfig: + description: Storage configuration for /opt/splunk/etc volume + properties: + ephemeralStorage: + description: If true, ephemeral (emptyDir) storage will be used + type: boolean + storageCapacity: + description: Storage capacity to request persistent volume claims + (default=”10Gi” for etc and "100Gi" for var) + type: string + storageClassName: + description: Name of StorageClass to use for persistent volume claims + type: string + type: object image: description: Image to use for Splunk pod containers (overrides RELATED_IMAGE_SPLUNK_ENTERPRISE environment variables) @@ -1087,9 +1101,6 @@ spec: to setup the soft links from ../master-apps/splunk-operator/local/ to /mnt/splunk-operator/local/ type: string - storageClassName: - description: Name of StorageClass to use for persistent volume claims - type: string tolerations: description: Pod's tolerations for Kubernetes node's taint items: @@ -1130,10 +1141,20 @@ spec: type: string type: object type: array - varStorage: - description: Storage capacity to request for /opt/splunk/var persistent - volume claims (default=”50Gi”) - type: string + varVolumeStorageConfig: + description: Storage configuration for /opt/splunk/var volume + properties: + ephemeralStorage: + description: If true, ephemeral (emptyDir) storage will be used + type: boolean + storageCapacity: + description: Storage capacity to request persistent volume claims + (default=”10Gi” for etc and "100Gi" for var) + type: string + storageClassName: + description: Name of StorageClass to use for persistent volume claims + type: string + type: object volumes: description: List of one or more Kubernetes volumes. These will be mounted in all pod containers as as /mnt/ diff --git a/deploy/olm-catalog/splunk/0.2.1/enterprise.splunk.com_indexerclusters_crd.yaml b/deploy/olm-catalog/splunk/0.2.1/enterprise.splunk.com_indexerclusters_crd.yaml index 5b0f94156..9f350717e 100644 --- a/deploy/olm-catalog/splunk/0.2.1/enterprise.splunk.com_indexerclusters_crd.yaml +++ b/deploy/olm-catalog/splunk/0.2.1/enterprise.splunk.com_indexerclusters_crd.yaml @@ -681,14 +681,28 @@ spec: description: Full path or URL for one or more default.yml files, separated by commas type: string - ephemeralStorage: - description: If true, ephemeral (emptyDir) storage will be used for - /opt/splunk/etc and /opt/splunk/var volumes - type: boolean - etcStorage: - description: Storage capacity to request for /opt/splunk/etc persistent - volume claims (default=”1Gi”) + defaultsUrlApps: + description: Full path or URL for one or more default.yml files specific + to App install, separated by commas This is meant as a temporary fix + until the SHC Deployer has its own CRD. The defaults listed here + will only be installed on a standalone, or a SHC Deployer or IDC CM + to be pushed to SH/IDXs in a cluster. This parameter is ignored on + individual SHs or IDXs within a cluster type: string + etcVolumeStorageConfig: + description: Storage configuration for /opt/splunk/etc volume + properties: + ephemeralStorage: + description: If true, ephemeral (emptyDir) storage will be used + type: boolean + storageCapacity: + description: Storage capacity to request persistent volume claims + (default=”10Gi” for etc and "100Gi" for var) + type: string + storageClassName: + description: Name of StorageClass to use for persistent volume claims + type: string + type: object image: description: Image to use for Splunk pod containers (overrides RELATED_IMAGE_SPLUNK_ENTERPRISE environment variables) @@ -1005,9 +1019,6 @@ spec: type: object type: object type: object - storageClassName: - description: Name of StorageClass to use for persistent volume claims - type: string tolerations: description: Pod's tolerations for Kubernetes node's taint items: @@ -1048,10 +1059,20 @@ spec: type: string type: object type: array - varStorage: - description: Storage capacity to request for /opt/splunk/var persistent - volume claims (default=”50Gi”) - type: string + varVolumeStorageConfig: + description: Storage configuration for /opt/splunk/var volume + properties: + ephemeralStorage: + description: If true, ephemeral (emptyDir) storage will be used + type: boolean + storageCapacity: + description: Storage capacity to request persistent volume claims + (default=”10Gi” for etc and "100Gi" for var) + type: string + storageClassName: + description: Name of StorageClass to use for persistent volume claims + type: string + type: object volumes: description: List of one or more Kubernetes volumes. These will be mounted in all pod containers as as /mnt/ diff --git a/deploy/olm-catalog/splunk/0.2.1/enterprise.splunk.com_licensemasters_crd.yaml b/deploy/olm-catalog/splunk/0.2.1/enterprise.splunk.com_licensemasters_crd.yaml index 5b2239343..4101001f5 100644 --- a/deploy/olm-catalog/splunk/0.2.1/enterprise.splunk.com_licensemasters_crd.yaml +++ b/deploy/olm-catalog/splunk/0.2.1/enterprise.splunk.com_licensemasters_crd.yaml @@ -664,14 +664,28 @@ spec: description: Full path or URL for one or more default.yml files, separated by commas type: string - ephemeralStorage: - description: If true, ephemeral (emptyDir) storage will be used for - /opt/splunk/etc and /opt/splunk/var volumes - type: boolean - etcStorage: - description: Storage capacity to request for /opt/splunk/etc persistent - volume claims (default=”1Gi”) + defaultsUrlApps: + description: Full path or URL for one or more default.yml files specific + to App install, separated by commas This is meant as a temporary fix + until the SHC Deployer has its own CRD. The defaults listed here + will only be installed on a standalone, or a SHC Deployer or IDC CM + to be pushed to SH/IDXs in a cluster. This parameter is ignored on + individual SHs or IDXs within a cluster type: string + etcVolumeStorageConfig: + description: Storage configuration for /opt/splunk/etc volume + properties: + ephemeralStorage: + description: If true, ephemeral (emptyDir) storage will be used + type: boolean + storageCapacity: + description: Storage capacity to request persistent volume claims + (default=”10Gi” for etc and "100Gi" for var) + type: string + storageClassName: + description: Name of StorageClass to use for persistent volume claims + type: string + type: object image: description: Image to use for Splunk pod containers (overrides RELATED_IMAGE_SPLUNK_ENTERPRISE environment variables) @@ -983,9 +997,6 @@ spec: type: object type: object type: object - storageClassName: - description: Name of StorageClass to use for persistent volume claims - type: string tolerations: description: Pod's tolerations for Kubernetes node's taint items: @@ -1026,10 +1037,20 @@ spec: type: string type: object type: array - varStorage: - description: Storage capacity to request for /opt/splunk/var persistent - volume claims (default=”50Gi”) - type: string + varVolumeStorageConfig: + description: Storage configuration for /opt/splunk/var volume + properties: + ephemeralStorage: + description: If true, ephemeral (emptyDir) storage will be used + type: boolean + storageCapacity: + description: Storage capacity to request persistent volume claims + (default=”10Gi” for etc and "100Gi" for var) + type: string + storageClassName: + description: Name of StorageClass to use for persistent volume claims + type: string + type: object volumes: description: List of one or more Kubernetes volumes. These will be mounted in all pod containers as as /mnt/ diff --git a/deploy/olm-catalog/splunk/0.2.1/enterprise.splunk.com_searchheadclusters_crd.yaml b/deploy/olm-catalog/splunk/0.2.1/enterprise.splunk.com_searchheadclusters_crd.yaml index 77d75f9aa..3be124ef3 100644 --- a/deploy/olm-catalog/splunk/0.2.1/enterprise.splunk.com_searchheadclusters_crd.yaml +++ b/deploy/olm-catalog/splunk/0.2.1/enterprise.splunk.com_searchheadclusters_crd.yaml @@ -681,14 +681,28 @@ spec: description: Full path or URL for one or more default.yml files, separated by commas type: string - ephemeralStorage: - description: If true, ephemeral (emptyDir) storage will be used for - /opt/splunk/etc and /opt/splunk/var volumes - type: boolean - etcStorage: - description: Storage capacity to request for /opt/splunk/etc persistent - volume claims (default=”1Gi”) + defaultsUrlApps: + description: Full path or URL for one or more default.yml files specific + to App install, separated by commas This is meant as a temporary fix + until the SHC Deployer has its own CRD. The defaults listed here + will only be installed on a standalone, or a SHC Deployer or IDC CM + to be pushed to SH/IDXs in a cluster. This parameter is ignored on + individual SHs or IDXs within a cluster type: string + etcVolumeStorageConfig: + description: Storage configuration for /opt/splunk/etc volume + properties: + ephemeralStorage: + description: If true, ephemeral (emptyDir) storage will be used + type: boolean + storageCapacity: + description: Storage capacity to request persistent volume claims + (default=”10Gi” for etc and "100Gi" for var) + type: string + storageClassName: + description: Name of StorageClass to use for persistent volume claims + type: string + type: object image: description: Image to use for Splunk pod containers (overrides RELATED_IMAGE_SPLUNK_ENTERPRISE environment variables) @@ -1047,9 +1061,6 @@ spec: description: 'UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids' type: string type: object - storageClassName: - description: Name of StorageClass to use for persistent volume claims - type: string tolerations: description: Pod's tolerations for Kubernetes node's taint items: @@ -1090,10 +1101,20 @@ spec: type: string type: object type: array - varStorage: - description: Storage capacity to request for /opt/splunk/var persistent - volume claims (default=”50Gi”) - type: string + varVolumeStorageConfig: + description: Storage configuration for /opt/splunk/var volume + properties: + ephemeralStorage: + description: If true, ephemeral (emptyDir) storage will be used + type: boolean + storageCapacity: + description: Storage capacity to request persistent volume claims + (default=”10Gi” for etc and "100Gi" for var) + type: string + storageClassName: + description: Name of StorageClass to use for persistent volume claims + type: string + type: object volumes: description: List of one or more Kubernetes volumes. These will be mounted in all pod containers as as /mnt/ diff --git a/deploy/olm-catalog/splunk/0.2.1/enterprise.splunk.com_standalones_crd.yaml b/deploy/olm-catalog/splunk/0.2.1/enterprise.splunk.com_standalones_crd.yaml index 044014cd5..0ec4085a6 100644 --- a/deploy/olm-catalog/splunk/0.2.1/enterprise.splunk.com_standalones_crd.yaml +++ b/deploy/olm-catalog/splunk/0.2.1/enterprise.splunk.com_standalones_crd.yaml @@ -674,14 +674,28 @@ spec: description: Full path or URL for one or more default.yml files, separated by commas type: string - ephemeralStorage: - description: If true, ephemeral (emptyDir) storage will be used for - /opt/splunk/etc and /opt/splunk/var volumes - type: boolean - etcStorage: - description: Storage capacity to request for /opt/splunk/etc persistent - volume claims (default=”1Gi”) + defaultsUrlApps: + description: Full path or URL for one or more default.yml files specific + to App install, separated by commas This is meant as a temporary fix + until the SHC Deployer has its own CRD. The defaults listed here + will only be installed on a standalone, or a SHC Deployer or IDC CM + to be pushed to SH/IDXs in a cluster. This parameter is ignored on + individual SHs or IDXs within a cluster type: string + etcVolumeStorageConfig: + description: Storage configuration for /opt/splunk/etc volume + properties: + ephemeralStorage: + description: If true, ephemeral (emptyDir) storage will be used + type: boolean + storageCapacity: + description: Storage capacity to request persistent volume claims + (default=”10Gi” for etc and "100Gi" for var) + type: string + storageClassName: + description: Name of StorageClass to use for persistent volume claims + type: string + type: object image: description: Image to use for Splunk pod containers (overrides RELATED_IMAGE_SPLUNK_ENTERPRISE environment variables) @@ -1142,9 +1156,6 @@ spec: description: 'UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids' type: string type: object - storageClassName: - description: Name of StorageClass to use for persistent volume claims - type: string tolerations: description: Pod's tolerations for Kubernetes node's taint items: @@ -1185,10 +1196,20 @@ spec: type: string type: object type: array - varStorage: - description: Storage capacity to request for /opt/splunk/var persistent - volume claims (default=”50Gi”) - type: string + varVolumeStorageConfig: + description: Storage configuration for /opt/splunk/var volume + properties: + ephemeralStorage: + description: If true, ephemeral (emptyDir) storage will be used + type: boolean + storageCapacity: + description: Storage capacity to request persistent volume claims + (default=”10Gi” for etc and "100Gi" for var) + type: string + storageClassName: + description: Name of StorageClass to use for persistent volume claims + type: string + type: object volumes: description: List of one or more Kubernetes volumes. These will be mounted in all pod containers as as /mnt/ diff --git a/docs/StorageClass.md b/docs/StorageClass.md index f337e4b84..b265893ef 100644 --- a/docs/StorageClass.md +++ b/docs/StorageClass.md @@ -11,10 +11,28 @@ deployments. For each pod, the following two volumes will be mounted: | `/opt/splunk/var` | This is used to store all indexed events, logs, and other data | By default, 10GiB volumes will be created for `/opt/splunk/etc` and 100GiB -volumes will be created for `/opt/splunk/var`. You can customize this for -each resource by using the `etcStorage` and `varStorage` parameters. +volumes will be created for `/opt/splunk/var`. -The `kubectl` command can be used to see which Storage Classes are available in +You can customize the `storage capacity` and `storage class names` for the `/opt/splunk/etc` +and `/opt/splunk/var` volumes by using the `storageCapacity` and `storageClassName` fields +under the `etcVolumeStorageConfig` and `varVolumeStorageConfig` spec as follows: + +```yaml +apiVersion: enterprise.splunk.com/v1beta1 +kind: Standalone +metadata: + name: example + finalizers: + - enterprise.splunk.com/delete-pvc +spec: + etcVolumeStorageConfig: + storageClassName: gp2 + storageCapacity: 15Gi + varVolumeStorageConfig: + storageClassName: gp2 + storageCapacity: 25Gi +``` +The following `kubectl` command can be used to see which Storage Classes are available in your Kubernetes cluster: ``` @@ -23,8 +41,15 @@ NAME PROVISIONER AGE gp2 (default) kubernetes.io/aws-ebs 176d ``` -You can use the `storageClassName` parameter to specify the Storage Class you -would like to use: +If no `storageClassName` is provided, the default Storage Class for your +Kubernetes cluster will be used. + + +## Ephemeral Storage + +For testing and demonstration purposes, you may bypass the use of persistent +storage by using the `ephemeralStorage` field under the `etcVolumeStorageConfig` +and `varVolumeStorageConfig` spec as follows: ```yaml apiVersion: enterprise.splunk.com/v1beta1 @@ -34,20 +59,13 @@ metadata: finalizers: - enterprise.splunk.com/delete-pvc spec: - storageClassName: "gp2" - etcStorage: "25Gi" - varStorage: "100Gi" + etcVolumeStorageConfig: + ephemeralStorage: true + varVolumeStorageConfig: + ephemeralStorage: true ``` -If no `storageClassName` is provided, the default Storage Class for your -Kubernetes cluster will be used. - - -## Ephemeral Storage - -For testing and demonstration purposes, you may bypass the use of persistent -storage by setting `ephemeralStorage: true` within any Splunk custom resource -spec. This will mount local, ephemeral volumes for `/opt/splunk/etc` and +This will mount local, ephemeral volumes for `/opt/splunk/etc` and `/opt/splunk/var` using the Kubernetes [emptyDir](https://kubernetes.io/docs/concepts/storage/volumes/#emptydir) feature. diff --git a/go.mod b/go.mod index 4f9c99e36..d34e93ca0 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,7 @@ require ( k8s.io/apimachinery v0.0.0 k8s.io/client-go v12.0.0+incompatible k8s.io/kubectl v0.0.0 + k8s.io/kubernetes v1.16.2 sigs.k8s.io/controller-runtime v0.4.0 ) diff --git a/go.sum b/go.sum index 4d15338c5..acdc47633 100644 --- a/go.sum +++ b/go.sum @@ -29,6 +29,7 @@ github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q github.com/GoogleCloudPlatform/k8s-cloud-provider v0.0.0-20190822182118-27a4ced34534/go.mod h1:iroGtC8B3tQiqtds1l+mgk/BBOrxbqjH+eUfFQYRc14= github.com/JeffAshton/win_pdh v0.0.0-20161109143554-76bb4ee9f0ab/go.mod h1:3VYc5hodBMJ5+l/7J4xAyMeuM2PNuepvHlGs8yilUCA= github.com/MakeNowJust/heredoc v0.0.0-20170808103936-bb23615498cd/go.mod h1:64YHyfSL2R96J44Nlwm39UHepQbyR5q10x7iYa1ks2E= +github.com/MakeNowJust/heredoc v0.0.0-20171113091838-e9091a26100e h1:eb0Pzkt15Bm7f2FFYv7sjY7NPFi3cPkS3tv1CcrFBWA= github.com/MakeNowJust/heredoc v0.0.0-20171113091838-e9091a26100e/go.mod h1:64YHyfSL2R96J44Nlwm39UHepQbyR5q10x7iYa1ks2E= github.com/Masterminds/goutils v1.1.0/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= @@ -93,6 +94,7 @@ github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.0 h1:yTUvW7Vhb89inJ+8irsUqiWjh8iT6sQPZiQzI6ReGkA= github.com/cespare/xxhash/v2 v2.1.0/go.mod h1:dgIUBU3pDso/gPgZ1osOZ0iQf77oPR28Tjxl5dIMyVM= +github.com/chai2010/gettext-go v0.0.0-20160711120539-c6fed771bfd5 h1:7aWHqerlJ41y6FOsEUvknqgXnGmJyJSbjhAWq5pO4F8= github.com/chai2010/gettext-go v0.0.0-20160711120539-c6fed771bfd5/go.mod h1:/iP1qXHoty45bqomnu2LM+VVyAEdWN+vtSHGlQgyxbw= github.com/checkpoint-restore/go-criu v0.0.0-20190109184317-bdb7599cd87b/go.mod h1:TrMrLQfeENAPYPRsJuq3jsqdlRh3lvi6trTZJG8+tho= github.com/cheekybits/genny v0.0.0-20170328200008-9127e812e1e9/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ= @@ -185,7 +187,9 @@ github.com/evanphx/json-patch v4.1.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLi github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch v4.5.0+incompatible h1:ouOWdg56aJriqS0huScTkVXPC5IcNrDCXZ6OoTAWu7M= github.com/evanphx/json-patch v4.5.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d h1:105gxyaGwCFad8crR9dcMQWvV9Hvulu6hwUh4tWPJnM= github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d/go.mod h1:ZZMPRZwes7CROmyNKgQzC3XPs6L/G2EJLHddWejkmf4= +github.com/fatih/camelcase v1.0.0 h1:hxNvNX/xYBp0ovncs8WyWZrOrpBNub/JfaMvbURyft8= github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc= github.com/fatih/color v1.6.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= @@ -315,6 +319,7 @@ github.com/golangplus/bytes v0.0.0-20160111154220-45c989fe5450/go.mod h1:Bk6SMAO github.com/golangplus/fmt v0.0.0-20150411045040-2a5d6d7d2995/go.mod h1:lJgMEyOkYFkPcDKwRXegd+iM6E7matEszMG5HhwytU8= github.com/golangplus/testing v0.0.0-20180327235837-af21d9c3145e/go.mod h1:0AA//k/eakGydO4jKRoRL2j92ZKSzTgj9tclaCrvXHk= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/cadvisor v0.34.0/go.mod h1:1nql6U13uTHaLYB8rLS5x9IJc2qT6Xd/Tr1sTX6NE48= github.com/google/certificate-transparency-go v1.0.21/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg= @@ -355,6 +360,7 @@ github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoA github.com/gosuri/uitable v0.0.1/go.mod h1:tKR86bXuXPZazfOTG1FIzvjIdXzd0mo4Vtn16vt0PJo= github.com/gregjones/httpcache v0.0.0-20170728041850-787624de3eb7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/gregjones/httpcache v0.0.0-20181110185634-c63ab54fda8f/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/gregjones/httpcache v0.0.0-20190203031600-7a902570cb17 h1:prg2TTpTOcJF1jRWL2zSU1FQNgB0STAFNux8GK82y8k= github.com/gregjones/httpcache v0.0.0-20190203031600-7a902570cb17/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/grpc-ecosystem/go-grpc-middleware v0.0.0-20190222133341-cfaf5686ec79/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= @@ -434,6 +440,7 @@ github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/libopenstorage/openstorage v1.0.0/go.mod h1:Sp1sIObHjat1BeXhfMqLZ14wnOzEhNx2YQedreMcUyc= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= +github.com/lithammer/dedent v1.1.0 h1:VNzHMVCBNG1j0fh3OrsFRkVUwStdDArbgBWoPAffktY= github.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z9BP0jIOc= github.com/lpabon/godbc v0.1.1/go.mod h1:Jo9QV0cf3U6jZABgiJ2skINAXb9j8m51r07g4KI92ZA= github.com/lucas-clemente/aes12 v0.0.0-20171027163421-cd47fb39b79f/go.mod h1:JpH9J1c9oX6otFSgdUHwUBUizmKlrMjxWnIAjff4m04= @@ -480,11 +487,13 @@ github.com/mistifyio/go-zfs v2.1.1+incompatible/go.mod h1:8AuVvqP/mXw1px98n46wfv github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-wordwrap v1.0.0 h1:6GlHJ/LTGMrIJbwgdqdl2eEH8o+Exx/0m8ir9Gns0u4= github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= github.com/mitchellh/hashstructure v0.0.0-20170609045927-2bca23e0e452/go.mod h1:QjSHrPWS+BGUVBYkbTZWEnOh3G1DutKwClXU/ABz6AQ= github.com/mitchellh/hashstructure v1.0.0/go.mod h1:QjSHrPWS+BGUVBYkbTZWEnOh3G1DutKwClXU/ABz6AQ= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/moby/moby v0.7.3-0.20190826074503-38ab9da00309 h1:cvy4lBOYN3gKfKj8Lzz5Q9TfviP+L7koMHY7SvkyTKs= github.com/moby/moby v0.7.3-0.20190826074503-38ab9da00309/go.mod h1:fDXVQ6+S340veQPv35CzDahGBmHsiclFwfEygB/TWMc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= @@ -555,6 +564,7 @@ github.com/pborman/uuid v1.2.0 h1:J7Q5mO4ysT1dv8hyrUGHb9+ooztCXu1D8MY8DZYsu3g= github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= github.com/pelletier/go-toml v1.0.1/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= github.com/phayes/freeport v0.0.0-20171002181615-b8543db493a5/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE= github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= @@ -935,6 +945,7 @@ k8s.io/apimachinery v0.0.0-20191004115801-a2eda9f80ab8 h1:Iieh/ZEgT3BWwbLD5qEKcY k8s.io/apimachinery v0.0.0-20191004115801-a2eda9f80ab8/go.mod h1:llRdnznGEAqC3DcNm6yEj472xaFVfLM7hnYofMb12tQ= k8s.io/apiserver v0.0.0-20191016112112-5190913f932d/go.mod h1:7OqfAolfWxUM/jJ/HBLyE+cdaWFBUoo5Q5pHgJVj2ws= k8s.io/autoscaler v0.0.0-20190607113959-1b4f1855cb8e/go.mod h1:QEXezc9uKPT91dwqhSJq3GNI3B1HxFRQHiku9kmrsSA= +k8s.io/cli-runtime v0.0.0-20191016114015-74ad18325ed5 h1:8ZfMjkMBzcXEawLsYHg9lDM7aLEVso3NiVKfUTnN56A= k8s.io/cli-runtime v0.0.0-20191016114015-74ad18325ed5/go.mod h1:sDl6WKSQkDM6zS1u9F49a0VooQ3ycYFBFLqd2jf2Xfo= k8s.io/client-go v0.0.0-20191016111102-bec269661e48 h1:C2XVy2z0dV94q9hSSoCuTPp1KOG7IegvbdXuz9VGxoU= k8s.io/client-go v0.0.0-20191016111102-bec269661e48/go.mod h1:hrwktSwYGI4JK+TJA3dMaFyyvHVi/aLarVHpbs8bgCU= @@ -969,6 +980,7 @@ k8s.io/kubectl v0.0.0-20191016120415-2ed914427d51 h1:RBkTKVMF+xsNsSOVc0+HdC0B5gD k8s.io/kubectl v0.0.0-20191016120415-2ed914427d51/go.mod h1:gL826ZTIfD4vXTGlmzgTbliCAT9NGiqpCqK2aNYv5MQ= k8s.io/kubelet v0.0.0-20191016114556-7841ed97f1b2/go.mod h1:SBvrtLbuePbJygVXGGCMtWKH07+qrN2dE1iMnteSG8E= k8s.io/kubernetes v1.16.0/go.mod h1:nlP2zevWKRGKuaaVbKIwozU0Rjg9leVDXkL4YTtjmVs= +k8s.io/kubernetes v1.16.2 h1:k0f/OVp6Yfv+UMTm6VYKhqjRgcvHh4QhN9coanjrito= k8s.io/kubernetes v1.16.2/go.mod h1:SmhGgKfQ30imqjFVj8AI+iW+zSyFsswNErKYeTfgoH0= k8s.io/legacy-cloud-providers v0.0.0-20191016115753-cf0698c3a16b/go.mod h1:tKW3pKqdRW8pMveUTpF5pJuCjQxg6a25iLo+Z9BXVH0= k8s.io/metrics v0.0.0-20191016113814-3b1a734dba6e/go.mod h1:ve7/vMWeY5lEBkZf6Bt5TTbGS3b8wAxwGbdXAsufjRs= @@ -987,6 +999,7 @@ rsc.io/letsencrypt v0.0.1/go.mod h1:buyQKZ6IXrRnB7TdkHP0RyEybLx18HHyOSoTyoOLqNY= sigs.k8s.io/controller-runtime v0.4.0 h1:wATM6/m+3w8lj8FXNaO6Fs/rq/vqoOjO1Q116Z9NPsg= sigs.k8s.io/controller-runtime v0.4.0/go.mod h1:ApC79lpY3PHW9xj/w9pj+lYkLgwAAUZwfXkME1Lajns= sigs.k8s.io/controller-tools v0.2.4/go.mod h1:m/ztfQNocGYBgTTCmFdnK94uVvgxeZeE3LtJvd/jIzA= +sigs.k8s.io/kustomize v2.0.3+incompatible h1:JUufWFNlI44MdtnjUqVnvh29rR37PQFzPbLXqhyOyX0= sigs.k8s.io/kustomize v2.0.3+incompatible/go.mod h1:MkjgH3RdOWrievjo6c9T245dYlB5QeXV4WCbnt/PEpU= sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI= sigs.k8s.io/structured-merge-diff v0.0.0-20190817042607-6149e4549fca/go.mod h1:IIgPezJWb76P0hotTxzDbWsMYB8APh18qZnxkomBpxA= diff --git a/pkg/apis/enterprise/v1beta1/common_types.go b/pkg/apis/enterprise/v1beta1/common_types.go index d3f161eff..5c6cc5d80 100644 --- a/pkg/apis/enterprise/v1beta1/common_types.go +++ b/pkg/apis/enterprise/v1beta1/common_types.go @@ -37,17 +37,11 @@ const ( type CommonSplunkSpec struct { splcommon.Spec `json:",inline"` - // Name of StorageClass to use for persistent volume claims - StorageClassName string `json:"storageClassName"` - - // Storage capacity to request for /opt/splunk/etc persistent volume claims (default=”1Gi”) - EtcStorage string `json:"etcStorage"` + // Storage configuration for /opt/splunk/etc volume + EtcVolumeStorageConfig StorageClassSpec `json:"etcVolumeStorageConfig"` - // Storage capacity to request for /opt/splunk/var persistent volume claims (default=”50Gi”) - VarStorage string `json:"varStorage"` - - // If true, ephemeral (emptyDir) storage will be used for /opt/splunk/etc and /opt/splunk/var volumes - EphemeralStorage bool `json:"ephemeralStorage"` + // Storage configuration for /opt/splunk/var volume + VarVolumeStorageConfig StorageClassSpec `json:"varVolumeStorageConfig"` // List of one or more Kubernetes volumes. These will be mounted in all pod containers as as /mnt/ Volumes []corev1.Volume `json:"volumes"` @@ -77,6 +71,18 @@ type CommonSplunkSpec struct { Mock bool `json:"Mock"` } +// StorageClassSpec defines storage class configuration +type StorageClassSpec struct { + // Name of StorageClass to use for persistent volume claims + StorageClassName string `json:"storageClassName"` + + // Storage capacity to request persistent volume claims (default=”10Gi” for etc and "100Gi" for var) + StorageCapacity string `json:"storageCapacity"` + + // If true, ephemeral (emptyDir) storage will be used + EphemeralStorage bool `json:"ephemeralStorage"` +} + // SmartStoreSpec defines Splunk indexes and remote storage volume configuration type SmartStoreSpec struct { // List of remote storage volumes diff --git a/pkg/apis/enterprise/v1beta1/zz_generated.deepcopy.go b/pkg/apis/enterprise/v1beta1/zz_generated.deepcopy.go index 643175b41..c232ad43b 100644 --- a/pkg/apis/enterprise/v1beta1/zz_generated.deepcopy.go +++ b/pkg/apis/enterprise/v1beta1/zz_generated.deepcopy.go @@ -150,6 +150,8 @@ func (in *ClusterMasterStatus) DeepCopy() *ClusterMasterStatus { func (in *CommonSplunkSpec) DeepCopyInto(out *CommonSplunkSpec) { *out = *in in.Spec.DeepCopyInto(&out.Spec) + out.EtcVolumeStorageConfig = in.EtcVolumeStorageConfig + out.VarVolumeStorageConfig = in.VarVolumeStorageConfig if in.Volumes != nil { in, out := &in.Volumes, &out.Volumes *out = make([]v1.Volume, len(*in)) @@ -819,6 +821,22 @@ func (in *StandaloneStatus) DeepCopy() *StandaloneStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *StorageClassSpec) DeepCopyInto(out *StorageClassSpec) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StorageClassSpec. +func (in *StorageClassSpec) DeepCopy() *StorageClassSpec { + if in == nil { + return nil + } + out := new(StorageClassSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *VolumeSpec) DeepCopyInto(out *VolumeSpec) { *out = *in diff --git a/pkg/splunk/common/names.go b/pkg/splunk/common/names.go index 3865760bb..b5c789f7c 100644 --- a/pkg/splunk/common/names.go +++ b/pkg/splunk/common/names.go @@ -37,6 +37,27 @@ const ( // IdxcSecret represents indexer cluster pass4Symmkey secret token IdxcSecret = "idxc_secret" + + // PvcNamePrefix is a helper string representing prefix for persistent volume claim names + PvcNamePrefix = "pvc-%s" + + // SplunkMountNamePrefix is a helper string representing Splunk Volume mount names + SplunkMountNamePrefix = "mnt-splunk-%s" + + // SplunkMountDirecPrefix is a helper string representing Splunk Volume mount directory + SplunkMountDirecPrefix = "/opt/splunk/%s" + + // EtcVolumeStorage indicates /opt/splunk/etc volume mounted on Pods + EtcVolumeStorage = "etc" + + // VarVolumeStorage indicates /opt/splunk/etc volume mounted on Pods + VarVolumeStorage = "var" + + // DefaultEtcVolumeStorageCapacity represents default storage capacity for etc volume + DefaultEtcVolumeStorageCapacity = "10Gi" + + // DefaultVarVolumeStorageCapacity represents default storage capacity for var volume + DefaultVarVolumeStorageCapacity = "100Gi" ) // GetVersionedSecretName returns a versioned secret name diff --git a/pkg/splunk/enterprise/clustermaster_test.go b/pkg/splunk/enterprise/clustermaster_test.go index ca5bc7830..125d103fa 100644 --- a/pkg/splunk/enterprise/clustermaster_test.go +++ b/pkg/splunk/enterprise/clustermaster_test.go @@ -265,13 +265,13 @@ func TestApplyClusterMasterWithSmartstore(t *testing.T) { t.Errorf("ApplyClusterMaster() should not have returned error") } - current.Spec.CommonSplunkSpec.EtcStorage = "-abcd" + current.Spec.CommonSplunkSpec.EtcVolumeStorageConfig.StorageCapacity = "-abcd" if _, err := ApplyClusterMaster(client, ¤t); err == nil { t.Errorf("ApplyClusterMaster() should have returned error") } var replicas int32 = 3 - current.Spec.CommonSplunkSpec.EtcStorage = "" + current.Spec.CommonSplunkSpec.EtcVolumeStorageConfig.StorageCapacity = "" ss.Status.ReadyReplicas = 3 ss.Spec.Replicas = &replicas ss.Spec.Template.Spec.Containers[0].Image = "splunk/splunk" diff --git a/pkg/splunk/enterprise/configuration.go b/pkg/splunk/enterprise/configuration.go index f660c67e9..add524f4c 100644 --- a/pkg/splunk/enterprise/configuration.go +++ b/pkg/splunk/enterprise/configuration.go @@ -45,60 +45,53 @@ func getSplunkLabels(instanceIdentifier string, instanceType InstanceType, partO } // getSplunkVolumeClaims returns a standard collection of Kubernetes volume claims. -func getSplunkVolumeClaims(cr splcommon.MetaObject, spec *enterprisev1.CommonSplunkSpec, labels map[string]string) ([]corev1.PersistentVolumeClaim, error) { - var etcStorage, varStorage resource.Quantity +func getSplunkVolumeClaims(cr splcommon.MetaObject, spec *enterprisev1.CommonSplunkSpec, labels map[string]string, volumeType string) (corev1.PersistentVolumeClaim, error) { + var storageCapacity resource.Quantity var err error - etcStorage, err = splcommon.ParseResourceQuantity(spec.EtcStorage, "10Gi") - if err != nil { - return []corev1.PersistentVolumeClaim{}, fmt.Errorf("%s: %s", "etcStorage", err) - } + storageClassName := "" - varStorage, err = splcommon.ParseResourceQuantity(spec.VarStorage, "100Gi") - if err != nil { - return []corev1.PersistentVolumeClaim{}, fmt.Errorf("%s: %s", "varStorage", err) + // Depending on the volume type, determine storage capacity and storage class name(if configured) + if volumeType == splcommon.EtcVolumeStorage { + storageCapacity, err = splcommon.ParseResourceQuantity(spec.EtcVolumeStorageConfig.StorageCapacity, splcommon.DefaultEtcVolumeStorageCapacity) + if err != nil { + return corev1.PersistentVolumeClaim{}, fmt.Errorf("%s: %s", "etcStorage", err) + } + if spec.EtcVolumeStorageConfig.StorageClassName != "" { + storageClassName = spec.EtcVolumeStorageConfig.StorageClassName + } + } else if volumeType == splcommon.VarVolumeStorage { + storageCapacity, err = splcommon.ParseResourceQuantity(spec.VarVolumeStorageConfig.StorageCapacity, splcommon.DefaultVarVolumeStorageCapacity) + if err != nil { + return corev1.PersistentVolumeClaim{}, fmt.Errorf("%s: %s", "varStorage", err) + } + if spec.VarVolumeStorageConfig.StorageClassName != "" { + storageClassName = spec.VarVolumeStorageConfig.StorageClassName + } } - volumeClaims := []corev1.PersistentVolumeClaim{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "pvc-etc", - Namespace: cr.GetNamespace(), - Labels: labels, - }, - Spec: corev1.PersistentVolumeClaimSpec{ - AccessModes: []corev1.PersistentVolumeAccessMode{"ReadWriteOnce"}, - Resources: corev1.ResourceRequirements{ - Requests: corev1.ResourceList{ - corev1.ResourceStorage: etcStorage, - }, - }, - }, + // Create a persistent volume claim + volumeClaim := corev1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf(splcommon.PvcNamePrefix, volumeType), + Namespace: cr.GetNamespace(), + Labels: labels, }, - { - ObjectMeta: metav1.ObjectMeta{ - Name: "pvc-var", - Namespace: cr.GetNamespace(), - Labels: labels, - }, - Spec: corev1.PersistentVolumeClaimSpec{ - AccessModes: []corev1.PersistentVolumeAccessMode{"ReadWriteOnce"}, - Resources: corev1.ResourceRequirements{ - Requests: corev1.ResourceList{ - corev1.ResourceStorage: varStorage, - }, + Spec: corev1.PersistentVolumeClaimSpec{ + AccessModes: []corev1.PersistentVolumeAccessMode{"ReadWriteOnce"}, + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceStorage: storageCapacity, }, }, }, } - if spec.StorageClassName != "" { - for idx := range volumeClaims { - volumeClaims[idx].Spec.StorageClassName = &spec.StorageClassName - } + // Assign storage class name if specified + if storageClassName != "" { + volumeClaim.Spec.StorageClassName = &storageClassName } - - return volumeClaims, nil + return volumeClaim, nil } // getSplunkService returns a Kubernetes Service object for Splunk instances configured for a Splunk Enterprise resource. @@ -315,6 +308,76 @@ func addSplunkVolumeToTemplate(podTemplateSpec *corev1.PodTemplateSpec, name str } } +// addPVCVolumes adds pvc volumes to statefulSet +func addPVCVolumes(cr splcommon.MetaObject, spec *enterprisev1.CommonSplunkSpec, statefulSet *appsv1.StatefulSet, labels map[string]string, volumeType string) error { + // prepare and append persistent volume claims if storage is not ephemeral + var err error + volumeClaimTemplate, err := getSplunkVolumeClaims(cr, spec, labels, volumeType) + if err != nil { + return err + } + statefulSet.Spec.VolumeClaimTemplates = append(statefulSet.Spec.VolumeClaimTemplates, volumeClaimTemplate) + + // add volume mounts to splunk container for the PVCs + statefulSet.Spec.Template.Spec.Containers[0].VolumeMounts = append(statefulSet.Spec.Template.Spec.Containers[0].VolumeMounts, + corev1.VolumeMount{ + Name: volumeClaimTemplate.GetName(), + MountPath: fmt.Sprintf(splcommon.SplunkMountDirecPrefix, volumeType), + }) + + return nil +} + +// addEphermalVolumes adds ephermal volumes to statefulSet +func addEphermalVolumes(statefulSet *appsv1.StatefulSet, volumeType string) error { + // add ephemeral volumes to the splunk pod + emptyVolumeSource := corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + } + statefulSet.Spec.Template.Spec.Volumes = append(statefulSet.Spec.Template.Spec.Volumes, + corev1.Volume{ + Name: fmt.Sprintf(splcommon.SplunkMountNamePrefix, volumeType), VolumeSource: emptyVolumeSource, + }) + + // add volume mounts to splunk container for the ephemeral volumes + statefulSet.Spec.Template.Spec.Containers[0].VolumeMounts = append(statefulSet.Spec.Template.Spec.Containers[0].VolumeMounts, + corev1.VolumeMount{ + Name: fmt.Sprintf(splcommon.SplunkMountNamePrefix, volumeType), + MountPath: fmt.Sprintf(splcommon.SplunkMountDirecPrefix, volumeType), + }) + + return nil +} + +// addStorageVolumes adds storage volumes to the StatefulSet +func addStorageVolumes(cr splcommon.MetaObject, spec *enterprisev1.CommonSplunkSpec, statefulSet *appsv1.StatefulSet, labels map[string]string) error { + // configure storage for mount path /opt/splunk/etc + if spec.EtcVolumeStorageConfig.EphemeralStorage { + // add Ephermal volumes + _ = addEphermalVolumes(statefulSet, splcommon.EtcVolumeStorage) + } else { + // add PVC volumes + err := addPVCVolumes(cr, spec, statefulSet, labels, splcommon.EtcVolumeStorage) + if err != nil { + return err + } + } + + // configure storage for mount path /opt/splunk/var + if spec.VarVolumeStorageConfig.EphemeralStorage { + // add Ephermal volumes + _ = addEphermalVolumes(statefulSet, splcommon.VarVolumeStorage) + } else { + // add PVC volumes + err := addPVCVolumes(cr, spec, statefulSet, labels, splcommon.VarVolumeStorage) + if err != nil { + return err + } + } + + return nil +} + // getSplunkStatefulSet returns a Kubernetes StatefulSet object for Splunk instances configured for a Splunk Enterprise resource. func getSplunkStatefulSet(client splcommon.ControllerClient, cr splcommon.MetaObject, spec *enterprisev1.CommonSplunkSpec, instanceType InstanceType, replicas int32, extraEnv []corev1.EnvVar) (*appsv1.StatefulSet, error) { @@ -372,48 +435,10 @@ func getSplunkStatefulSet(client splcommon.ControllerClient, cr splcommon.MetaOb }, } - // update template to include storage for etc and var volumes - if spec.EphemeralStorage { - // add ephemeral volumes to the splunk pod for etc and opt - emptyVolumeSource := corev1.VolumeSource{ - EmptyDir: &corev1.EmptyDirVolumeSource{}, - } - statefulSet.Spec.Template.Spec.Volumes = []corev1.Volume{ - {Name: "mnt-splunk-etc", VolumeSource: emptyVolumeSource}, - {Name: "mnt-splunk-var", VolumeSource: emptyVolumeSource}, - } - - // add volume mounts to splunk container for the ephemeral volumes - statefulSet.Spec.Template.Spec.Containers[0].VolumeMounts = []corev1.VolumeMount{ - { - Name: "mnt-splunk-etc", - MountPath: "/opt/splunk/etc", - }, - { - Name: "mnt-splunk-var", - MountPath: "/opt/splunk/var", - }, - } - - } else { - // prepare and append persistent volume claims if storage is not ephemeral - var err error - statefulSet.Spec.VolumeClaimTemplates, err = getSplunkVolumeClaims(cr, spec, labels) - if err != nil { - return nil, err - } - - // add volume mounts to splunk container for the PVCs - statefulSet.Spec.Template.Spec.Containers[0].VolumeMounts = []corev1.VolumeMount{ - { - Name: "pvc-etc", - MountPath: "/opt/splunk/etc", - }, - { - Name: "pvc-var", - MountPath: "/opt/splunk/var", - }, - } + // Add storage volumes + err := addStorageVolumes(cr, spec, statefulSet, labels) + if err != nil { + return statefulSet, err } // append labels and annotations from parent diff --git a/pkg/splunk/enterprise/configuration_test.go b/pkg/splunk/enterprise/configuration_test.go index ec37dfb13..26cd825d6 100644 --- a/pkg/splunk/enterprise/configuration_test.go +++ b/pkg/splunk/enterprise/configuration_test.go @@ -24,6 +24,7 @@ import ( splctrl "github.com/splunk/splunk-operator/pkg/splunk/controller" spltest "github.com/splunk/splunk-operator/pkg/splunk/test" splutil "github.com/splunk/splunk-operator/pkg/splunk/util" + appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/intstr" @@ -845,3 +846,71 @@ func TestAreRemoteVolumeKeysChanged(t *testing.T) { t.Errorf("Empty volume should not report a key change") } } + +func TestAddStorageVolumes(t *testing.T) { + labels := make(map[string]string) + var replicas int32 = 1 + + // Create CR + cr := enterprisev1.ClusterMaster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "CM", + Namespace: "test", + }, + } + + // create statefulset configuration + statefulSet := &appsv1.StatefulSet{ + TypeMeta: metav1.TypeMeta{ + Kind: "StatefulSet", + APIVersion: "apps/v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "test-statefulset", + Namespace: cr.GetNamespace(), + }, + Spec: appsv1.StatefulSetSpec{ + Replicas: &replicas, + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Image: "test", + Name: "splunk", + }, + }, + }, + }, + }, + } + + // Define valid common spec + spec := &enterprisev1.CommonSplunkSpec{} + err := addStorageVolumes(&cr, spec, statefulSet, labels) + if err != nil { + t.Errorf("Unable to add storage volumes, error: %s", err.Error()) + } + + // Define invalid EtcVolumeStorageConfig + spec = &enterprisev1.CommonSplunkSpec{ + EtcVolumeStorageConfig: enterprisev1.StorageClassSpec{ + StorageCapacity: "----", + }, + } + err = addStorageVolumes(&cr, spec, statefulSet, labels) + if err == nil { + t.Errorf("Unable to idenitfy incorrect EtcVolumeStorageConfig resource quantity") + } + + // Define invalid VarVolumeStorageConfig + spec = &enterprisev1.CommonSplunkSpec{ + VarVolumeStorageConfig: enterprisev1.StorageClassSpec{ + StorageCapacity: "----", + }, + } + err = addStorageVolumes(&cr, spec, statefulSet, labels) + if err == nil { + t.Errorf("Unable to idenitfy incorrect VarVolumeStorageConfig resource quantity") + } + +} diff --git a/pkg/splunk/enterprise/indexercluster_test.go b/pkg/splunk/enterprise/indexercluster_test.go index bfd9e681c..1ff743f4f 100644 --- a/pkg/splunk/enterprise/indexercluster_test.go +++ b/pkg/splunk/enterprise/indexercluster_test.go @@ -990,7 +990,7 @@ func TestInvalidIndexerClusterSpec(t *testing.T) { } cm.Status.Phase = splcommon.PhaseError - cr.Spec.CommonSplunkSpec.EtcStorage = "-abcd" + cr.Spec.CommonSplunkSpec.EtcVolumeStorageConfig.StorageCapacity = "-abcd" if _, err := ApplyIndexerCluster(c, &cr); err == nil { t.Errorf("ApplyIndxerCluster() should have returned error") } diff --git a/pkg/splunk/enterprise/standalone_test.go b/pkg/splunk/enterprise/standalone_test.go index ba2a4e57d..03bab24fc 100644 --- a/pkg/splunk/enterprise/standalone_test.go +++ b/pkg/splunk/enterprise/standalone_test.go @@ -200,15 +200,18 @@ func TestGetStandaloneStatefulSet(t *testing.T) { test(`{"kind":"StatefulSet","apiVersion":"apps/v1","metadata":{"name":"splunk-stack1-standalone","namespace":"test","creationTimestamp":null,"ownerReferences":[{"apiVersion":"","kind":"","name":"stack1","uid":"","controller":true}]},"spec":{"replicas":1,"selector":{"matchLabels":{"app.kubernetes.io/component":"standalone","app.kubernetes.io/instance":"splunk-stack1-standalone","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"standalone","app.kubernetes.io/part-of":"splunk-stack1-standalone"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app.kubernetes.io/component":"standalone","app.kubernetes.io/instance":"splunk-stack1-standalone","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"standalone","app.kubernetes.io/part-of":"splunk-stack1-standalone"},"annotations":{"traffic.sidecar.istio.io/excludeOutboundPorts":"8089,8191,9997,7777,9000,17000,17500,19000","traffic.sidecar.istio.io/includeInboundPorts":"8000,8088"}},"spec":{"volumes":[{"name":"mnt-splunk-secrets","secret":{"secretName":"splunk-stack1-standalone-secret-v1","defaultMode":420}}],"containers":[{"name":"splunk","image":"splunk/splunk","ports":[{"name":"splunkweb","containerPort":8000,"protocol":"TCP"},{"name":"hec","containerPort":8088,"protocol":"TCP"},{"name":"splunkd","containerPort":8089,"protocol":"TCP"},{"name":"dfsmaster","containerPort":9000,"protocol":"TCP"},{"name":"s2s","containerPort":9997,"protocol":"TCP"},{"name":"dfccontrol","containerPort":17000,"protocol":"TCP"},{"name":"datareceive","containerPort":19000,"protocol":"TCP"}],"env":[{"name":"SPLUNK_HOME","value":"/opt/splunk"},{"name":"SPLUNK_START_ARGS","value":"--accept-license"},{"name":"SPLUNK_DEFAULTS_URL","value":"/mnt/splunk-secrets/default.yml"},{"name":"SPLUNK_HOME_OWNERSHIP_ENFORCEMENT","value":"false"},{"name":"SPLUNK_ROLE","value":"splunk_standalone"},{"name":"SPLUNK_DECLARATIVE_ADMIN_PASSWORD","value":"true"}],"resources":{"limits":{"cpu":"4","memory":"8Gi"},"requests":{"cpu":"100m","memory":"512Mi"}},"volumeMounts":[{"name":"pvc-etc","mountPath":"/opt/splunk/etc"},{"name":"pvc-var","mountPath":"/opt/splunk/var"},{"name":"mnt-splunk-secrets","mountPath":"/mnt/splunk-secrets"}],"livenessProbe":{"exec":{"command":["/sbin/checkstate.sh"]},"initialDelaySeconds":300,"timeoutSeconds":30,"periodSeconds":30},"readinessProbe":{"exec":{"command":["/bin/grep","started","/opt/container_artifact/splunk-container.state"]},"initialDelaySeconds":10,"timeoutSeconds":5,"periodSeconds":5},"imagePullPolicy":"IfNotPresent"}],"securityContext":{"runAsUser":41812,"fsGroup":41812},"affinity":{"podAntiAffinity":{"preferredDuringSchedulingIgnoredDuringExecution":[{"weight":100,"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app.kubernetes.io/instance","operator":"In","values":["splunk-stack1-standalone"]}]},"topologyKey":"kubernetes.io/hostname"}}]}},"schedulerName":"default-scheduler"}},"volumeClaimTemplates":[{"metadata":{"name":"pvc-etc","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"standalone","app.kubernetes.io/instance":"splunk-stack1-standalone","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"standalone","app.kubernetes.io/part-of":"splunk-stack1-standalone"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"10Gi"}}},"status":{}},{"metadata":{"name":"pvc-var","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"standalone","app.kubernetes.io/instance":"splunk-stack1-standalone","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"standalone","app.kubernetes.io/part-of":"splunk-stack1-standalone"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"100Gi"}}},"status":{}}],"serviceName":"splunk-stack1-standalone-headless","podManagementPolicy":"Parallel","updateStrategy":{"type":"OnDelete"}},"status":{"replicas":0}}`) - cr.Spec.EphemeralStorage = true + cr.Spec.EtcVolumeStorageConfig.EphemeralStorage = true + cr.Spec.VarVolumeStorageConfig.EphemeralStorage = true test(`{"kind":"StatefulSet","apiVersion":"apps/v1","metadata":{"name":"splunk-stack1-standalone","namespace":"test","creationTimestamp":null,"ownerReferences":[{"apiVersion":"","kind":"","name":"stack1","uid":"","controller":true}]},"spec":{"replicas":1,"selector":{"matchLabels":{"app.kubernetes.io/component":"standalone","app.kubernetes.io/instance":"splunk-stack1-standalone","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"standalone","app.kubernetes.io/part-of":"splunk-stack1-standalone"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app.kubernetes.io/component":"standalone","app.kubernetes.io/instance":"splunk-stack1-standalone","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"standalone","app.kubernetes.io/part-of":"splunk-stack1-standalone"},"annotations":{"traffic.sidecar.istio.io/excludeOutboundPorts":"8089,8191,9997,7777,9000,17000,17500,19000","traffic.sidecar.istio.io/includeInboundPorts":"8000,8088"}},"spec":{"volumes":[{"name":"mnt-splunk-etc","emptyDir":{}},{"name":"mnt-splunk-var","emptyDir":{}},{"name":"mnt-splunk-secrets","secret":{"secretName":"splunk-stack1-standalone-secret-v1","defaultMode":420}}],"containers":[{"name":"splunk","image":"splunk/splunk","ports":[{"name":"splunkweb","containerPort":8000,"protocol":"TCP"},{"name":"hec","containerPort":8088,"protocol":"TCP"},{"name":"splunkd","containerPort":8089,"protocol":"TCP"},{"name":"dfsmaster","containerPort":9000,"protocol":"TCP"},{"name":"s2s","containerPort":9997,"protocol":"TCP"},{"name":"dfccontrol","containerPort":17000,"protocol":"TCP"},{"name":"datareceive","containerPort":19000,"protocol":"TCP"}],"env":[{"name":"SPLUNK_HOME","value":"/opt/splunk"},{"name":"SPLUNK_START_ARGS","value":"--accept-license"},{"name":"SPLUNK_DEFAULTS_URL","value":"/mnt/splunk-secrets/default.yml"},{"name":"SPLUNK_HOME_OWNERSHIP_ENFORCEMENT","value":"false"},{"name":"SPLUNK_ROLE","value":"splunk_standalone"},{"name":"SPLUNK_DECLARATIVE_ADMIN_PASSWORD","value":"true"}],"resources":{"limits":{"cpu":"4","memory":"8Gi"},"requests":{"cpu":"100m","memory":"512Mi"}},"volumeMounts":[{"name":"mnt-splunk-etc","mountPath":"/opt/splunk/etc"},{"name":"mnt-splunk-var","mountPath":"/opt/splunk/var"},{"name":"mnt-splunk-secrets","mountPath":"/mnt/splunk-secrets"}],"livenessProbe":{"exec":{"command":["/sbin/checkstate.sh"]},"initialDelaySeconds":300,"timeoutSeconds":30,"periodSeconds":30},"readinessProbe":{"exec":{"command":["/bin/grep","started","/opt/container_artifact/splunk-container.state"]},"initialDelaySeconds":10,"timeoutSeconds":5,"periodSeconds":5},"imagePullPolicy":"IfNotPresent"}],"securityContext":{"runAsUser":41812,"fsGroup":41812},"affinity":{"podAntiAffinity":{"preferredDuringSchedulingIgnoredDuringExecution":[{"weight":100,"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app.kubernetes.io/instance","operator":"In","values":["splunk-stack1-standalone"]}]},"topologyKey":"kubernetes.io/hostname"}}]}},"schedulerName":"default-scheduler"}},"serviceName":"splunk-stack1-standalone-headless","podManagementPolicy":"Parallel","updateStrategy":{"type":"OnDelete"}},"status":{"replicas":0}}`) - cr.Spec.EphemeralStorage = false + cr.Spec.EtcVolumeStorageConfig.EphemeralStorage = false + cr.Spec.VarVolumeStorageConfig.EphemeralStorage = false cr.Spec.SparkRef.Name = cr.GetName() test(`{"kind":"StatefulSet","apiVersion":"apps/v1","metadata":{"name":"splunk-stack1-standalone","namespace":"test","creationTimestamp":null,"ownerReferences":[{"apiVersion":"","kind":"","name":"stack1","uid":"","controller":true}]},"spec":{"replicas":1,"selector":{"matchLabels":{"app.kubernetes.io/component":"standalone","app.kubernetes.io/instance":"splunk-stack1-standalone","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"standalone","app.kubernetes.io/part-of":"splunk-stack1-standalone"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app.kubernetes.io/component":"standalone","app.kubernetes.io/instance":"splunk-stack1-standalone","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"standalone","app.kubernetes.io/part-of":"splunk-stack1-standalone"},"annotations":{"traffic.sidecar.istio.io/excludeOutboundPorts":"8089,8191,9997,7777,9000,17000,17500,19000","traffic.sidecar.istio.io/includeInboundPorts":"8000,8088"}},"spec":{"volumes":[{"name":"mnt-splunk-secrets","secret":{"secretName":"splunk-stack1-standalone-secret-v1","defaultMode":420}},{"name":"mnt-splunk-jdk","emptyDir":{}},{"name":"mnt-splunk-spark","emptyDir":{}}],"initContainers":[{"name":"init","image":"splunk/spark","command":["bash","-c","cp -r /opt/jdk /mnt \u0026\u0026 cp -r /opt/spark /mnt"],"resources":{"limits":{"cpu":"1","memory":"512Mi"},"requests":{"cpu":"250m","memory":"128Mi"}},"volumeMounts":[{"name":"mnt-splunk-jdk","mountPath":"/mnt/jdk"},{"name":"mnt-splunk-spark","mountPath":"/mnt/spark"}],"imagePullPolicy":"IfNotPresent"}],"containers":[{"name":"splunk","image":"splunk/splunk","ports":[{"name":"splunkweb","containerPort":8000,"protocol":"TCP"},{"name":"hec","containerPort":8088,"protocol":"TCP"},{"name":"splunkd","containerPort":8089,"protocol":"TCP"},{"name":"dfsmaster","containerPort":9000,"protocol":"TCP"},{"name":"s2s","containerPort":9997,"protocol":"TCP"},{"name":"dfccontrol","containerPort":17000,"protocol":"TCP"},{"name":"datareceive","containerPort":19000,"protocol":"TCP"}],"env":[{"name":"SPLUNK_HOME","value":"/opt/splunk"},{"name":"SPLUNK_START_ARGS","value":"--accept-license"},{"name":"SPLUNK_DEFAULTS_URL","value":"/mnt/splunk-secrets/default.yml"},{"name":"SPLUNK_HOME_OWNERSHIP_ENFORCEMENT","value":"false"},{"name":"SPLUNK_ROLE","value":"splunk_standalone"},{"name":"SPLUNK_DECLARATIVE_ADMIN_PASSWORD","value":"true"},{"name":"SPLUNK_ENABLE_DFS","value":"true"},{"name":"SPARK_MASTER_HOST","value":"splunk-stack1-spark-master-service"},{"name":"SPARK_MASTER_WEBUI_PORT","value":"8009"},{"name":"SPARK_HOME","value":"/mnt/splunk-spark"},{"name":"JAVA_HOME","value":"/mnt/splunk-jdk"},{"name":"SPLUNK_DFW_NUM_SLOTS_ENABLED","value":"false"}],"resources":{"limits":{"cpu":"4","memory":"8Gi"},"requests":{"cpu":"100m","memory":"512Mi"}},"volumeMounts":[{"name":"pvc-etc","mountPath":"/opt/splunk/etc"},{"name":"pvc-var","mountPath":"/opt/splunk/var"},{"name":"mnt-splunk-secrets","mountPath":"/mnt/splunk-secrets"},{"name":"mnt-splunk-jdk","mountPath":"/mnt/splunk-jdk"},{"name":"mnt-splunk-spark","mountPath":"/mnt/splunk-spark"}],"livenessProbe":{"exec":{"command":["/sbin/checkstate.sh"]},"initialDelaySeconds":300,"timeoutSeconds":30,"periodSeconds":30},"readinessProbe":{"exec":{"command":["/bin/grep","started","/opt/container_artifact/splunk-container.state"]},"initialDelaySeconds":10,"timeoutSeconds":5,"periodSeconds":5},"imagePullPolicy":"IfNotPresent"}],"securityContext":{"runAsUser":41812,"fsGroup":41812},"affinity":{"podAntiAffinity":{"preferredDuringSchedulingIgnoredDuringExecution":[{"weight":100,"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app.kubernetes.io/instance","operator":"In","values":["splunk-stack1-standalone"]}]},"topologyKey":"kubernetes.io/hostname"}}]}},"schedulerName":"default-scheduler"}},"volumeClaimTemplates":[{"metadata":{"name":"pvc-etc","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"standalone","app.kubernetes.io/instance":"splunk-stack1-standalone","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"standalone","app.kubernetes.io/part-of":"splunk-stack1-standalone"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"10Gi"}}},"status":{}},{"metadata":{"name":"pvc-var","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"standalone","app.kubernetes.io/instance":"splunk-stack1-standalone","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"standalone","app.kubernetes.io/part-of":"splunk-stack1-standalone"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"100Gi"}}},"status":{}}],"serviceName":"splunk-stack1-standalone-headless","podManagementPolicy":"Parallel","updateStrategy":{"type":"OnDelete"}},"status":{"replicas":0}}`) cr.Spec.ClusterMasterRef.Name = "stack2" - cr.Spec.StorageClassName = "gp2" + cr.Spec.EtcVolumeStorageConfig.StorageClassName = "gp2" + cr.Spec.VarVolumeStorageConfig.StorageClassName = "gp2" cr.Spec.SchedulerName = "custom-scheduler" cr.Spec.Defaults = "defaults-string" cr.Spec.DefaultsURL = "/mnt/defaults/defaults.yml" From 159ef86ba307d0bbfa5965bb1c0c0992b6d3dea4 Mon Sep 17 00:00:00 2001 From: Param Dhanoya Date: Tue, 5 Jan 2021 14:46:13 -0800 Subject: [PATCH 12/43] Added option to pass test selection env variable through CIRCLE CI --- .circleci/config.yml | 86 ++++++++++++++++++- test/delete_cr/deletecr_test.go | 2 +- test/example/example_suite_test.go | 2 +- test/ingest_search/ingest_search_test.go | 4 +- .../monitoring_console_test.go | 8 +- test/run-tests.sh | 15 +++- test/smoke/smoke_test.go | 8 +- test/testenv/mcutil.go | 2 +- 8 files changed, 107 insertions(+), 20 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 66c86b3b9..d03f247a3 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -8,7 +8,14 @@ orbs: workflows: version: 2 - build-and-push: + nightly-integration-test: + triggers: + - schedule: + cron: "0 12 * * *" + filters: + branches: + only: + - develop jobs: - build-image - unit-tests @@ -16,6 +23,17 @@ workflows: requires: - build-image - unit-tests + - vulnerability-scan: + requires: + - build-image + build-and-push: + jobs: + - build-image + - unit-tests + - smoke-tests: + requires: + - build-image + - unit-tests - vulnerability-scan: requires: - build-image @@ -23,7 +41,7 @@ workflows: requires: - build-image - unit-tests - - integration-tests + - smoke-tests - vulnerability-scan filters: branches: @@ -33,7 +51,7 @@ workflows: requires: - build-image - unit-tests - - integration-tests + - smoke-tests - vulnerability-scan filters: branches: @@ -43,7 +61,7 @@ workflows: requires: - build-image - unit-tests - - integration-tests + - smoke-tests - vulnerability-scan filters: tags: @@ -154,6 +172,64 @@ jobs: - store_artifacts: name: Save coverage.out as artifact path: coverage.out + # Runs smoke tests against a k8s cluster + smoke-tests: + executor: ubuntu-machine + steps: + - run: + name: Setup Splunk operator and enterprise image env vars + command: | + echo 'export SPLUNK_OPERATOR_IMAGE=${IMAGE_NAME}:${CIRCLE_SHA1}' >> $BASH_ENV + echo 'export SPLUNK_ENTERPRISE_IMAGE=${ENTERPRISE_IMAGE_NAME}' >> $BASH_ENV + echo 'export COMMIT_HASH=$(echo ${CIRCLE_SHA1:0:7})' >> $BASH_ENV + - kubernetes/install + - aws-cli/install + - aws-eks/install-eksctl + - run: + name: Install kind tool + command: | + curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.7.0/kind-$(uname)-amd64 + chmod +x ./kind + sudo mv ./kind /usr/local/bin + - checkout + - attach_workspace: + name: Restore workspace + at: /tmp + # load the operator image to local registry in the VM + - load_image + - run: + name: Print out version and environment + command: | + ls -al + echo "GO VERSION=`go version`" + echo "CIRCLE_SHA1=$CIRCLE_SHA1" + echo "SPLUNK_OPERATOR_IMAGE=$SPLUNK_OPERATOR_IMAGE" + echo "SPLUNK_ENTEPRISE_IMAGE=$SPLUNK_ENTERPRISE_IMAGE" + echo "PRIVATE_REGISTRY=$PRIVATE_REGISTRY" + echo "CLUSTER_PROVIDER=$CLUSTER_PROVIDER" + - run: + # Deploys a eks or kind cluster depending of CLUSTER_PROVIDER flag. If cluster already exists, + # it will skip. Uses NUM_WORKERS for size of cluster + name: Deploy k8s cluster + command: | + make cluster-up + kubectl version # log the k8s version + no_output_timeout: 30m + - run: + # Run the smoke tests againsts the cluster deployed above. + # Test againsts the SPLUNK_OPERATOR_IMAGE and SPLUNK_ENTERPRISE_IMAGE + name: Run smoke tests + command: | + make int-test + mkdir -p /tmp/test-results + find ./test -name "*junit.xml" -exec cp {} /tmp/test-results \; + environment: + TEST_FOCUS: smoke + - store_test_results: + name: Save test results + path: /tmp/test-results + - store_artifacts: + path: /tmp/test-results # Runs integration tests against a k8s cluster integration-tests: executor: ubuntu-machine @@ -205,6 +281,8 @@ jobs: make int-test mkdir -p /tmp/test-results find ./test -name "*junit.xml" -exec cp {} /tmp/test-results \; + environment: + TEST_FOCUS: "smoke|ingest_search|monitoring_console|deletecr" - store_test_results: name: Save test results path: /tmp/test-results diff --git a/test/delete_cr/deletecr_test.go b/test/delete_cr/deletecr_test.go index 7a72192b6..b3fe25733 100644 --- a/test/delete_cr/deletecr_test.go +++ b/test/delete_cr/deletecr_test.go @@ -38,7 +38,7 @@ var _ = Describe("DeleteCR test", func() { }) Context("Multisite cluster deployment (M13 - Multisite indexer cluster, Search head cluster)", func() { - It("can deploy indexers and search head cluster", func() { + It("deletecr: can deploy indexers and search head cluster", func() { err := deployment.DeploySingleSiteCluster(deployment.GetName(), 3) Expect(err).To(Succeed(), "Unable to deploy cluster") diff --git a/test/example/example_suite_test.go b/test/example/example_suite_test.go index 3b4406bf1..1b52773d2 100644 --- a/test/example/example_suite_test.go +++ b/test/example/example_suite_test.go @@ -14,7 +14,7 @@ import ( var ( testenvInstance *testenv.TestEnv - testSuiteName = "example-suite-" + testenv.RandomDNSName(6) + testSuiteName = "example-" + testenv.RandomDNSName(3) ) func init() { diff --git a/test/ingest_search/ingest_search_test.go b/test/ingest_search/ingest_search_test.go index ae1492372..69994f9af 100644 --- a/test/ingest_search/ingest_search_test.go +++ b/test/ingest_search/ingest_search_test.go @@ -38,7 +38,7 @@ var _ = Describe("Ingest and Search Test", func() { }) Context("Standalone deployment (S1)", func() { - It("can search internal logs for standalone instance", func() { + It("ingest_search: can search internal logs for standalone instance", func() { standalone, err := deployment.DeployStandalone(deployment.GetName()) Expect(err).To(Succeed(), "Unable to deploy standalone instance ") @@ -114,7 +114,7 @@ var _ = Describe("Ingest and Search Test", func() { }) Context("Standalone deployment (S1)", func() { - It("can ingest custom data to new index and search", func() { + It("ingest_search: can ingest custom data to new index and search", func() { standalone, err := deployment.DeployStandalone(deployment.GetName()) Expect(err).To(Succeed(), "Unable to deploy standalone instance ") diff --git a/test/monitoring_console/monitoring_console_test.go b/test/monitoring_console/monitoring_console_test.go index a19b5884c..3b41aace9 100644 --- a/test/monitoring_console/monitoring_console_test.go +++ b/test/monitoring_console/monitoring_console_test.go @@ -37,7 +37,7 @@ var _ = Describe("Monitoring Console test", func() { }) Context("Standalone deployment (S1)", func() { - It("can deploy a MC with standalone instance and update MC with new standalone deployment", func() { + It("monitoring_console: can deploy a MC with standalone instance and update MC with new standalone deployment", func() { standaloneOneName := deployment.GetName() standaloneOne, err := deployment.DeployStandalone(standaloneOneName) @@ -98,7 +98,7 @@ var _ = Describe("Monitoring Console test", func() { }) Context("Standalone deployment with Scale up", func() { - It("can deploy a MC with standalone instance and update MC when standalone is scaled up", func() { + It("monitoring_console: can deploy a MC with standalone instance and update MC when standalone is scaled up", func() { standalone, err := deployment.DeployStandalone(deployment.GetName()) Expect(err).To(Succeed(), "Unable to deploy standalone instance ") @@ -168,7 +168,7 @@ var _ = Describe("Monitoring Console test", func() { }) Context("SearchHeadCluster deployment with Scale Up", func() { - It("MC can configure SHC instances after scale up in a namespace", func() { + It("monitoring_console: MC can configure SHC instances after scale up in a namespace", func() { _, err := deployment.DeploySearchHeadCluster(deployment.GetName(), "", "", "") Expect(err).To(Succeed(), "Unable to deploy search head cluster") @@ -232,7 +232,7 @@ var _ = Describe("Monitoring Console test", func() { }) Context("SearchHeadCluster and Standalone", func() { - It("MC can configure SHC and Standalone instances in a namespace", func() { + It("monitoring_console: MC can configure SHC and Standalone instances in a namespace", func() { _, err := deployment.DeploySearchHeadCluster(deployment.GetName(), "", "", "") Expect(err).To(Succeed(), "Unable to deploy search head cluster") diff --git a/test/run-tests.sh b/test/run-tests.sh index 6123fd8cc..77aeb1b02 100755 --- a/test/run-tests.sh +++ b/test/run-tests.sh @@ -76,6 +76,15 @@ fi echo "Running test using number of nodes: ${NUM_NODES}" echo "Running test using these images: ${PRIVATE_SPLUNK_OPERATOR_IMAGE} and ${PRIVATE_SPLUNK_ENTERPRISE_IMAGE}..." -# run Ginkgo -# Running only smoke test cases. To run different test packages add/remove path from skipPackage argument -ginkgo -v -progress -r -stream -nodes=${NUM_NODES} --skipPackage=example,ingest_search,monitoring_console,delete_cr ${topdir}/test -- -commit-hash=${COMMIT_HASH} -operator-image=${PRIVATE_SPLUNK_OPERATOR_IMAGE} -splunk-image=${PRIVATE_SPLUNK_ENTERPRISE_IMAGE} + +# Check if test foucs is set +if [[ -z "${TEST_FOCUS}" ]]; then + TEST_TO_RUN="smoke:" + echo "Test focus not set running smoke test by default :: ${TEST_TO_RUN}" +else + TEST_TO_RUN="${TEST_FOCUS}" + echo "Running following test :: ${TEST_TO_RUN}" +fi + +# Running only smoke test cases by default or value passed through TEST_FOCUS env variable. To run different test packages add/remove path from focus argument or TEST_FOCUS variable +ginkgo -v -progress -r -stream -nodes=${NUM_NODES} --focus="${TEST_TO_RUN}" ${topdir}/test -- -commit-hash=${COMMIT_HASH} -operator-image=${PRIVATE_SPLUNK_OPERATOR_IMAGE} -splunk-image=${PRIVATE_SPLUNK_ENTERPRISE_IMAGE} \ No newline at end of file diff --git a/test/smoke/smoke_test.go b/test/smoke/smoke_test.go index 2b0906997..48eaf4b40 100644 --- a/test/smoke/smoke_test.go +++ b/test/smoke/smoke_test.go @@ -38,7 +38,7 @@ var _ = Describe("Smoke test", func() { }) Context("Standalone deployment (S1)", func() { - It("can deploy a standalone instance", func() { + It("smoke: can deploy a standalone instance", func() { standalone, err := deployment.DeployStandalone(deployment.GetName()) Expect(err).To(Succeed(), "Unable to deploy standalone instance ") @@ -52,7 +52,7 @@ var _ = Describe("Smoke test", func() { }) Context("Clustered deployment (C3 - clustered indexer, search head cluster)", func() { - It("can deploy indexers and search head cluster", func() { + It("smoke: can deploy indexers and search head cluster", func() { idxCount := 3 err := deployment.DeploySingleSiteCluster(deployment.GetName(), idxCount) @@ -76,7 +76,7 @@ var _ = Describe("Smoke test", func() { }) Context("Multisite cluster deployment (M13 - Multisite indexer cluster, Search head cluster)", func() { - It("can deploy indexers and search head cluster", func() { + It("smoke: can deploy indexers and search head cluster", func() { siteCount := 3 err := deployment.DeployMultisiteClusterWithSearchHead(deployment.GetName(), 1, siteCount) @@ -103,7 +103,7 @@ var _ = Describe("Smoke test", func() { }) Context("Multisite cluster deployment (M1 - multisite indexer cluster)", func() { - It("can deploy multisite indexers cluster", func() { + It("smoke: can deploy multisite indexers cluster", func() { siteCount := 3 err := deployment.DeployMultisiteCluster(deployment.GetName(), 1, siteCount) diff --git a/test/testenv/mcutil.go b/test/testenv/mcutil.go index 2542314ca..572e0dc60 100644 --- a/test/testenv/mcutil.go +++ b/test/testenv/mcutil.go @@ -37,7 +37,7 @@ func getMCSts(ns string) string { mcSts := fmt.Sprintf(MonitoringConsoleSts, ns) output, err := exec.Command("kubectl", "get", "sts", "-n", ns, mcSts).Output() if err != nil { - cmd := fmt.Sprintf("kubectl get pods -n %s", ns) + cmd := fmt.Sprintf("kubectl get sts -n %s %s", ns, mcSts) logf.Log.Error(err, "Failed to execute command", "command", cmd) return "" } From b7abb5fbe4b7fada15297eba3dfac44317a761fe Mon Sep 17 00:00:00 2001 From: akondur Date: Fri, 8 Jan 2021 13:38:51 -0800 Subject: [PATCH 13/43] Addressing review comments --- .../enterprise.splunk.com_clustermasters_crd.yaml | 10 ++++------ ...enterprise.splunk.com_indexerclusters_crd.yaml | 10 ++++------ .../enterprise.splunk.com_licensemasters_crd.yaml | 10 ++++------ ...erprise.splunk.com_searchheadclusters_crd.yaml | 10 ++++------ .../enterprise.splunk.com_standalones_crd.yaml | 10 ++++------ .../enterprise.splunk.com_clustermasters_crd.yaml | 10 ++++------ ...enterprise.splunk.com_indexerclusters_crd.yaml | 10 ++++------ .../enterprise.splunk.com_licensemasters_crd.yaml | 10 ++++------ ...erprise.splunk.com_searchheadclusters_crd.yaml | 10 ++++------ .../enterprise.splunk.com_standalones_crd.yaml | 10 ++++------ docs/CustomResources.md | 15 ++++++++------- pkg/apis/enterprise/v1beta1/common_types.go | 8 ++++---- 12 files changed, 52 insertions(+), 71 deletions(-) diff --git a/deploy/crds/enterprise.splunk.com_clustermasters_crd.yaml b/deploy/crds/enterprise.splunk.com_clustermasters_crd.yaml index 64d70f3c2..939aa73c6 100644 --- a/deploy/crds/enterprise.splunk.com_clustermasters_crd.yaml +++ b/deploy/crds/enterprise.splunk.com_clustermasters_crd.yaml @@ -660,12 +660,10 @@ spec: by commas type: string defaultsUrlApps: - description: Full path or URL for one or more default.yml files specific - to App install, separated by commas This is meant as a temporary fix - until the SHC Deployer has its own CRD. The defaults listed here - will only be installed on a standalone, or a SHC Deployer or IDC CM - to be pushed to SH/IDXs in a cluster. This parameter is ignored on - individual SHs or IDXs within a cluster + description: Full path or URL for one or more defaults.yml files specific + to App install, separated by commas. The defaults listed here will + be installed on the CM, standalone, search head deployer or license + master instance. type: string etcVolumeStorageConfig: description: Storage configuration for /opt/splunk/etc volume diff --git a/deploy/crds/enterprise.splunk.com_indexerclusters_crd.yaml b/deploy/crds/enterprise.splunk.com_indexerclusters_crd.yaml index 9f350717e..615b1dc4e 100644 --- a/deploy/crds/enterprise.splunk.com_indexerclusters_crd.yaml +++ b/deploy/crds/enterprise.splunk.com_indexerclusters_crd.yaml @@ -682,12 +682,10 @@ spec: by commas type: string defaultsUrlApps: - description: Full path or URL for one or more default.yml files specific - to App install, separated by commas This is meant as a temporary fix - until the SHC Deployer has its own CRD. The defaults listed here - will only be installed on a standalone, or a SHC Deployer or IDC CM - to be pushed to SH/IDXs in a cluster. This parameter is ignored on - individual SHs or IDXs within a cluster + description: Full path or URL for one or more defaults.yml files specific + to App install, separated by commas. The defaults listed here will + be installed on the CM, standalone, search head deployer or license + master instance. type: string etcVolumeStorageConfig: description: Storage configuration for /opt/splunk/etc volume diff --git a/deploy/crds/enterprise.splunk.com_licensemasters_crd.yaml b/deploy/crds/enterprise.splunk.com_licensemasters_crd.yaml index 4101001f5..6e7c0e0f3 100644 --- a/deploy/crds/enterprise.splunk.com_licensemasters_crd.yaml +++ b/deploy/crds/enterprise.splunk.com_licensemasters_crd.yaml @@ -665,12 +665,10 @@ spec: by commas type: string defaultsUrlApps: - description: Full path or URL for one or more default.yml files specific - to App install, separated by commas This is meant as a temporary fix - until the SHC Deployer has its own CRD. The defaults listed here - will only be installed on a standalone, or a SHC Deployer or IDC CM - to be pushed to SH/IDXs in a cluster. This parameter is ignored on - individual SHs or IDXs within a cluster + description: Full path or URL for one or more defaults.yml files specific + to App install, separated by commas. The defaults listed here will + be installed on the CM, standalone, search head deployer or license + master instance. type: string etcVolumeStorageConfig: description: Storage configuration for /opt/splunk/etc volume diff --git a/deploy/crds/enterprise.splunk.com_searchheadclusters_crd.yaml b/deploy/crds/enterprise.splunk.com_searchheadclusters_crd.yaml index 3be124ef3..f99e2a661 100644 --- a/deploy/crds/enterprise.splunk.com_searchheadclusters_crd.yaml +++ b/deploy/crds/enterprise.splunk.com_searchheadclusters_crd.yaml @@ -682,12 +682,10 @@ spec: by commas type: string defaultsUrlApps: - description: Full path or URL for one or more default.yml files specific - to App install, separated by commas This is meant as a temporary fix - until the SHC Deployer has its own CRD. The defaults listed here - will only be installed on a standalone, or a SHC Deployer or IDC CM - to be pushed to SH/IDXs in a cluster. This parameter is ignored on - individual SHs or IDXs within a cluster + description: Full path or URL for one or more defaults.yml files specific + to App install, separated by commas. The defaults listed here will + be installed on the CM, standalone, search head deployer or license + master instance. type: string etcVolumeStorageConfig: description: Storage configuration for /opt/splunk/etc volume diff --git a/deploy/crds/enterprise.splunk.com_standalones_crd.yaml b/deploy/crds/enterprise.splunk.com_standalones_crd.yaml index 0ec4085a6..7b9e68614 100644 --- a/deploy/crds/enterprise.splunk.com_standalones_crd.yaml +++ b/deploy/crds/enterprise.splunk.com_standalones_crd.yaml @@ -675,12 +675,10 @@ spec: by commas type: string defaultsUrlApps: - description: Full path or URL for one or more default.yml files specific - to App install, separated by commas This is meant as a temporary fix - until the SHC Deployer has its own CRD. The defaults listed here - will only be installed on a standalone, or a SHC Deployer or IDC CM - to be pushed to SH/IDXs in a cluster. This parameter is ignored on - individual SHs or IDXs within a cluster + description: Full path or URL for one or more defaults.yml files specific + to App install, separated by commas. The defaults listed here will + be installed on the CM, standalone, search head deployer or license + master instance. type: string etcVolumeStorageConfig: description: Storage configuration for /opt/splunk/etc volume diff --git a/deploy/olm-catalog/splunk/0.2.1/enterprise.splunk.com_clustermasters_crd.yaml b/deploy/olm-catalog/splunk/0.2.1/enterprise.splunk.com_clustermasters_crd.yaml index 64d70f3c2..939aa73c6 100644 --- a/deploy/olm-catalog/splunk/0.2.1/enterprise.splunk.com_clustermasters_crd.yaml +++ b/deploy/olm-catalog/splunk/0.2.1/enterprise.splunk.com_clustermasters_crd.yaml @@ -660,12 +660,10 @@ spec: by commas type: string defaultsUrlApps: - description: Full path or URL for one or more default.yml files specific - to App install, separated by commas This is meant as a temporary fix - until the SHC Deployer has its own CRD. The defaults listed here - will only be installed on a standalone, or a SHC Deployer or IDC CM - to be pushed to SH/IDXs in a cluster. This parameter is ignored on - individual SHs or IDXs within a cluster + description: Full path or URL for one or more defaults.yml files specific + to App install, separated by commas. The defaults listed here will + be installed on the CM, standalone, search head deployer or license + master instance. type: string etcVolumeStorageConfig: description: Storage configuration for /opt/splunk/etc volume diff --git a/deploy/olm-catalog/splunk/0.2.1/enterprise.splunk.com_indexerclusters_crd.yaml b/deploy/olm-catalog/splunk/0.2.1/enterprise.splunk.com_indexerclusters_crd.yaml index 9f350717e..615b1dc4e 100644 --- a/deploy/olm-catalog/splunk/0.2.1/enterprise.splunk.com_indexerclusters_crd.yaml +++ b/deploy/olm-catalog/splunk/0.2.1/enterprise.splunk.com_indexerclusters_crd.yaml @@ -682,12 +682,10 @@ spec: by commas type: string defaultsUrlApps: - description: Full path or URL for one or more default.yml files specific - to App install, separated by commas This is meant as a temporary fix - until the SHC Deployer has its own CRD. The defaults listed here - will only be installed on a standalone, or a SHC Deployer or IDC CM - to be pushed to SH/IDXs in a cluster. This parameter is ignored on - individual SHs or IDXs within a cluster + description: Full path or URL for one or more defaults.yml files specific + to App install, separated by commas. The defaults listed here will + be installed on the CM, standalone, search head deployer or license + master instance. type: string etcVolumeStorageConfig: description: Storage configuration for /opt/splunk/etc volume diff --git a/deploy/olm-catalog/splunk/0.2.1/enterprise.splunk.com_licensemasters_crd.yaml b/deploy/olm-catalog/splunk/0.2.1/enterprise.splunk.com_licensemasters_crd.yaml index 4101001f5..6e7c0e0f3 100644 --- a/deploy/olm-catalog/splunk/0.2.1/enterprise.splunk.com_licensemasters_crd.yaml +++ b/deploy/olm-catalog/splunk/0.2.1/enterprise.splunk.com_licensemasters_crd.yaml @@ -665,12 +665,10 @@ spec: by commas type: string defaultsUrlApps: - description: Full path or URL for one or more default.yml files specific - to App install, separated by commas This is meant as a temporary fix - until the SHC Deployer has its own CRD. The defaults listed here - will only be installed on a standalone, or a SHC Deployer or IDC CM - to be pushed to SH/IDXs in a cluster. This parameter is ignored on - individual SHs or IDXs within a cluster + description: Full path or URL for one or more defaults.yml files specific + to App install, separated by commas. The defaults listed here will + be installed on the CM, standalone, search head deployer or license + master instance. type: string etcVolumeStorageConfig: description: Storage configuration for /opt/splunk/etc volume diff --git a/deploy/olm-catalog/splunk/0.2.1/enterprise.splunk.com_searchheadclusters_crd.yaml b/deploy/olm-catalog/splunk/0.2.1/enterprise.splunk.com_searchheadclusters_crd.yaml index 3be124ef3..f99e2a661 100644 --- a/deploy/olm-catalog/splunk/0.2.1/enterprise.splunk.com_searchheadclusters_crd.yaml +++ b/deploy/olm-catalog/splunk/0.2.1/enterprise.splunk.com_searchheadclusters_crd.yaml @@ -682,12 +682,10 @@ spec: by commas type: string defaultsUrlApps: - description: Full path or URL for one or more default.yml files specific - to App install, separated by commas This is meant as a temporary fix - until the SHC Deployer has its own CRD. The defaults listed here - will only be installed on a standalone, or a SHC Deployer or IDC CM - to be pushed to SH/IDXs in a cluster. This parameter is ignored on - individual SHs or IDXs within a cluster + description: Full path or URL for one or more defaults.yml files specific + to App install, separated by commas. The defaults listed here will + be installed on the CM, standalone, search head deployer or license + master instance. type: string etcVolumeStorageConfig: description: Storage configuration for /opt/splunk/etc volume diff --git a/deploy/olm-catalog/splunk/0.2.1/enterprise.splunk.com_standalones_crd.yaml b/deploy/olm-catalog/splunk/0.2.1/enterprise.splunk.com_standalones_crd.yaml index 0ec4085a6..7b9e68614 100644 --- a/deploy/olm-catalog/splunk/0.2.1/enterprise.splunk.com_standalones_crd.yaml +++ b/deploy/olm-catalog/splunk/0.2.1/enterprise.splunk.com_standalones_crd.yaml @@ -675,12 +675,10 @@ spec: by commas type: string defaultsUrlApps: - description: Full path or URL for one or more default.yml files specific - to App install, separated by commas This is meant as a temporary fix - until the SHC Deployer has its own CRD. The defaults listed here - will only be installed on a standalone, or a SHC Deployer or IDC CM - to be pushed to SH/IDXs in a cluster. This parameter is ignored on - individual SHs or IDXs within a cluster + description: Full path or URL for one or more defaults.yml files specific + to App install, separated by commas. The defaults listed here will + be installed on the CM, standalone, search head deployer or license + master instance. type: string etcVolumeStorageConfig: description: Storage configuration for /opt/splunk/etc volume diff --git a/docs/CustomResources.md b/docs/CustomResources.md index 3506e9741..2e30ea614 100644 --- a/docs/CustomResources.md +++ b/docs/CustomResources.md @@ -88,9 +88,12 @@ kind: Standalone metadata: name: example spec: - storageClassName: gp2 - etcStorage: "100Gi" - varStorage: "500Gi" + etcVolumeStorageConfig: + storageClassName: gp2 + storageCapacity: 15Gi + varVolumeStorageConfig: + storageClassName: gp2 + storageCapacity: 25Gi volumes: - name: licenses configMap: @@ -107,10 +110,8 @@ Enterprise resources, including: `Standalone`, `LicenseMaster`, | Key | Type | Description | | ------------------ | ------- | ----------------------------------------------------------------------------- | -| storageClassName | string | Name of [StorageClass](StorageClass.md) to use for persistent volume claims | -| etcStorage | string | Storage capacity to request for Splunk etc volume claims (default="10Gi") | -| varStorage | string | Storage capacity to request for Splunk var volume claims (default="100Gi") | -| ephemeralStorage | boolean | If true, ephemeral (emptyDir) storage will be used for etc and var volumes (default=false) | +| etcVolumeStorageConfig | StorageClassSpec | Storage class spec for Splunk etc volume as described in [StorageClass](StorageClass.md) | +| varVolumeStorageConfig | StorageClassSpec | Storage class spec for Splunk var volume as described in [StorageClass](StorageClass.md) | | volumes | [[]Volume](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.17/#volume-v1-core) | List of one or more [Kubernetes volumes](https://kubernetes.io/docs/concepts/storage/volumes/). These will be mounted in all container pods as as `/mnt/` | | defaults | string | Inline map of [default.yml](https://github.com/splunk/splunk-ansible/blob/develop/docs/advanced/default.yml.spec.md) overrides used to initialize the environment | | defaultsUrl | string | Full path or URL for one or more [default.yml](https://github.com/splunk/splunk-ansible/blob/develop/docs/advanced/default.yml.spec.md) files, separated by commas | diff --git a/pkg/apis/enterprise/v1beta1/common_types.go b/pkg/apis/enterprise/v1beta1/common_types.go index 5c6cc5d80..37ddf74f8 100644 --- a/pkg/apis/enterprise/v1beta1/common_types.go +++ b/pkg/apis/enterprise/v1beta1/common_types.go @@ -52,10 +52,10 @@ type CommonSplunkSpec struct { // Full path or URL for one or more default.yml files, separated by commas DefaultsURL string `json:"defaultsUrl"` - // Full path or URL for one or more default.yml files specific to App install, separated by commas - // This is meant as a temporary fix until the SHC Deployer has its own CRD. The defaults listed here will - // only be installed on a standalone, or a SHC Deployer or IDC CM to be pushed to SH/IDXs in a cluster. - // This parameter is ignored on individual SHs or IDXs within a cluster + // Full path or URL for one or more defaults.yml files specific + // to App install, separated by commas. The defaults listed here + // will be installed on the CM, standalone, search head deployer + // or license master instance. DefaultsURLApps string `json:"defaultsUrlApps"` // Full path or URL for a Splunk Enterprise license file From 9aa689b95b497bd0f1e82a79df5799f106414e42 Mon Sep 17 00:00:00 2001 From: akondur Date: Fri, 8 Jan 2021 14:17:50 -0800 Subject: [PATCH 14/43] Adding support to configure a custom serviceaccount per CRD --- ...erprise.splunk.com_clustermasters_crd.yaml | 4 + ...rprise.splunk.com_indexerclusters_crd.yaml | 4 + ...erprise.splunk.com_licensemasters_crd.yaml | 4 + ...ise.splunk.com_searchheadclusters_crd.yaml | 4 + ...enterprise.splunk.com_standalones_crd.yaml | 4 + ...erprise.splunk.com_clustermasters_crd.yaml | 4 + ...rprise.splunk.com_indexerclusters_crd.yaml | 4 + ...erprise.splunk.com_licensemasters_crd.yaml | 4 + ...ise.splunk.com_searchheadclusters_crd.yaml | 4 + ...enterprise.splunk.com_standalones_crd.yaml | 4 + docs/CustomResources.md | 4 +- pkg/apis/enterprise/v1beta1/common_types.go | 3 + pkg/splunk/controller/serviceaccount.go | 57 +++++++++++++++ pkg/splunk/controller/serviceaccount_test.go | 73 +++++++++++++++++++ pkg/splunk/enterprise/clustermaster_test.go | 10 +++ pkg/splunk/enterprise/configuration.go | 10 +++ pkg/splunk/enterprise/indexercluster_test.go | 11 +++ pkg/splunk/enterprise/licensemaster_test.go | 12 +++ .../enterprise/searchheadcluster_test.go | 21 ++++++ pkg/splunk/enterprise/standalone_test.go | 11 +++ pkg/splunk/test/controller.go | 2 + 21 files changed, 252 insertions(+), 2 deletions(-) create mode 100644 pkg/splunk/controller/serviceaccount.go create mode 100644 pkg/splunk/controller/serviceaccount_test.go diff --git a/deploy/crds/enterprise.splunk.com_clustermasters_crd.yaml b/deploy/crds/enterprise.splunk.com_clustermasters_crd.yaml index d02058084..b2492620f 100644 --- a/deploy/crds/enterprise.splunk.com_clustermasters_crd.yaml +++ b/deploy/crds/enterprise.splunk.com_clustermasters_crd.yaml @@ -744,6 +744,10 @@ spec: description: Name of Scheduler to use for pod placement (defaults to “default-scheduler”) type: string + serviceAccount: + description: ServiceAccount is the service account used by the pods + deployed by the CRD + type: string serviceTemplate: description: ServiceTemplate is a template used to create Kubernetes services diff --git a/deploy/crds/enterprise.splunk.com_indexerclusters_crd.yaml b/deploy/crds/enterprise.splunk.com_indexerclusters_crd.yaml index 5b0f94156..ca9fe63bc 100644 --- a/deploy/crds/enterprise.splunk.com_indexerclusters_crd.yaml +++ b/deploy/crds/enterprise.splunk.com_indexerclusters_crd.yaml @@ -767,6 +767,10 @@ spec: description: Name of Scheduler to use for pod placement (defaults to “default-scheduler”) type: string + serviceAccount: + description: ServiceAccount is the service account used by the pods + deployed by the CRD + type: string serviceTemplate: description: ServiceTemplate is a template used to create Kubernetes services diff --git a/deploy/crds/enterprise.splunk.com_licensemasters_crd.yaml b/deploy/crds/enterprise.splunk.com_licensemasters_crd.yaml index 89200ca61..4064727b9 100644 --- a/deploy/crds/enterprise.splunk.com_licensemasters_crd.yaml +++ b/deploy/crds/enterprise.splunk.com_licensemasters_crd.yaml @@ -749,6 +749,10 @@ spec: description: Name of Scheduler to use for pod placement (defaults to “default-scheduler”) type: string + serviceAccount: + description: ServiceAccount is the service account used by the pods + deployed by the CRD + type: string serviceTemplate: description: ServiceTemplate is a template used to create Kubernetes services diff --git a/deploy/crds/enterprise.splunk.com_searchheadclusters_crd.yaml b/deploy/crds/enterprise.splunk.com_searchheadclusters_crd.yaml index b189409b8..f6ce507b4 100644 --- a/deploy/crds/enterprise.splunk.com_searchheadclusters_crd.yaml +++ b/deploy/crds/enterprise.splunk.com_searchheadclusters_crd.yaml @@ -771,6 +771,10 @@ spec: description: Name of Scheduler to use for pod placement (defaults to “default-scheduler”) type: string + serviceAccount: + description: ServiceAccount is the service account used by the pods + deployed by the CRD + type: string serviceTemplate: description: ServiceTemplate is a template used to create Kubernetes services diff --git a/deploy/crds/enterprise.splunk.com_standalones_crd.yaml b/deploy/crds/enterprise.splunk.com_standalones_crd.yaml index 293f7bdfe..a3d03aab0 100644 --- a/deploy/crds/enterprise.splunk.com_standalones_crd.yaml +++ b/deploy/crds/enterprise.splunk.com_standalones_crd.yaml @@ -763,6 +763,10 @@ spec: description: Name of Scheduler to use for pod placement (defaults to “default-scheduler”) type: string + serviceAccount: + description: ServiceAccount is the service account used by the pods + deployed by the CRD + type: string serviceTemplate: description: ServiceTemplate is a template used to create Kubernetes services diff --git a/deploy/olm-catalog/splunk/0.2.1/enterprise.splunk.com_clustermasters_crd.yaml b/deploy/olm-catalog/splunk/0.2.1/enterprise.splunk.com_clustermasters_crd.yaml index 5c9b2adc7..3b9ae908b 100644 --- a/deploy/olm-catalog/splunk/0.2.1/enterprise.splunk.com_clustermasters_crd.yaml +++ b/deploy/olm-catalog/splunk/0.2.1/enterprise.splunk.com_clustermasters_crd.yaml @@ -740,6 +740,10 @@ spec: description: Name of Scheduler to use for pod placement (defaults to “default-scheduler”) type: string + serviceAccount: + description: ServiceAccount is the service account used by the pods + deployed by the CRD + type: string serviceTemplate: description: ServiceTemplate is a template used to create Kubernetes services diff --git a/deploy/olm-catalog/splunk/0.2.1/enterprise.splunk.com_indexerclusters_crd.yaml b/deploy/olm-catalog/splunk/0.2.1/enterprise.splunk.com_indexerclusters_crd.yaml index 5b0f94156..ca9fe63bc 100644 --- a/deploy/olm-catalog/splunk/0.2.1/enterprise.splunk.com_indexerclusters_crd.yaml +++ b/deploy/olm-catalog/splunk/0.2.1/enterprise.splunk.com_indexerclusters_crd.yaml @@ -767,6 +767,10 @@ spec: description: Name of Scheduler to use for pod placement (defaults to “default-scheduler”) type: string + serviceAccount: + description: ServiceAccount is the service account used by the pods + deployed by the CRD + type: string serviceTemplate: description: ServiceTemplate is a template used to create Kubernetes services diff --git a/deploy/olm-catalog/splunk/0.2.1/enterprise.splunk.com_licensemasters_crd.yaml b/deploy/olm-catalog/splunk/0.2.1/enterprise.splunk.com_licensemasters_crd.yaml index 5b2239343..555067d44 100644 --- a/deploy/olm-catalog/splunk/0.2.1/enterprise.splunk.com_licensemasters_crd.yaml +++ b/deploy/olm-catalog/splunk/0.2.1/enterprise.splunk.com_licensemasters_crd.yaml @@ -745,6 +745,10 @@ spec: description: Name of Scheduler to use for pod placement (defaults to “default-scheduler”) type: string + serviceAccount: + description: ServiceAccount is the service account used by the pods + deployed by the CRD + type: string serviceTemplate: description: ServiceTemplate is a template used to create Kubernetes services diff --git a/deploy/olm-catalog/splunk/0.2.1/enterprise.splunk.com_searchheadclusters_crd.yaml b/deploy/olm-catalog/splunk/0.2.1/enterprise.splunk.com_searchheadclusters_crd.yaml index 77d75f9aa..bbe22a970 100644 --- a/deploy/olm-catalog/splunk/0.2.1/enterprise.splunk.com_searchheadclusters_crd.yaml +++ b/deploy/olm-catalog/splunk/0.2.1/enterprise.splunk.com_searchheadclusters_crd.yaml @@ -767,6 +767,10 @@ spec: description: Name of Scheduler to use for pod placement (defaults to “default-scheduler”) type: string + serviceAccount: + description: ServiceAccount is the service account used by the pods + deployed by the CRD + type: string serviceTemplate: description: ServiceTemplate is a template used to create Kubernetes services diff --git a/deploy/olm-catalog/splunk/0.2.1/enterprise.splunk.com_standalones_crd.yaml b/deploy/olm-catalog/splunk/0.2.1/enterprise.splunk.com_standalones_crd.yaml index 044014cd5..3ce82b3cd 100644 --- a/deploy/olm-catalog/splunk/0.2.1/enterprise.splunk.com_standalones_crd.yaml +++ b/deploy/olm-catalog/splunk/0.2.1/enterprise.splunk.com_standalones_crd.yaml @@ -759,6 +759,10 @@ spec: description: Name of Scheduler to use for pod placement (defaults to “default-scheduler”) type: string + serviceAccount: + description: ServiceAccount is the service account used by the pods + deployed by the CRD + type: string serviceTemplate: description: ServiceTemplate is a template used to create Kubernetes services diff --git a/docs/CustomResources.md b/docs/CustomResources.md index 3506e9741..889ff68a8 100644 --- a/docs/CustomResources.md +++ b/docs/CustomResources.md @@ -79,7 +79,6 @@ configuration parameters: | resources | [ResourceRequirements](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.17/#resourcerequirements-v1-core) | CPU and memory [compute resource requirements](https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/) to use for each pod instance (defaults shown in example above) | | serviceTemplate | [Service](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.17/#service-v1-core) | Template used to create Kubernetes [Services](https://kubernetes.io/docs/concepts/services-networking/service/) | - ## Common Spec Parameters for Splunk Enterprise Resources ```yaml @@ -99,6 +98,7 @@ spec: name: example clusterMasterRef: name: example + serviceAccount: custom-serviceaccount ``` The following additional configuration parameters may be used for all Splunk @@ -117,7 +117,7 @@ Enterprise resources, including: `Standalone`, `LicenseMaster`, | licenseUrl | string | Full path or URL for a Splunk Enterprise license file | | licenseMasterRef | [ObjectReference](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.17/#objectreference-v1-core) | Reference to a Splunk Operator managed `LicenseMaster` instance (via `name` and optionally `namespace`) to use for licensing | | clusterMasterRef | [ObjectReference](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.17/#objectreference-v1-core) | Reference to a Splunk Operator managed `ClusterMaster` instance (via `name` and optionally `namespace`) to use for indexing | - +| serviceAccount | [ServiceAccount](https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/) | Represents the service account used by the pods deployed by the CRD | ## Spark Resource Spec Parameters diff --git a/pkg/apis/enterprise/v1beta1/common_types.go b/pkg/apis/enterprise/v1beta1/common_types.go index d3f161eff..404f5f231 100644 --- a/pkg/apis/enterprise/v1beta1/common_types.go +++ b/pkg/apis/enterprise/v1beta1/common_types.go @@ -75,6 +75,9 @@ type CommonSplunkSpec struct { // Mock to differentiate between UTs and actual reconcile Mock bool `json:"Mock"` + + // ServiceAccount is the service account used by the pods deployed by the CRD + ServiceAccount string `json:"serviceAccount"` } // SmartStoreSpec defines Splunk indexes and remote storage volume configuration diff --git a/pkg/splunk/controller/serviceaccount.go b/pkg/splunk/controller/serviceaccount.go new file mode 100644 index 000000000..4bf11b968 --- /dev/null +++ b/pkg/splunk/controller/serviceaccount.go @@ -0,0 +1,57 @@ +// Copyright (c) 2018-2020 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" + "reflect" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" + + splcommon "github.com/splunk/splunk-operator/pkg/splunk/common" + splutil "github.com/splunk/splunk-operator/pkg/splunk/util" +) + +// ApplyServiceAccount creates or updates a Kubernetes serviceAccount +func ApplyServiceAccount(client splcommon.ControllerClient, serviceAccount *corev1.ServiceAccount) error { + namespacedName := types.NamespacedName{Namespace: serviceAccount.GetNamespace(), Name: serviceAccount.GetName()} + var current corev1.ServiceAccount + + err := client.Get(context.TODO(), namespacedName, ¤t) + if err == nil { + if !reflect.DeepEqual(serviceAccount, ¤t) { + current = *serviceAccount + err = splutil.UpdateResource(client, ¤t) + } + } else { + err = splutil.CreateResource(client, serviceAccount) + } + + return err +} + +// GetServiceAccount gets the serviceAccount resource in a given namespace +func GetServiceAccount(client splcommon.ControllerClient, namespacedName types.NamespacedName) (*corev1.ServiceAccount, error) { + scopedLog := log.WithName("GetServiceAccount").WithValues("serviceAccount", namespacedName.Name, + "namespace", namespacedName.Namespace) + var serviceAccount corev1.ServiceAccount + err := client.Get(context.TODO(), namespacedName, &serviceAccount) + if err != nil { + scopedLog.Info("ServiceAccount not found") + return nil, err + } + return &serviceAccount, nil +} diff --git a/pkg/splunk/controller/serviceaccount_test.go b/pkg/splunk/controller/serviceaccount_test.go new file mode 100644 index 000000000..30f8d37d4 --- /dev/null +++ b/pkg/splunk/controller/serviceaccount_test.go @@ -0,0 +1,73 @@ +// Copyright (c) 2018-2020 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 ( + "testing" + + spltest "github.com/splunk/splunk-operator/pkg/splunk/test" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" +) + +func TestApplyServiceAccount(t *testing.T) { + funcCalls := []spltest.MockFuncCall{{MetaName: "*v1.ServiceAccount-test-defaults"}} + createCalls := map[string][]spltest.MockFuncCall{"Get": funcCalls, "Create": funcCalls} + updateCalls := map[string][]spltest.MockFuncCall{"Get": funcCalls, "Update": funcCalls} + current := corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: "defaults", + Namespace: "test", + }, + } + revised := current.DeepCopy() + revised.ResourceVersion = "dummy" + reconcile := func(c *spltest.MockClient, cr interface{}) error { + err := ApplyServiceAccount(c, cr.(*corev1.ServiceAccount)) + return err + } + spltest.ReconcileTester(t, "TestApplyServiceAccount", ¤t, revised, createCalls, updateCalls, reconcile, false) +} + +func TestGetServiceAccount(t *testing.T) { + current := corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: "defaults", + Namespace: "test", + }, + } + + client := spltest.NewMockClient() + namespacedName := types.NamespacedName{Namespace: current.GetNamespace(), Name: current.GetName()} + + // serviceAccount doesn't exist + _, err := GetServiceAccount(client, namespacedName) + if err == nil { + t.Errorf("Should return an error, when the serviceAccount doesn't exist") + } + + // Create serviceAccount + err = ApplyServiceAccount(client, ¤t) + if err != nil { + t.Errorf("Failed to create the serviceAccount. Error: %s", err.Error()) + } + + // Make sure serviceAccount exists + _, err = GetServiceAccount(client, namespacedName) + if err != nil { + t.Errorf("Should not return an error, when the serviceAccount exists") + } +} diff --git a/pkg/splunk/enterprise/clustermaster_test.go b/pkg/splunk/enterprise/clustermaster_test.go index ca5bc7830..0f40eb298 100644 --- a/pkg/splunk/enterprise/clustermaster_test.go +++ b/pkg/splunk/enterprise/clustermaster_test.go @@ -128,6 +128,16 @@ func TestGetClusterMasterStatefulSet(t *testing.T) { cr.Spec.DefaultsURLApps = "/mnt/apps/apps.yml" test(`{"kind":"StatefulSet","apiVersion":"apps/v1","metadata":{"name":"splunk-stack1-cluster-master","namespace":"test","creationTimestamp":null,"ownerReferences":[{"apiVersion":"","kind":"","name":"stack1","uid":"","controller":true}]},"spec":{"replicas":1,"selector":{"matchLabels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-cluster-master","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"cluster-master","app.kubernetes.io/part-of":"splunk-stack1-indexer"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-cluster-master","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"cluster-master","app.kubernetes.io/part-of":"splunk-stack1-indexer"},"annotations":{"traffic.sidecar.istio.io/excludeOutboundPorts":"8089,8191,9997,7777,9000,17000,17500,19000","traffic.sidecar.istio.io/includeInboundPorts":"8000"}},"spec":{"volumes":[{"name":"mnt-splunk-secrets","secret":{"secretName":"splunk-stack1-cluster-master-secret-v1","defaultMode":420}}],"containers":[{"name":"splunk","image":"splunk/splunk","ports":[{"name":"splunkweb","containerPort":8000,"protocol":"TCP"},{"name":"splunkd","containerPort":8089,"protocol":"TCP"}],"env":[{"name":"SPLUNK_HOME","value":"/opt/splunk"},{"name":"SPLUNK_START_ARGS","value":"--accept-license"},{"name":"SPLUNK_DEFAULTS_URL","value":"/mnt/apps/apps.yml,/mnt/splunk-secrets/default.yml"},{"name":"SPLUNK_HOME_OWNERSHIP_ENFORCEMENT","value":"false"},{"name":"SPLUNK_ROLE","value":"splunk_cluster_master"},{"name":"SPLUNK_DECLARATIVE_ADMIN_PASSWORD","value":"true"},{"name":"SPLUNK_LICENSE_URI","value":"/mnt/splunk.lic"},{"name":"SPLUNK_CLUSTER_MASTER_URL","value":"localhost"}],"resources":{"limits":{"cpu":"4","memory":"8Gi"},"requests":{"cpu":"100m","memory":"512Mi"}},"volumeMounts":[{"name":"pvc-etc","mountPath":"/opt/splunk/etc"},{"name":"pvc-var","mountPath":"/opt/splunk/var"},{"name":"mnt-splunk-secrets","mountPath":"/mnt/splunk-secrets"}],"livenessProbe":{"exec":{"command":["/sbin/checkstate.sh"]},"initialDelaySeconds":300,"timeoutSeconds":30,"periodSeconds":30},"readinessProbe":{"exec":{"command":["/bin/grep","started","/opt/container_artifact/splunk-container.state"]},"initialDelaySeconds":10,"timeoutSeconds":5,"periodSeconds":5},"imagePullPolicy":"IfNotPresent"}],"securityContext":{"runAsUser":41812,"fsGroup":41812},"affinity":{"podAntiAffinity":{"preferredDuringSchedulingIgnoredDuringExecution":[{"weight":100,"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app.kubernetes.io/instance","operator":"In","values":["splunk-stack1-cluster-master"]}]},"topologyKey":"kubernetes.io/hostname"}}]}},"schedulerName":"default-scheduler"}},"volumeClaimTemplates":[{"metadata":{"name":"pvc-etc","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-cluster-master","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"cluster-master","app.kubernetes.io/part-of":"splunk-stack1-indexer"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"10Gi"}}},"status":{}},{"metadata":{"name":"pvc-var","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-cluster-master","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"cluster-master","app.kubernetes.io/part-of":"splunk-stack1-indexer"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"100Gi"}}},"status":{}}],"serviceName":"splunk-stack1-cluster-master-headless","podManagementPolicy":"Parallel","updateStrategy":{"type":"OnDelete"}},"status":{"replicas":0}}`) + // Create a serviceaccount + current := corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: "defaults", + Namespace: "test", + }, + } + _ = splutil.CreateResource(c, ¤t) + cr.Spec.ServiceAccount = "defaults" + test(`{"kind":"StatefulSet","apiVersion":"apps/v1","metadata":{"name":"splunk-stack1-cluster-master","namespace":"test","creationTimestamp":null,"ownerReferences":[{"apiVersion":"","kind":"","name":"stack1","uid":"","controller":true}]},"spec":{"replicas":1,"selector":{"matchLabels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-cluster-master","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"cluster-master","app.kubernetes.io/part-of":"splunk-stack1-indexer"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-cluster-master","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"cluster-master","app.kubernetes.io/part-of":"splunk-stack1-indexer"},"annotations":{"traffic.sidecar.istio.io/excludeOutboundPorts":"8089,8191,9997,7777,9000,17000,17500,19000","traffic.sidecar.istio.io/includeInboundPorts":"8000"}},"spec":{"volumes":[{"name":"mnt-splunk-secrets","secret":{"secretName":"splunk-stack1-cluster-master-secret-v1","defaultMode":420}}],"containers":[{"name":"splunk","image":"splunk/splunk","ports":[{"name":"splunkweb","containerPort":8000,"protocol":"TCP"},{"name":"splunkd","containerPort":8089,"protocol":"TCP"}],"env":[{"name":"SPLUNK_HOME","value":"/opt/splunk"},{"name":"SPLUNK_START_ARGS","value":"--accept-license"},{"name":"SPLUNK_DEFAULTS_URL","value":"/mnt/apps/apps.yml,/mnt/splunk-secrets/default.yml"},{"name":"SPLUNK_HOME_OWNERSHIP_ENFORCEMENT","value":"false"},{"name":"SPLUNK_ROLE","value":"splunk_cluster_master"},{"name":"SPLUNK_DECLARATIVE_ADMIN_PASSWORD","value":"true"},{"name":"SPLUNK_LICENSE_URI","value":"/mnt/splunk.lic"},{"name":"SPLUNK_CLUSTER_MASTER_URL","value":"localhost"}],"resources":{"limits":{"cpu":"4","memory":"8Gi"},"requests":{"cpu":"100m","memory":"512Mi"}},"volumeMounts":[{"name":"pvc-etc","mountPath":"/opt/splunk/etc"},{"name":"pvc-var","mountPath":"/opt/splunk/var"},{"name":"mnt-splunk-secrets","mountPath":"/mnt/splunk-secrets"}],"livenessProbe":{"exec":{"command":["/sbin/checkstate.sh"]},"initialDelaySeconds":300,"timeoutSeconds":30,"periodSeconds":30},"readinessProbe":{"exec":{"command":["/bin/grep","started","/opt/container_artifact/splunk-container.state"]},"initialDelaySeconds":10,"timeoutSeconds":5,"periodSeconds":5},"imagePullPolicy":"IfNotPresent"}],"serviceAccountName":"defaults","securityContext":{"runAsUser":41812,"fsGroup":41812},"affinity":{"podAntiAffinity":{"preferredDuringSchedulingIgnoredDuringExecution":[{"weight":100,"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app.kubernetes.io/instance","operator":"In","values":["splunk-stack1-cluster-master"]}]},"topologyKey":"kubernetes.io/hostname"}}]}},"schedulerName":"default-scheduler"}},"volumeClaimTemplates":[{"metadata":{"name":"pvc-etc","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-cluster-master","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"cluster-master","app.kubernetes.io/part-of":"splunk-stack1-indexer"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"10Gi"}}},"status":{}},{"metadata":{"name":"pvc-var","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-cluster-master","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"cluster-master","app.kubernetes.io/part-of":"splunk-stack1-indexer"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"100Gi"}}},"status":{}}],"serviceName":"splunk-stack1-cluster-master-headless","podManagementPolicy":"Parallel","updateStrategy":{"type":"OnDelete"}},"status":{"replicas":0}}`) } func TestApplyClusterMasterWithSmartstore(t *testing.T) { diff --git a/pkg/splunk/enterprise/configuration.go b/pkg/splunk/enterprise/configuration.go index f660c67e9..3fb93edcc 100644 --- a/pkg/splunk/enterprise/configuration.go +++ b/pkg/splunk/enterprise/configuration.go @@ -416,6 +416,16 @@ func getSplunkStatefulSet(client splcommon.ControllerClient, cr splcommon.MetaOb } } + // add serviceaccount if configured + if spec.ServiceAccount != "" { + namespacedName := types.NamespacedName{Namespace: statefulSet.GetNamespace(), Name: spec.ServiceAccount} + _, err := splctrl.GetServiceAccount(client, namespacedName) + if err == nil { + // serviceAccount exists + statefulSet.Spec.Template.Spec.ServiceAccountName = spec.ServiceAccount + } + } + // append labels and annotations from parent splcommon.AppendParentMeta(statefulSet.Spec.Template.GetObjectMeta(), cr.GetObjectMeta()) diff --git a/pkg/splunk/enterprise/indexercluster_test.go b/pkg/splunk/enterprise/indexercluster_test.go index bfd9e681c..fef178609 100644 --- a/pkg/splunk/enterprise/indexercluster_test.go +++ b/pkg/splunk/enterprise/indexercluster_test.go @@ -1035,6 +1035,17 @@ func TestGetIndexerStatefulSet(t *testing.T) { cr.Spec.DefaultsURLApps = "/mnt/apps/apps.yml" test(`{"kind":"StatefulSet","apiVersion":"apps/v1","metadata":{"name":"splunk-stack1-indexer","namespace":"test","creationTimestamp":null,"ownerReferences":[{"apiVersion":"","kind":"","name":"stack1","uid":"","controller":true}]},"spec":{"replicas":1,"selector":{"matchLabels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-master1-indexer"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-master1-indexer"},"annotations":{"traffic.sidecar.istio.io/excludeOutboundPorts":"8089,8191,9997,7777,9000,17000,17500,19000","traffic.sidecar.istio.io/includeInboundPorts":"8000,8088"}},"spec":{"volumes":[{"name":"mnt-splunk-secrets","secret":{"secretName":"splunk-stack1-indexer-secret-v1","defaultMode":420}}],"containers":[{"name":"splunk","image":"splunk/splunk","ports":[{"name":"splunkweb","containerPort":8000,"protocol":"TCP"},{"name":"hec","containerPort":8088,"protocol":"TCP"},{"name":"splunkd","containerPort":8089,"protocol":"TCP"},{"name":"s2s","containerPort":9997,"protocol":"TCP"},{"name":"user-defined","containerPort":32000,"protocol":"UDP"}],"env":[{"name":"SPLUNK_HOME","value":"/opt/splunk"},{"name":"SPLUNK_START_ARGS","value":"--accept-license"},{"name":"SPLUNK_DEFAULTS_URL","value":"/mnt/splunk-secrets/default.yml"},{"name":"SPLUNK_HOME_OWNERSHIP_ENFORCEMENT","value":"false"},{"name":"SPLUNK_ROLE","value":"splunk_indexer"},{"name":"SPLUNK_DECLARATIVE_ADMIN_PASSWORD","value":"true"},{"name":"SPLUNK_INDEXER_URL","value":"splunk-stack1-indexer-0.splunk-stack1-indexer-headless.test.svc.cluster.local"},{"name":"SPLUNK_CLUSTER_MASTER_URL","value":"splunk-master1-cluster-master-service"}],"resources":{"limits":{"cpu":"4","memory":"8Gi"},"requests":{"cpu":"100m","memory":"512Mi"}},"volumeMounts":[{"name":"pvc-etc","mountPath":"/opt/splunk/etc"},{"name":"pvc-var","mountPath":"/opt/splunk/var"},{"name":"mnt-splunk-secrets","mountPath":"/mnt/splunk-secrets"}],"livenessProbe":{"exec":{"command":["/sbin/checkstate.sh"]},"initialDelaySeconds":300,"timeoutSeconds":30,"periodSeconds":30},"readinessProbe":{"exec":{"command":["/bin/grep","started","/opt/container_artifact/splunk-container.state"]},"initialDelaySeconds":10,"timeoutSeconds":5,"periodSeconds":5},"imagePullPolicy":"IfNotPresent"}],"securityContext":{"runAsUser":41812,"fsGroup":41812},"affinity":{"podAntiAffinity":{"preferredDuringSchedulingIgnoredDuringExecution":[{"weight":100,"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app.kubernetes.io/instance","operator":"In","values":["splunk-stack1-indexer"]}]},"topologyKey":"kubernetes.io/hostname"}}]}},"schedulerName":"default-scheduler"}},"volumeClaimTemplates":[{"metadata":{"name":"pvc-etc","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-master1-indexer"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"10Gi"}}},"status":{}},{"metadata":{"name":"pvc-var","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-master1-indexer"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"100Gi"}}},"status":{}}],"serviceName":"splunk-stack1-indexer-headless","podManagementPolicy":"Parallel","updateStrategy":{"type":"OnDelete"}},"status":{"replicas":0}}`) + // Create a serviceaccount + current := corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: "defaults", + Namespace: "test", + }, + } + _ = splutil.CreateResource(c, ¤t) + cr.Spec.ServiceAccount = "defaults" + test(`{"kind":"StatefulSet","apiVersion":"apps/v1","metadata":{"name":"splunk-stack1-indexer","namespace":"test","creationTimestamp":null,"ownerReferences":[{"apiVersion":"","kind":"","name":"stack1","uid":"","controller":true}]},"spec":{"replicas":1,"selector":{"matchLabels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-master1-indexer"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-master1-indexer"},"annotations":{"traffic.sidecar.istio.io/excludeOutboundPorts":"8089,8191,9997,7777,9000,17000,17500,19000","traffic.sidecar.istio.io/includeInboundPorts":"8000,8088"}},"spec":{"volumes":[{"name":"mnt-splunk-secrets","secret":{"secretName":"splunk-stack1-indexer-secret-v1","defaultMode":420}}],"containers":[{"name":"splunk","image":"splunk/splunk","ports":[{"name":"splunkweb","containerPort":8000,"protocol":"TCP"},{"name":"hec","containerPort":8088,"protocol":"TCP"},{"name":"splunkd","containerPort":8089,"protocol":"TCP"},{"name":"s2s","containerPort":9997,"protocol":"TCP"},{"name":"user-defined","containerPort":32000,"protocol":"UDP"}],"env":[{"name":"SPLUNK_HOME","value":"/opt/splunk"},{"name":"SPLUNK_START_ARGS","value":"--accept-license"},{"name":"SPLUNK_DEFAULTS_URL","value":"/mnt/splunk-secrets/default.yml"},{"name":"SPLUNK_HOME_OWNERSHIP_ENFORCEMENT","value":"false"},{"name":"SPLUNK_ROLE","value":"splunk_indexer"},{"name":"SPLUNK_DECLARATIVE_ADMIN_PASSWORD","value":"true"},{"name":"SPLUNK_INDEXER_URL","value":"splunk-stack1-indexer-0.splunk-stack1-indexer-headless.test.svc.cluster.local"},{"name":"SPLUNK_CLUSTER_MASTER_URL","value":"splunk-master1-cluster-master-service"}],"resources":{"limits":{"cpu":"4","memory":"8Gi"},"requests":{"cpu":"100m","memory":"512Mi"}},"volumeMounts":[{"name":"pvc-etc","mountPath":"/opt/splunk/etc"},{"name":"pvc-var","mountPath":"/opt/splunk/var"},{"name":"mnt-splunk-secrets","mountPath":"/mnt/splunk-secrets"}],"livenessProbe":{"exec":{"command":["/sbin/checkstate.sh"]},"initialDelaySeconds":300,"timeoutSeconds":30,"periodSeconds":30},"readinessProbe":{"exec":{"command":["/bin/grep","started","/opt/container_artifact/splunk-container.state"]},"initialDelaySeconds":10,"timeoutSeconds":5,"periodSeconds":5},"imagePullPolicy":"IfNotPresent"}],"serviceAccountName":"defaults","securityContext":{"runAsUser":41812,"fsGroup":41812},"affinity":{"podAntiAffinity":{"preferredDuringSchedulingIgnoredDuringExecution":[{"weight":100,"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app.kubernetes.io/instance","operator":"In","values":["splunk-stack1-indexer"]}]},"topologyKey":"kubernetes.io/hostname"}}]}},"schedulerName":"default-scheduler"}},"volumeClaimTemplates":[{"metadata":{"name":"pvc-etc","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-master1-indexer"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"10Gi"}}},"status":{}},{"metadata":{"name":"pvc-var","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-master1-indexer"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"100Gi"}}},"status":{}}],"serviceName":"splunk-stack1-indexer-headless","podManagementPolicy":"Parallel","updateStrategy":{"type":"OnDelete"}},"status":{"replicas":0}}`) + cr.Spec.ClusterMasterRef.Namespace = "other" if err := validateIndexerClusterSpec(&cr); err == nil { t.Errorf("validateIndexerClusterSpec() error expected on multisite IndexerCluster referencing a cluster master located in a different namespace") diff --git a/pkg/splunk/enterprise/licensemaster_test.go b/pkg/splunk/enterprise/licensemaster_test.go index 2302a946c..33972935d 100644 --- a/pkg/splunk/enterprise/licensemaster_test.go +++ b/pkg/splunk/enterprise/licensemaster_test.go @@ -18,6 +18,7 @@ import ( "testing" "time" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/client" @@ -108,4 +109,15 @@ func TestGetLicenseMasterStatefulSet(t *testing.T) { // Allow installing apps via DefaultsURLApps for Licence Master cr.Spec.DefaultsURLApps = "/mnt/apps/apps.yml" test(`{"kind":"StatefulSet","apiVersion":"apps/v1","metadata":{"name":"splunk-stack1-license-master","namespace":"test","creationTimestamp":null,"ownerReferences":[{"apiVersion":"","kind":"","name":"stack1","uid":"","controller":true}]},"spec":{"replicas":1,"selector":{"matchLabels":{"app.kubernetes.io/component":"license-master","app.kubernetes.io/instance":"splunk-stack1-license-master","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"license-master","app.kubernetes.io/part-of":"splunk-stack1-license-master"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app.kubernetes.io/component":"license-master","app.kubernetes.io/instance":"splunk-stack1-license-master","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"license-master","app.kubernetes.io/part-of":"splunk-stack1-license-master"},"annotations":{"traffic.sidecar.istio.io/excludeOutboundPorts":"8089,8191,9997,7777,9000,17000,17500,19000","traffic.sidecar.istio.io/includeInboundPorts":"8000"}},"spec":{"volumes":[{"name":"mnt-splunk-secrets","secret":{"secretName":"splunk-stack1-license-master-secret-v1","defaultMode":420}}],"containers":[{"name":"splunk","image":"splunk/splunk","ports":[{"name":"splunkweb","containerPort":8000,"protocol":"TCP"},{"name":"splunkd","containerPort":8089,"protocol":"TCP"}],"env":[{"name":"SPLUNK_HOME","value":"/opt/splunk"},{"name":"SPLUNK_START_ARGS","value":"--accept-license"},{"name":"SPLUNK_DEFAULTS_URL","value":"/mnt/apps/apps.yml,/mnt/splunk-secrets/default.yml"},{"name":"SPLUNK_HOME_OWNERSHIP_ENFORCEMENT","value":"false"},{"name":"SPLUNK_ROLE","value":"splunk_license_master"},{"name":"SPLUNK_DECLARATIVE_ADMIN_PASSWORD","value":"true"},{"name":"SPLUNK_LICENSE_URI","value":"/mnt/splunk.lic"}],"resources":{"limits":{"cpu":"4","memory":"8Gi"},"requests":{"cpu":"100m","memory":"512Mi"}},"volumeMounts":[{"name":"pvc-etc","mountPath":"/opt/splunk/etc"},{"name":"pvc-var","mountPath":"/opt/splunk/var"},{"name":"mnt-splunk-secrets","mountPath":"/mnt/splunk-secrets"}],"livenessProbe":{"exec":{"command":["/sbin/checkstate.sh"]},"initialDelaySeconds":300,"timeoutSeconds":30,"periodSeconds":30},"readinessProbe":{"exec":{"command":["/bin/grep","started","/opt/container_artifact/splunk-container.state"]},"initialDelaySeconds":10,"timeoutSeconds":5,"periodSeconds":5},"imagePullPolicy":"IfNotPresent"}],"securityContext":{"runAsUser":41812,"fsGroup":41812},"affinity":{"podAntiAffinity":{"preferredDuringSchedulingIgnoredDuringExecution":[{"weight":100,"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app.kubernetes.io/instance","operator":"In","values":["splunk-stack1-license-master"]}]},"topologyKey":"kubernetes.io/hostname"}}]}},"schedulerName":"default-scheduler"}},"volumeClaimTemplates":[{"metadata":{"name":"pvc-etc","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"license-master","app.kubernetes.io/instance":"splunk-stack1-license-master","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"license-master","app.kubernetes.io/part-of":"splunk-stack1-license-master"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"10Gi"}}},"status":{}},{"metadata":{"name":"pvc-var","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"license-master","app.kubernetes.io/instance":"splunk-stack1-license-master","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"license-master","app.kubernetes.io/part-of":"splunk-stack1-license-master"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"100Gi"}}},"status":{}}],"serviceName":"splunk-stack1-license-master-headless","podManagementPolicy":"Parallel","updateStrategy":{"type":"OnDelete"}},"status":{"replicas":0}}`) + + // Create a serviceaccount + current := corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: "defaults", + Namespace: "test", + }, + } + _ = splutil.CreateResource(c, ¤t) + cr.Spec.ServiceAccount = "defaults" + test(`{"kind":"StatefulSet","apiVersion":"apps/v1","metadata":{"name":"splunk-stack1-license-master","namespace":"test","creationTimestamp":null,"ownerReferences":[{"apiVersion":"","kind":"","name":"stack1","uid":"","controller":true}]},"spec":{"replicas":1,"selector":{"matchLabels":{"app.kubernetes.io/component":"license-master","app.kubernetes.io/instance":"splunk-stack1-license-master","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"license-master","app.kubernetes.io/part-of":"splunk-stack1-license-master"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app.kubernetes.io/component":"license-master","app.kubernetes.io/instance":"splunk-stack1-license-master","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"license-master","app.kubernetes.io/part-of":"splunk-stack1-license-master"},"annotations":{"traffic.sidecar.istio.io/excludeOutboundPorts":"8089,8191,9997,7777,9000,17000,17500,19000","traffic.sidecar.istio.io/includeInboundPorts":"8000"}},"spec":{"volumes":[{"name":"mnt-splunk-secrets","secret":{"secretName":"splunk-stack1-license-master-secret-v1","defaultMode":420}}],"containers":[{"name":"splunk","image":"splunk/splunk","ports":[{"name":"splunkweb","containerPort":8000,"protocol":"TCP"},{"name":"splunkd","containerPort":8089,"protocol":"TCP"}],"env":[{"name":"SPLUNK_HOME","value":"/opt/splunk"},{"name":"SPLUNK_START_ARGS","value":"--accept-license"},{"name":"SPLUNK_DEFAULTS_URL","value":"/mnt/apps/apps.yml,/mnt/splunk-secrets/default.yml"},{"name":"SPLUNK_HOME_OWNERSHIP_ENFORCEMENT","value":"false"},{"name":"SPLUNK_ROLE","value":"splunk_license_master"},{"name":"SPLUNK_DECLARATIVE_ADMIN_PASSWORD","value":"true"},{"name":"SPLUNK_LICENSE_URI","value":"/mnt/splunk.lic"}],"resources":{"limits":{"cpu":"4","memory":"8Gi"},"requests":{"cpu":"100m","memory":"512Mi"}},"volumeMounts":[{"name":"pvc-etc","mountPath":"/opt/splunk/etc"},{"name":"pvc-var","mountPath":"/opt/splunk/var"},{"name":"mnt-splunk-secrets","mountPath":"/mnt/splunk-secrets"}],"livenessProbe":{"exec":{"command":["/sbin/checkstate.sh"]},"initialDelaySeconds":300,"timeoutSeconds":30,"periodSeconds":30},"readinessProbe":{"exec":{"command":["/bin/grep","started","/opt/container_artifact/splunk-container.state"]},"initialDelaySeconds":10,"timeoutSeconds":5,"periodSeconds":5},"imagePullPolicy":"IfNotPresent"}],"serviceAccountName":"defaults","securityContext":{"runAsUser":41812,"fsGroup":41812},"affinity":{"podAntiAffinity":{"preferredDuringSchedulingIgnoredDuringExecution":[{"weight":100,"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app.kubernetes.io/instance","operator":"In","values":["splunk-stack1-license-master"]}]},"topologyKey":"kubernetes.io/hostname"}}]}},"schedulerName":"default-scheduler"}},"volumeClaimTemplates":[{"metadata":{"name":"pvc-etc","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"license-master","app.kubernetes.io/instance":"splunk-stack1-license-master","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"license-master","app.kubernetes.io/part-of":"splunk-stack1-license-master"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"10Gi"}}},"status":{}},{"metadata":{"name":"pvc-var","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"license-master","app.kubernetes.io/instance":"splunk-stack1-license-master","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"license-master","app.kubernetes.io/part-of":"splunk-stack1-license-master"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"100Gi"}}},"status":{}}],"serviceName":"splunk-stack1-license-master-headless","podManagementPolicy":"Parallel","updateStrategy":{"type":"OnDelete"}},"status":{"replicas":0}}`) } diff --git a/pkg/splunk/enterprise/searchheadcluster_test.go b/pkg/splunk/enterprise/searchheadcluster_test.go index 65ab8b0f8..0aa0c9810 100644 --- a/pkg/splunk/enterprise/searchheadcluster_test.go +++ b/pkg/splunk/enterprise/searchheadcluster_test.go @@ -565,6 +565,17 @@ func TestGetSearchHeadStatefulSet(t *testing.T) { // Define additional service port in CR and verified the statefulset has the new port cr.Spec.ServiceTemplate.Spec.Ports = []corev1.ServicePort{{Name: "user-defined", Port: 32000, Protocol: "UDP"}} test(`{"kind":"StatefulSet","apiVersion":"apps/v1","metadata":{"name":"splunk-stack1-search-head","namespace":"test","creationTimestamp":null,"ownerReferences":[{"apiVersion":"","kind":"","name":"stack1","uid":"","controller":true}]},"spec":{"replicas":6,"selector":{"matchLabels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-search-head","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"search-head","app.kubernetes.io/part-of":"splunk-stack1-search-head"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-search-head","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"search-head","app.kubernetes.io/part-of":"splunk-stack1-search-head"},"annotations":{"traffic.sidecar.istio.io/excludeOutboundPorts":"8089,8191,9997,7777,9000,17000,17500,19000","traffic.sidecar.istio.io/includeInboundPorts":"8000"}},"spec":{"volumes":[{"name":"mnt-splunk-secrets","secret":{"secretName":"splunk-stack1-search-head-secret-v1","defaultMode":420}},{"name":"mnt-splunk-jdk","emptyDir":{}},{"name":"mnt-splunk-spark","emptyDir":{}}],"initContainers":[{"name":"init","image":"splunk/spark","command":["bash","-c","cp -r /opt/jdk /mnt \u0026\u0026 cp -r /opt/spark /mnt"],"resources":{"limits":{"cpu":"1","memory":"512Mi"},"requests":{"cpu":"250m","memory":"128Mi"}},"volumeMounts":[{"name":"mnt-splunk-jdk","mountPath":"/mnt/jdk"},{"name":"mnt-splunk-spark","mountPath":"/mnt/spark"}],"imagePullPolicy":"IfNotPresent"}],"containers":[{"name":"splunk","image":"splunk/splunk","ports":[{"name":"splunkweb","containerPort":8000,"protocol":"TCP"},{"name":"splunkd","containerPort":8089,"protocol":"TCP"},{"name":"dfsmaster","containerPort":9000,"protocol":"TCP"},{"name":"dfccontrol","containerPort":17000,"protocol":"TCP"},{"name":"datareceive","containerPort":19000,"protocol":"TCP"},{"name":"user-defined","containerPort":32000,"protocol":"UDP"}],"env":[{"name":"SPLUNK_HOME","value":"/opt/splunk"},{"name":"SPLUNK_START_ARGS","value":"--accept-license"},{"name":"SPLUNK_DEFAULTS_URL","value":"/mnt/splunk-secrets/default.yml"},{"name":"SPLUNK_HOME_OWNERSHIP_ENFORCEMENT","value":"false"},{"name":"SPLUNK_ROLE","value":"splunk_search_head"},{"name":"SPLUNK_DECLARATIVE_ADMIN_PASSWORD","value":"true"},{"name":"SPLUNK_SEARCH_HEAD_URL","value":"splunk-stack1-search-head-0.splunk-stack1-search-head-headless.test.svc.cluster.local,splunk-stack1-search-head-1.splunk-stack1-search-head-headless.test.svc.cluster.local,splunk-stack1-search-head-2.splunk-stack1-search-head-headless.test.svc.cluster.local,splunk-stack1-search-head-3.splunk-stack1-search-head-headless.test.svc.cluster.local,splunk-stack1-search-head-4.splunk-stack1-search-head-headless.test.svc.cluster.local,splunk-stack1-search-head-5.splunk-stack1-search-head-headless.test.svc.cluster.local"},{"name":"SPLUNK_SEARCH_HEAD_CAPTAIN_URL","value":"splunk-stack1-search-head-0.splunk-stack1-search-head-headless.test.svc.cluster.local"},{"name":"SPLUNK_DEPLOYER_URL","value":"splunk-stack1-deployer-service"},{"name":"SPLUNK_CLUSTER_MASTER_URL","value":"splunk-stack1-cluster-master-service.test2.svc.cluster.local"},{"name":"SPLUNK_ENABLE_DFS","value":"true"},{"name":"SPARK_MASTER_HOST","value":"splunk-stack1-spark-master-service"},{"name":"SPARK_MASTER_WEBUI_PORT","value":"8009"},{"name":"SPARK_HOME","value":"/mnt/splunk-spark"},{"name":"JAVA_HOME","value":"/mnt/splunk-jdk"},{"name":"SPLUNK_DFW_NUM_SLOTS_ENABLED","value":"true"}],"resources":{"limits":{"cpu":"4","memory":"8Gi"},"requests":{"cpu":"100m","memory":"512Mi"}},"volumeMounts":[{"name":"pvc-etc","mountPath":"/opt/splunk/etc"},{"name":"pvc-var","mountPath":"/opt/splunk/var"},{"name":"mnt-splunk-secrets","mountPath":"/mnt/splunk-secrets"},{"name":"mnt-splunk-jdk","mountPath":"/mnt/splunk-jdk"},{"name":"mnt-splunk-spark","mountPath":"/mnt/splunk-spark"}],"livenessProbe":{"exec":{"command":["/sbin/checkstate.sh"]},"initialDelaySeconds":300,"timeoutSeconds":30,"periodSeconds":30},"readinessProbe":{"exec":{"command":["/bin/grep","started","/opt/container_artifact/splunk-container.state"]},"initialDelaySeconds":10,"timeoutSeconds":5,"periodSeconds":5},"imagePullPolicy":"IfNotPresent"}],"securityContext":{"runAsUser":41812,"fsGroup":41812},"affinity":{"podAntiAffinity":{"preferredDuringSchedulingIgnoredDuringExecution":[{"weight":100,"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app.kubernetes.io/instance","operator":"In","values":["splunk-stack1-search-head"]}]},"topologyKey":"kubernetes.io/hostname"}}]}},"schedulerName":"default-scheduler"}},"volumeClaimTemplates":[{"metadata":{"name":"pvc-etc","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-search-head","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"search-head","app.kubernetes.io/part-of":"splunk-stack1-search-head"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"10Gi"}}},"status":{}},{"metadata":{"name":"pvc-var","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-search-head","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"search-head","app.kubernetes.io/part-of":"splunk-stack1-search-head"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"100Gi"}}},"status":{}}],"serviceName":"splunk-stack1-search-head-headless","podManagementPolicy":"Parallel","updateStrategy":{"type":"OnDelete"}},"status":{"replicas":0}}`) + + // Create a serviceaccount + current := corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: "defaults", + Namespace: "test", + }, + } + _ = splutil.CreateResource(c, ¤t) + cr.Spec.ServiceAccount = "defaults" + test(`{"kind":"StatefulSet","apiVersion":"apps/v1","metadata":{"name":"splunk-stack1-search-head","namespace":"test","creationTimestamp":null,"ownerReferences":[{"apiVersion":"","kind":"","name":"stack1","uid":"","controller":true}]},"spec":{"replicas":6,"selector":{"matchLabels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-search-head","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"search-head","app.kubernetes.io/part-of":"splunk-stack1-search-head"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-search-head","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"search-head","app.kubernetes.io/part-of":"splunk-stack1-search-head"},"annotations":{"traffic.sidecar.istio.io/excludeOutboundPorts":"8089,8191,9997,7777,9000,17000,17500,19000","traffic.sidecar.istio.io/includeInboundPorts":"8000"}},"spec":{"volumes":[{"name":"mnt-splunk-secrets","secret":{"secretName":"splunk-stack1-search-head-secret-v1","defaultMode":420}},{"name":"mnt-splunk-jdk","emptyDir":{}},{"name":"mnt-splunk-spark","emptyDir":{}}],"initContainers":[{"name":"init","image":"splunk/spark","command":["bash","-c","cp -r /opt/jdk /mnt \u0026\u0026 cp -r /opt/spark /mnt"],"resources":{"limits":{"cpu":"1","memory":"512Mi"},"requests":{"cpu":"250m","memory":"128Mi"}},"volumeMounts":[{"name":"mnt-splunk-jdk","mountPath":"/mnt/jdk"},{"name":"mnt-splunk-spark","mountPath":"/mnt/spark"}],"imagePullPolicy":"IfNotPresent"}],"containers":[{"name":"splunk","image":"splunk/splunk","ports":[{"name":"splunkweb","containerPort":8000,"protocol":"TCP"},{"name":"splunkd","containerPort":8089,"protocol":"TCP"},{"name":"dfsmaster","containerPort":9000,"protocol":"TCP"},{"name":"dfccontrol","containerPort":17000,"protocol":"TCP"},{"name":"datareceive","containerPort":19000,"protocol":"TCP"},{"name":"user-defined","containerPort":32000,"protocol":"UDP"}],"env":[{"name":"SPLUNK_HOME","value":"/opt/splunk"},{"name":"SPLUNK_START_ARGS","value":"--accept-license"},{"name":"SPLUNK_DEFAULTS_URL","value":"/mnt/splunk-secrets/default.yml"},{"name":"SPLUNK_HOME_OWNERSHIP_ENFORCEMENT","value":"false"},{"name":"SPLUNK_ROLE","value":"splunk_search_head"},{"name":"SPLUNK_DECLARATIVE_ADMIN_PASSWORD","value":"true"},{"name":"SPLUNK_SEARCH_HEAD_URL","value":"splunk-stack1-search-head-0.splunk-stack1-search-head-headless.test.svc.cluster.local,splunk-stack1-search-head-1.splunk-stack1-search-head-headless.test.svc.cluster.local,splunk-stack1-search-head-2.splunk-stack1-search-head-headless.test.svc.cluster.local,splunk-stack1-search-head-3.splunk-stack1-search-head-headless.test.svc.cluster.local,splunk-stack1-search-head-4.splunk-stack1-search-head-headless.test.svc.cluster.local,splunk-stack1-search-head-5.splunk-stack1-search-head-headless.test.svc.cluster.local"},{"name":"SPLUNK_SEARCH_HEAD_CAPTAIN_URL","value":"splunk-stack1-search-head-0.splunk-stack1-search-head-headless.test.svc.cluster.local"},{"name":"SPLUNK_DEPLOYER_URL","value":"splunk-stack1-deployer-service"},{"name":"SPLUNK_CLUSTER_MASTER_URL","value":"splunk-stack1-cluster-master-service.test2.svc.cluster.local"},{"name":"SPLUNK_ENABLE_DFS","value":"true"},{"name":"SPARK_MASTER_HOST","value":"splunk-stack1-spark-master-service"},{"name":"SPARK_MASTER_WEBUI_PORT","value":"8009"},{"name":"SPARK_HOME","value":"/mnt/splunk-spark"},{"name":"JAVA_HOME","value":"/mnt/splunk-jdk"},{"name":"SPLUNK_DFW_NUM_SLOTS_ENABLED","value":"true"}],"resources":{"limits":{"cpu":"4","memory":"8Gi"},"requests":{"cpu":"100m","memory":"512Mi"}},"volumeMounts":[{"name":"pvc-etc","mountPath":"/opt/splunk/etc"},{"name":"pvc-var","mountPath":"/opt/splunk/var"},{"name":"mnt-splunk-secrets","mountPath":"/mnt/splunk-secrets"},{"name":"mnt-splunk-jdk","mountPath":"/mnt/splunk-jdk"},{"name":"mnt-splunk-spark","mountPath":"/mnt/splunk-spark"}],"livenessProbe":{"exec":{"command":["/sbin/checkstate.sh"]},"initialDelaySeconds":300,"timeoutSeconds":30,"periodSeconds":30},"readinessProbe":{"exec":{"command":["/bin/grep","started","/opt/container_artifact/splunk-container.state"]},"initialDelaySeconds":10,"timeoutSeconds":5,"periodSeconds":5},"imagePullPolicy":"IfNotPresent"}],"serviceAccountName":"defaults","securityContext":{"runAsUser":41812,"fsGroup":41812},"affinity":{"podAntiAffinity":{"preferredDuringSchedulingIgnoredDuringExecution":[{"weight":100,"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app.kubernetes.io/instance","operator":"In","values":["splunk-stack1-search-head"]}]},"topologyKey":"kubernetes.io/hostname"}}]}},"schedulerName":"default-scheduler"}},"volumeClaimTemplates":[{"metadata":{"name":"pvc-etc","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-search-head","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"search-head","app.kubernetes.io/part-of":"splunk-stack1-search-head"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"10Gi"}}},"status":{}},{"metadata":{"name":"pvc-var","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-search-head","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"search-head","app.kubernetes.io/part-of":"splunk-stack1-search-head"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"100Gi"}}},"status":{}}],"serviceName":"splunk-stack1-search-head-headless","podManagementPolicy":"Parallel","updateStrategy":{"type":"OnDelete"}},"status":{"replicas":0}}`) } func TestGetDeployerStatefulSet(t *testing.T) { @@ -598,4 +609,14 @@ func TestGetDeployerStatefulSet(t *testing.T) { cr.Spec.DefaultsURLApps = "/mnt/apps/apps.yml" test(`{"kind":"StatefulSet","apiVersion":"apps/v1","metadata":{"name":"splunk-stack1-deployer","namespace":"test","creationTimestamp":null,"ownerReferences":[{"apiVersion":"","kind":"","name":"stack1","uid":"","controller":true}]},"spec":{"replicas":1,"selector":{"matchLabels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-deployer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"deployer","app.kubernetes.io/part-of":"splunk-stack1-search-head"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-deployer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"deployer","app.kubernetes.io/part-of":"splunk-stack1-search-head"},"annotations":{"traffic.sidecar.istio.io/excludeOutboundPorts":"8089,8191,9997,7777,9000,17000,17500,19000","traffic.sidecar.istio.io/includeInboundPorts":"8000"}},"spec":{"volumes":[{"name":"mnt-splunk-secrets","secret":{"secretName":"splunk-stack1-deployer-secret-v1","defaultMode":420}}],"containers":[{"name":"splunk","image":"splunk/splunk","ports":[{"name":"splunkweb","containerPort":8000,"protocol":"TCP"},{"name":"splunkd","containerPort":8089,"protocol":"TCP"}],"env":[{"name":"SPLUNK_HOME","value":"/opt/splunk"},{"name":"SPLUNK_START_ARGS","value":"--accept-license"},{"name":"SPLUNK_DEFAULTS_URL","value":"/mnt/apps/apps.yml,/mnt/splunk-secrets/default.yml"},{"name":"SPLUNK_HOME_OWNERSHIP_ENFORCEMENT","value":"false"},{"name":"SPLUNK_ROLE","value":"splunk_deployer"},{"name":"SPLUNK_DECLARATIVE_ADMIN_PASSWORD","value":"true"},{"name":"SPLUNK_SEARCH_HEAD_URL","value":"splunk-stack1-search-head-0.splunk-stack1-search-head-headless.test.svc.cluster.local,splunk-stack1-search-head-1.splunk-stack1-search-head-headless.test.svc.cluster.local,splunk-stack1-search-head-2.splunk-stack1-search-head-headless.test.svc.cluster.local"},{"name":"SPLUNK_SEARCH_HEAD_CAPTAIN_URL","value":"splunk-stack1-search-head-0.splunk-stack1-search-head-headless.test.svc.cluster.local"}],"resources":{"limits":{"cpu":"4","memory":"8Gi"},"requests":{"cpu":"100m","memory":"512Mi"}},"volumeMounts":[{"name":"pvc-etc","mountPath":"/opt/splunk/etc"},{"name":"pvc-var","mountPath":"/opt/splunk/var"},{"name":"mnt-splunk-secrets","mountPath":"/mnt/splunk-secrets"}],"livenessProbe":{"exec":{"command":["/sbin/checkstate.sh"]},"initialDelaySeconds":300,"timeoutSeconds":30,"periodSeconds":30},"readinessProbe":{"exec":{"command":["/bin/grep","started","/opt/container_artifact/splunk-container.state"]},"initialDelaySeconds":10,"timeoutSeconds":5,"periodSeconds":5},"imagePullPolicy":"IfNotPresent"}],"securityContext":{"runAsUser":41812,"fsGroup":41812},"affinity":{"podAntiAffinity":{"preferredDuringSchedulingIgnoredDuringExecution":[{"weight":100,"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app.kubernetes.io/instance","operator":"In","values":["splunk-stack1-deployer"]}]},"topologyKey":"kubernetes.io/hostname"}}]}},"schedulerName":"default-scheduler"}},"volumeClaimTemplates":[{"metadata":{"name":"pvc-etc","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-deployer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"deployer","app.kubernetes.io/part-of":"splunk-stack1-search-head"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"10Gi"}}},"status":{}},{"metadata":{"name":"pvc-var","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-deployer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"deployer","app.kubernetes.io/part-of":"splunk-stack1-search-head"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"100Gi"}}},"status":{}}],"serviceName":"splunk-stack1-deployer-headless","podManagementPolicy":"Parallel","updateStrategy":{"type":"OnDelete"}},"status":{"replicas":0}}`) + // Create a serviceaccount + current := corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: "defaults", + Namespace: "test", + }, + } + _ = splutil.CreateResource(c, ¤t) + cr.Spec.ServiceAccount = "defaults" + test(`{"kind":"StatefulSet","apiVersion":"apps/v1","metadata":{"name":"splunk-stack1-deployer","namespace":"test","creationTimestamp":null,"ownerReferences":[{"apiVersion":"","kind":"","name":"stack1","uid":"","controller":true}]},"spec":{"replicas":1,"selector":{"matchLabels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-deployer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"deployer","app.kubernetes.io/part-of":"splunk-stack1-search-head"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-deployer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"deployer","app.kubernetes.io/part-of":"splunk-stack1-search-head"},"annotations":{"traffic.sidecar.istio.io/excludeOutboundPorts":"8089,8191,9997,7777,9000,17000,17500,19000","traffic.sidecar.istio.io/includeInboundPorts":"8000"}},"spec":{"volumes":[{"name":"mnt-splunk-secrets","secret":{"secretName":"splunk-stack1-deployer-secret-v1","defaultMode":420}}],"containers":[{"name":"splunk","image":"splunk/splunk","ports":[{"name":"splunkweb","containerPort":8000,"protocol":"TCP"},{"name":"splunkd","containerPort":8089,"protocol":"TCP"}],"env":[{"name":"SPLUNK_HOME","value":"/opt/splunk"},{"name":"SPLUNK_START_ARGS","value":"--accept-license"},{"name":"SPLUNK_DEFAULTS_URL","value":"/mnt/apps/apps.yml,/mnt/splunk-secrets/default.yml"},{"name":"SPLUNK_HOME_OWNERSHIP_ENFORCEMENT","value":"false"},{"name":"SPLUNK_ROLE","value":"splunk_deployer"},{"name":"SPLUNK_DECLARATIVE_ADMIN_PASSWORD","value":"true"},{"name":"SPLUNK_SEARCH_HEAD_URL","value":"splunk-stack1-search-head-0.splunk-stack1-search-head-headless.test.svc.cluster.local,splunk-stack1-search-head-1.splunk-stack1-search-head-headless.test.svc.cluster.local,splunk-stack1-search-head-2.splunk-stack1-search-head-headless.test.svc.cluster.local"},{"name":"SPLUNK_SEARCH_HEAD_CAPTAIN_URL","value":"splunk-stack1-search-head-0.splunk-stack1-search-head-headless.test.svc.cluster.local"}],"resources":{"limits":{"cpu":"4","memory":"8Gi"},"requests":{"cpu":"100m","memory":"512Mi"}},"volumeMounts":[{"name":"pvc-etc","mountPath":"/opt/splunk/etc"},{"name":"pvc-var","mountPath":"/opt/splunk/var"},{"name":"mnt-splunk-secrets","mountPath":"/mnt/splunk-secrets"}],"livenessProbe":{"exec":{"command":["/sbin/checkstate.sh"]},"initialDelaySeconds":300,"timeoutSeconds":30,"periodSeconds":30},"readinessProbe":{"exec":{"command":["/bin/grep","started","/opt/container_artifact/splunk-container.state"]},"initialDelaySeconds":10,"timeoutSeconds":5,"periodSeconds":5},"imagePullPolicy":"IfNotPresent"}],"serviceAccountName":"defaults","securityContext":{"runAsUser":41812,"fsGroup":41812},"affinity":{"podAntiAffinity":{"preferredDuringSchedulingIgnoredDuringExecution":[{"weight":100,"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app.kubernetes.io/instance","operator":"In","values":["splunk-stack1-deployer"]}]},"topologyKey":"kubernetes.io/hostname"}}]}},"schedulerName":"default-scheduler"}},"volumeClaimTemplates":[{"metadata":{"name":"pvc-etc","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-deployer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"deployer","app.kubernetes.io/part-of":"splunk-stack1-search-head"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"10Gi"}}},"status":{}},{"metadata":{"name":"pvc-var","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-deployer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"deployer","app.kubernetes.io/part-of":"splunk-stack1-search-head"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"100Gi"}}},"status":{}}],"serviceName":"splunk-stack1-deployer-headless","podManagementPolicy":"Parallel","updateStrategy":{"type":"OnDelete"}},"status":{"replicas":0}}`) } diff --git a/pkg/splunk/enterprise/standalone_test.go b/pkg/splunk/enterprise/standalone_test.go index ba2a4e57d..d11a0aebe 100644 --- a/pkg/splunk/enterprise/standalone_test.go +++ b/pkg/splunk/enterprise/standalone_test.go @@ -219,6 +219,17 @@ func TestGetStandaloneStatefulSet(t *testing.T) { cr.Spec.DefaultsURLApps = "/mnt/apps/apps.yml" test(`{"kind":"StatefulSet","apiVersion":"apps/v1","metadata":{"name":"splunk-stack1-standalone","namespace":"test","creationTimestamp":null,"ownerReferences":[{"apiVersion":"","kind":"","name":"stack1","uid":"","controller":true}]},"spec":{"replicas":1,"selector":{"matchLabels":{"app.kubernetes.io/component":"standalone","app.kubernetes.io/instance":"splunk-stack1-standalone","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"standalone","app.kubernetes.io/part-of":"splunk-stack1-standalone"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app.kubernetes.io/component":"standalone","app.kubernetes.io/instance":"splunk-stack1-standalone","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"standalone","app.kubernetes.io/part-of":"splunk-stack1-standalone"},"annotations":{"traffic.sidecar.istio.io/excludeOutboundPorts":"8089,8191,9997,7777,9000,17000,17500,19000","traffic.sidecar.istio.io/includeInboundPorts":"8000,8088"}},"spec":{"volumes":[{"name":"defaults"},{"name":"mnt-splunk-secrets","secret":{"secretName":"splunk-stack1-standalone-secret-v1","defaultMode":420}},{"name":"mnt-splunk-defaults","configMap":{"name":"splunk-stack1-standalone-defaults","defaultMode":420}},{"name":"mnt-splunk-jdk","emptyDir":{}},{"name":"mnt-splunk-spark","emptyDir":{}}],"initContainers":[{"name":"init","image":"splunk/spark","command":["bash","-c","cp -r /opt/jdk /mnt \u0026\u0026 cp -r /opt/spark /mnt"],"resources":{"limits":{"cpu":"1","memory":"512Mi"},"requests":{"cpu":"250m","memory":"128Mi"}},"volumeMounts":[{"name":"mnt-splunk-jdk","mountPath":"/mnt/jdk"},{"name":"mnt-splunk-spark","mountPath":"/mnt/spark"}],"imagePullPolicy":"IfNotPresent"}],"containers":[{"name":"splunk","image":"splunk/splunk","ports":[{"name":"splunkweb","containerPort":8000,"protocol":"TCP"},{"name":"hec","containerPort":8088,"protocol":"TCP"},{"name":"splunkd","containerPort":8089,"protocol":"TCP"},{"name":"dfsmaster","containerPort":9000,"protocol":"TCP"},{"name":"s2s","containerPort":9997,"protocol":"TCP"},{"name":"dfccontrol","containerPort":17000,"protocol":"TCP"},{"name":"datareceive","containerPort":19000,"protocol":"TCP"}],"env":[{"name":"SPLUNK_HOME","value":"/opt/splunk"},{"name":"SPLUNK_START_ARGS","value":"--accept-license"},{"name":"SPLUNK_DEFAULTS_URL","value":"/mnt/splunk-defaults/default.yml,/mnt/defaults/defaults.yml,/mnt/apps/apps.yml,/mnt/splunk-secrets/default.yml"},{"name":"SPLUNK_HOME_OWNERSHIP_ENFORCEMENT","value":"false"},{"name":"SPLUNK_ROLE","value":"splunk_search_head"},{"name":"SPLUNK_DECLARATIVE_ADMIN_PASSWORD","value":"true"},{"name":"SPLUNK_CLUSTER_MASTER_URL","value":"splunk-stack2-cluster-master-service"},{"name":"SPLUNK_ENABLE_DFS","value":"true"},{"name":"SPARK_MASTER_HOST","value":"splunk-stack1-spark-master-service"},{"name":"SPARK_MASTER_WEBUI_PORT","value":"8009"},{"name":"SPARK_HOME","value":"/mnt/splunk-spark"},{"name":"JAVA_HOME","value":"/mnt/splunk-jdk"},{"name":"SPLUNK_DFW_NUM_SLOTS_ENABLED","value":"false"}],"resources":{"limits":{"cpu":"4","memory":"8Gi"},"requests":{"cpu":"100m","memory":"512Mi"}},"volumeMounts":[{"name":"pvc-etc","mountPath":"/opt/splunk/etc"},{"name":"pvc-var","mountPath":"/opt/splunk/var"},{"name":"defaults","mountPath":"/mnt/defaults"},{"name":"mnt-splunk-secrets","mountPath":"/mnt/splunk-secrets"},{"name":"mnt-splunk-defaults","mountPath":"/mnt/splunk-defaults"},{"name":"mnt-splunk-jdk","mountPath":"/mnt/splunk-jdk"},{"name":"mnt-splunk-spark","mountPath":"/mnt/splunk-spark"}],"livenessProbe":{"exec":{"command":["/sbin/checkstate.sh"]},"initialDelaySeconds":300,"timeoutSeconds":30,"periodSeconds":30},"readinessProbe":{"exec":{"command":["/bin/grep","started","/opt/container_artifact/splunk-container.state"]},"initialDelaySeconds":10,"timeoutSeconds":5,"periodSeconds":5},"imagePullPolicy":"IfNotPresent"}],"securityContext":{"runAsUser":41812,"fsGroup":41812},"affinity":{"podAntiAffinity":{"preferredDuringSchedulingIgnoredDuringExecution":[{"weight":100,"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app.kubernetes.io/instance","operator":"In","values":["splunk-stack1-standalone"]}]},"topologyKey":"kubernetes.io/hostname"}}]}},"schedulerName":"custom-scheduler"}},"volumeClaimTemplates":[{"metadata":{"name":"pvc-etc","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"standalone","app.kubernetes.io/instance":"splunk-stack1-standalone","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"standalone","app.kubernetes.io/part-of":"splunk-stack1-standalone"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"10Gi"}},"storageClassName":"gp2"},"status":{}},{"metadata":{"name":"pvc-var","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"standalone","app.kubernetes.io/instance":"splunk-stack1-standalone","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"standalone","app.kubernetes.io/part-of":"splunk-stack1-standalone"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"100Gi"}},"storageClassName":"gp2"},"status":{}}],"serviceName":"splunk-stack1-standalone-headless","podManagementPolicy":"Parallel","updateStrategy":{"type":"OnDelete"}},"status":{"replicas":0}}`) + + // Create a serviceaccount + current := corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: "defaults", + Namespace: "test", + }, + } + _ = splutil.CreateResource(c, ¤t) + cr.Spec.ServiceAccount = "defaults" + test(`{"kind":"StatefulSet","apiVersion":"apps/v1","metadata":{"name":"splunk-stack1-standalone","namespace":"test","creationTimestamp":null,"ownerReferences":[{"apiVersion":"","kind":"","name":"stack1","uid":"","controller":true}]},"spec":{"replicas":1,"selector":{"matchLabels":{"app.kubernetes.io/component":"standalone","app.kubernetes.io/instance":"splunk-stack1-standalone","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"standalone","app.kubernetes.io/part-of":"splunk-stack1-standalone"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app.kubernetes.io/component":"standalone","app.kubernetes.io/instance":"splunk-stack1-standalone","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"standalone","app.kubernetes.io/part-of":"splunk-stack1-standalone"},"annotations":{"traffic.sidecar.istio.io/excludeOutboundPorts":"8089,8191,9997,7777,9000,17000,17500,19000","traffic.sidecar.istio.io/includeInboundPorts":"8000,8088"}},"spec":{"volumes":[{"name":"defaults"},{"name":"mnt-splunk-secrets","secret":{"secretName":"splunk-stack1-standalone-secret-v1","defaultMode":420}},{"name":"mnt-splunk-defaults","configMap":{"name":"splunk-stack1-standalone-defaults","defaultMode":420}},{"name":"mnt-splunk-jdk","emptyDir":{}},{"name":"mnt-splunk-spark","emptyDir":{}}],"initContainers":[{"name":"init","image":"splunk/spark","command":["bash","-c","cp -r /opt/jdk /mnt \u0026\u0026 cp -r /opt/spark /mnt"],"resources":{"limits":{"cpu":"1","memory":"512Mi"},"requests":{"cpu":"250m","memory":"128Mi"}},"volumeMounts":[{"name":"mnt-splunk-jdk","mountPath":"/mnt/jdk"},{"name":"mnt-splunk-spark","mountPath":"/mnt/spark"}],"imagePullPolicy":"IfNotPresent"}],"containers":[{"name":"splunk","image":"splunk/splunk","ports":[{"name":"splunkweb","containerPort":8000,"protocol":"TCP"},{"name":"hec","containerPort":8088,"protocol":"TCP"},{"name":"splunkd","containerPort":8089,"protocol":"TCP"},{"name":"dfsmaster","containerPort":9000,"protocol":"TCP"},{"name":"s2s","containerPort":9997,"protocol":"TCP"},{"name":"dfccontrol","containerPort":17000,"protocol":"TCP"},{"name":"datareceive","containerPort":19000,"protocol":"TCP"}],"env":[{"name":"SPLUNK_HOME","value":"/opt/splunk"},{"name":"SPLUNK_START_ARGS","value":"--accept-license"},{"name":"SPLUNK_DEFAULTS_URL","value":"/mnt/splunk-defaults/default.yml,/mnt/defaults/defaults.yml,/mnt/apps/apps.yml,/mnt/splunk-secrets/default.yml"},{"name":"SPLUNK_HOME_OWNERSHIP_ENFORCEMENT","value":"false"},{"name":"SPLUNK_ROLE","value":"splunk_search_head"},{"name":"SPLUNK_DECLARATIVE_ADMIN_PASSWORD","value":"true"},{"name":"SPLUNK_CLUSTER_MASTER_URL","value":"splunk-stack2-cluster-master-service"},{"name":"SPLUNK_ENABLE_DFS","value":"true"},{"name":"SPARK_MASTER_HOST","value":"splunk-stack1-spark-master-service"},{"name":"SPARK_MASTER_WEBUI_PORT","value":"8009"},{"name":"SPARK_HOME","value":"/mnt/splunk-spark"},{"name":"JAVA_HOME","value":"/mnt/splunk-jdk"},{"name":"SPLUNK_DFW_NUM_SLOTS_ENABLED","value":"false"}],"resources":{"limits":{"cpu":"4","memory":"8Gi"},"requests":{"cpu":"100m","memory":"512Mi"}},"volumeMounts":[{"name":"pvc-etc","mountPath":"/opt/splunk/etc"},{"name":"pvc-var","mountPath":"/opt/splunk/var"},{"name":"defaults","mountPath":"/mnt/defaults"},{"name":"mnt-splunk-secrets","mountPath":"/mnt/splunk-secrets"},{"name":"mnt-splunk-defaults","mountPath":"/mnt/splunk-defaults"},{"name":"mnt-splunk-jdk","mountPath":"/mnt/splunk-jdk"},{"name":"mnt-splunk-spark","mountPath":"/mnt/splunk-spark"}],"livenessProbe":{"exec":{"command":["/sbin/checkstate.sh"]},"initialDelaySeconds":300,"timeoutSeconds":30,"periodSeconds":30},"readinessProbe":{"exec":{"command":["/bin/grep","started","/opt/container_artifact/splunk-container.state"]},"initialDelaySeconds":10,"timeoutSeconds":5,"periodSeconds":5},"imagePullPolicy":"IfNotPresent"}],"serviceAccountName":"defaults","securityContext":{"runAsUser":41812,"fsGroup":41812},"affinity":{"podAntiAffinity":{"preferredDuringSchedulingIgnoredDuringExecution":[{"weight":100,"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app.kubernetes.io/instance","operator":"In","values":["splunk-stack1-standalone"]}]},"topologyKey":"kubernetes.io/hostname"}}]}},"schedulerName":"custom-scheduler"}},"volumeClaimTemplates":[{"metadata":{"name":"pvc-etc","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"standalone","app.kubernetes.io/instance":"splunk-stack1-standalone","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"standalone","app.kubernetes.io/part-of":"splunk-stack1-standalone"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"10Gi"}},"storageClassName":"gp2"},"status":{}},{"metadata":{"name":"pvc-var","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"standalone","app.kubernetes.io/instance":"splunk-stack1-standalone","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"standalone","app.kubernetes.io/part-of":"splunk-stack1-standalone"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"100Gi"}},"storageClassName":"gp2"},"status":{}}],"serviceName":"splunk-stack1-standalone-headless","podManagementPolicy":"Parallel","updateStrategy":{"type":"OnDelete"}},"status":{"replicas":0}}`) } func TestApplyStandaloneSmartstoreKeyChangeDetection(t *testing.T) { diff --git a/pkg/splunk/test/controller.go b/pkg/splunk/test/controller.go index 099177361..99b5149b0 100644 --- a/pkg/splunk/test/controller.go +++ b/pkg/splunk/test/controller.go @@ -85,6 +85,8 @@ func coreObjectCopier(dst, src *runtime.Object) bool { *dstP.(*corev1.Service) = *srcP.(*corev1.Service) case *corev1.Pod: *dstP.(*corev1.Pod) = *srcP.(*corev1.Pod) + case *corev1.ServiceAccount: + *dstP.(*corev1.ServiceAccount) = *srcP.(*corev1.ServiceAccount) default: return false } From d31f937dfd828c30fad1885751199ea1eafb254f Mon Sep 17 00:00:00 2001 From: akondur Date: Fri, 8 Jan 2021 15:18:42 -0800 Subject: [PATCH 15/43] Addressing review comments --- docs/CustomResources.md | 6 +++--- docs/Examples.md | 8 ++++++-- docs/StorageClass.md | 2 +- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/docs/CustomResources.md b/docs/CustomResources.md index 2e30ea614..e8eb8bbb6 100644 --- a/docs/CustomResources.md +++ b/docs/CustomResources.md @@ -92,7 +92,7 @@ spec: storageClassName: gp2 storageCapacity: 15Gi varVolumeStorageConfig: - storageClassName: gp2 + storageClassName: customStorageClass storageCapacity: 25Gi volumes: - name: licenses @@ -110,8 +110,8 @@ Enterprise resources, including: `Standalone`, `LicenseMaster`, | Key | Type | Description | | ------------------ | ------- | ----------------------------------------------------------------------------- | -| etcVolumeStorageConfig | StorageClassSpec | Storage class spec for Splunk etc volume as described in [StorageClass](StorageClass.md) | -| varVolumeStorageConfig | StorageClassSpec | Storage class spec for Splunk var volume as described in [StorageClass](StorageClass.md) | +| etcVolumeStorageConfig | StorageClassSpec | Storage class spec for Splunk etc volume as described in [StorageClass](StorageClass.md) | +| varVolumeStorageConfig | StorageClassSpec | Storage class spec for Splunk var volume as described in [StorageClass](StorageClass.md) | | volumes | [[]Volume](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.17/#volume-v1-core) | List of one or more [Kubernetes volumes](https://kubernetes.io/docs/concepts/storage/volumes/). These will be mounted in all container pods as as `/mnt/` | | defaults | string | Inline map of [default.yml](https://github.com/splunk/splunk-ansible/blob/develop/docs/advanced/default.yml.spec.md) overrides used to initialize the environment | | defaultsUrl | string | Full path or URL for one or more [default.yml](https://github.com/splunk/splunk-ansible/blob/develop/docs/advanced/default.yml.spec.md) files, separated by commas | diff --git a/docs/Examples.md b/docs/Examples.md index 7aed57522..2f173a3d9 100644 --- a/docs/Examples.md +++ b/docs/Examples.md @@ -206,8 +206,12 @@ metadata: finalizers: - enterprise.splunk.com/delete-pvc spec: - storageClassName: standard - varStorage: "4Gi" + etcVolumeStorageConfig: + storageClassName: gp2 + storageCapacity: 15Gi + varVolumeStorageConfig: + storageClassName: customStorageClass + storageCapacity: 25Gi --- apiVersion: enterprise.splunk.com/v1beta1 kind: IndexerCluster diff --git a/docs/StorageClass.md b/docs/StorageClass.md index b265893ef..3dad417a8 100644 --- a/docs/StorageClass.md +++ b/docs/StorageClass.md @@ -29,7 +29,7 @@ spec: storageClassName: gp2 storageCapacity: 15Gi varVolumeStorageConfig: - storageClassName: gp2 + storageClassName: customStorageClass storageCapacity: 25Gi ``` The following `kubectl` command can be used to see which Storage Classes are available in From b0dedf0759005f8916679e60b986f5ea6b707e10 Mon Sep 17 00:00:00 2001 From: Victor Ebken Date: Fri, 8 Jan 2021 15:02:55 -0800 Subject: [PATCH 16/43] CSPL-637 need a naming specification for ports for istio When using istio ingress, istio uses a port naming convention "[-]" to help determine port protocol. This PR updates our port names to conform with this convention. --- pkg/splunk/enterprise/clustermaster_test.go | 6 ++-- pkg/splunk/enterprise/configuration.go | 28 +++++++++---------- pkg/splunk/enterprise/configuration_test.go | 12 ++++---- pkg/splunk/enterprise/indexercluster_test.go | 6 ++-- pkg/splunk/enterprise/licensemaster_test.go | 4 +-- .../enterprise/searchheadcluster_test.go | 12 ++++---- pkg/splunk/enterprise/standalone_test.go | 8 +++--- 7 files changed, 38 insertions(+), 38 deletions(-) diff --git a/pkg/splunk/enterprise/clustermaster_test.go b/pkg/splunk/enterprise/clustermaster_test.go index 2a8d377e7..5849be074 100644 --- a/pkg/splunk/enterprise/clustermaster_test.go +++ b/pkg/splunk/enterprise/clustermaster_test.go @@ -115,15 +115,15 @@ func TestGetClusterMasterStatefulSet(t *testing.T) { configTester(t, fmt.Sprintf("getClusterMasterStatefulSet"), f, want) } - test(`{"kind":"StatefulSet","apiVersion":"apps/v1","metadata":{"name":"splunk-stack1-cluster-master","namespace":"test","creationTimestamp":null,"ownerReferences":[{"apiVersion":"","kind":"","name":"stack1","uid":"","controller":true}]},"spec":{"replicas":1,"selector":{"matchLabels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-cluster-master","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"cluster-master","app.kubernetes.io/part-of":"splunk-stack1-indexer"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-cluster-master","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"cluster-master","app.kubernetes.io/part-of":"splunk-stack1-indexer"},"annotations":{"traffic.sidecar.istio.io/excludeOutboundPorts":"8089,8191,9997,7777,9000,17000,17500,19000","traffic.sidecar.istio.io/includeInboundPorts":"8000"}},"spec":{"volumes":[{"name":"mnt-splunk-secrets","secret":{"secretName":"splunk-stack1-cluster-master-secret-v1","defaultMode":420}}],"containers":[{"name":"splunk","image":"splunk/splunk","ports":[{"name":"splunkweb","containerPort":8000,"protocol":"TCP"},{"name":"splunkd","containerPort":8089,"protocol":"TCP"}],"env":[{"name":"SPLUNK_HOME","value":"/opt/splunk"},{"name":"SPLUNK_START_ARGS","value":"--accept-license"},{"name":"SPLUNK_DEFAULTS_URL","value":"/mnt/splunk-secrets/default.yml"},{"name":"SPLUNK_HOME_OWNERSHIP_ENFORCEMENT","value":"false"},{"name":"SPLUNK_ROLE","value":"splunk_cluster_master"},{"name":"SPLUNK_DECLARATIVE_ADMIN_PASSWORD","value":"true"},{"name":"SPLUNK_CLUSTER_MASTER_URL","value":"localhost"}],"resources":{"limits":{"cpu":"4","memory":"8Gi"},"requests":{"cpu":"100m","memory":"512Mi"}},"volumeMounts":[{"name":"pvc-etc","mountPath":"/opt/splunk/etc"},{"name":"pvc-var","mountPath":"/opt/splunk/var"},{"name":"mnt-splunk-secrets","mountPath":"/mnt/splunk-secrets"}],"livenessProbe":{"exec":{"command":["/sbin/checkstate.sh"]},"initialDelaySeconds":300,"timeoutSeconds":30,"periodSeconds":30},"readinessProbe":{"exec":{"command":["/bin/grep","started","/opt/container_artifact/splunk-container.state"]},"initialDelaySeconds":10,"timeoutSeconds":5,"periodSeconds":5},"imagePullPolicy":"IfNotPresent"}],"securityContext":{"runAsUser":41812,"fsGroup":41812},"affinity":{"podAntiAffinity":{"preferredDuringSchedulingIgnoredDuringExecution":[{"weight":100,"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app.kubernetes.io/instance","operator":"In","values":["splunk-stack1-cluster-master"]}]},"topologyKey":"kubernetes.io/hostname"}}]}},"schedulerName":"default-scheduler"}},"volumeClaimTemplates":[{"metadata":{"name":"pvc-etc","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-cluster-master","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"cluster-master","app.kubernetes.io/part-of":"splunk-stack1-indexer"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"10Gi"}}},"status":{}},{"metadata":{"name":"pvc-var","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-cluster-master","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"cluster-master","app.kubernetes.io/part-of":"splunk-stack1-indexer"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"100Gi"}}},"status":{}}],"serviceName":"splunk-stack1-cluster-master-headless","podManagementPolicy":"Parallel","updateStrategy":{"type":"OnDelete"}},"status":{"replicas":0}}`) + test(`{"kind":"StatefulSet","apiVersion":"apps/v1","metadata":{"name":"splunk-stack1-cluster-master","namespace":"test","creationTimestamp":null,"ownerReferences":[{"apiVersion":"","kind":"","name":"stack1","uid":"","controller":true}]},"spec":{"replicas":1,"selector":{"matchLabels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-cluster-master","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"cluster-master","app.kubernetes.io/part-of":"splunk-stack1-indexer"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-cluster-master","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"cluster-master","app.kubernetes.io/part-of":"splunk-stack1-indexer"},"annotations":{"traffic.sidecar.istio.io/excludeOutboundPorts":"8089,8191,9997,7777,9000,17000,17500,19000","traffic.sidecar.istio.io/includeInboundPorts":"8000"}},"spec":{"volumes":[{"name":"mnt-splunk-secrets","secret":{"secretName":"splunk-stack1-cluster-master-secret-v1","defaultMode":420}}],"containers":[{"name":"splunk","image":"splunk/splunk","ports":[{"name":"http-splunkweb","containerPort":8000,"protocol":"TCP"},{"name":"https-splunkd","containerPort":8089,"protocol":"TCP"}],"env":[{"name":"SPLUNK_HOME","value":"/opt/splunk"},{"name":"SPLUNK_START_ARGS","value":"--accept-license"},{"name":"SPLUNK_DEFAULTS_URL","value":"/mnt/splunk-secrets/default.yml"},{"name":"SPLUNK_HOME_OWNERSHIP_ENFORCEMENT","value":"false"},{"name":"SPLUNK_ROLE","value":"splunk_cluster_master"},{"name":"SPLUNK_DECLARATIVE_ADMIN_PASSWORD","value":"true"},{"name":"SPLUNK_CLUSTER_MASTER_URL","value":"localhost"}],"resources":{"limits":{"cpu":"4","memory":"8Gi"},"requests":{"cpu":"100m","memory":"512Mi"}},"volumeMounts":[{"name":"pvc-etc","mountPath":"/opt/splunk/etc"},{"name":"pvc-var","mountPath":"/opt/splunk/var"},{"name":"mnt-splunk-secrets","mountPath":"/mnt/splunk-secrets"}],"livenessProbe":{"exec":{"command":["/sbin/checkstate.sh"]},"initialDelaySeconds":300,"timeoutSeconds":30,"periodSeconds":30},"readinessProbe":{"exec":{"command":["/bin/grep","started","/opt/container_artifact/splunk-container.state"]},"initialDelaySeconds":10,"timeoutSeconds":5,"periodSeconds":5},"imagePullPolicy":"IfNotPresent"}],"securityContext":{"runAsUser":41812,"fsGroup":41812},"affinity":{"podAntiAffinity":{"preferredDuringSchedulingIgnoredDuringExecution":[{"weight":100,"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app.kubernetes.io/instance","operator":"In","values":["splunk-stack1-cluster-master"]}]},"topologyKey":"kubernetes.io/hostname"}}]}},"schedulerName":"default-scheduler"}},"volumeClaimTemplates":[{"metadata":{"name":"pvc-etc","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-cluster-master","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"cluster-master","app.kubernetes.io/part-of":"splunk-stack1-indexer"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"10Gi"}}},"status":{}},{"metadata":{"name":"pvc-var","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-cluster-master","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"cluster-master","app.kubernetes.io/part-of":"splunk-stack1-indexer"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"100Gi"}}},"status":{}}],"serviceName":"splunk-stack1-cluster-master-headless","podManagementPolicy":"Parallel","updateStrategy":{"type":"OnDelete"}},"status":{"replicas":0}}`) cr.Spec.LicenseMasterRef.Name = "stack1" cr.Spec.LicenseMasterRef.Namespace = "test" - test(`{"kind":"StatefulSet","apiVersion":"apps/v1","metadata":{"name":"splunk-stack1-cluster-master","namespace":"test","creationTimestamp":null,"ownerReferences":[{"apiVersion":"","kind":"","name":"stack1","uid":"","controller":true}]},"spec":{"replicas":1,"selector":{"matchLabels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-cluster-master","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"cluster-master","app.kubernetes.io/part-of":"splunk-stack1-indexer"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-cluster-master","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"cluster-master","app.kubernetes.io/part-of":"splunk-stack1-indexer"},"annotations":{"traffic.sidecar.istio.io/excludeOutboundPorts":"8089,8191,9997,7777,9000,17000,17500,19000","traffic.sidecar.istio.io/includeInboundPorts":"8000"}},"spec":{"volumes":[{"name":"mnt-splunk-secrets","secret":{"secretName":"splunk-stack1-cluster-master-secret-v1","defaultMode":420}}],"containers":[{"name":"splunk","image":"splunk/splunk","ports":[{"name":"splunkweb","containerPort":8000,"protocol":"TCP"},{"name":"splunkd","containerPort":8089,"protocol":"TCP"}],"env":[{"name":"SPLUNK_HOME","value":"/opt/splunk"},{"name":"SPLUNK_START_ARGS","value":"--accept-license"},{"name":"SPLUNK_DEFAULTS_URL","value":"/mnt/splunk-secrets/default.yml"},{"name":"SPLUNK_HOME_OWNERSHIP_ENFORCEMENT","value":"false"},{"name":"SPLUNK_ROLE","value":"splunk_cluster_master"},{"name":"SPLUNK_DECLARATIVE_ADMIN_PASSWORD","value":"true"},{"name":"SPLUNK_LICENSE_MASTER_URL","value":"splunk-stack1-license-master-service.test.svc.cluster.local"},{"name":"SPLUNK_CLUSTER_MASTER_URL","value":"localhost"}],"resources":{"limits":{"cpu":"4","memory":"8Gi"},"requests":{"cpu":"100m","memory":"512Mi"}},"volumeMounts":[{"name":"pvc-etc","mountPath":"/opt/splunk/etc"},{"name":"pvc-var","mountPath":"/opt/splunk/var"},{"name":"mnt-splunk-secrets","mountPath":"/mnt/splunk-secrets"}],"livenessProbe":{"exec":{"command":["/sbin/checkstate.sh"]},"initialDelaySeconds":300,"timeoutSeconds":30,"periodSeconds":30},"readinessProbe":{"exec":{"command":["/bin/grep","started","/opt/container_artifact/splunk-container.state"]},"initialDelaySeconds":10,"timeoutSeconds":5,"periodSeconds":5},"imagePullPolicy":"IfNotPresent"}],"securityContext":{"runAsUser":41812,"fsGroup":41812},"affinity":{"podAntiAffinity":{"preferredDuringSchedulingIgnoredDuringExecution":[{"weight":100,"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app.kubernetes.io/instance","operator":"In","values":["splunk-stack1-cluster-master"]}]},"topologyKey":"kubernetes.io/hostname"}}]}},"schedulerName":"default-scheduler"}},"volumeClaimTemplates":[{"metadata":{"name":"pvc-etc","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-cluster-master","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"cluster-master","app.kubernetes.io/part-of":"splunk-stack1-indexer"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"10Gi"}}},"status":{}},{"metadata":{"name":"pvc-var","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-cluster-master","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"cluster-master","app.kubernetes.io/part-of":"splunk-stack1-indexer"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"100Gi"}}},"status":{}}],"serviceName":"splunk-stack1-cluster-master-headless","podManagementPolicy":"Parallel","updateStrategy":{"type":"OnDelete"}},"status":{"replicas":0}}`) + test(`{"kind":"StatefulSet","apiVersion":"apps/v1","metadata":{"name":"splunk-stack1-cluster-master","namespace":"test","creationTimestamp":null,"ownerReferences":[{"apiVersion":"","kind":"","name":"stack1","uid":"","controller":true}]},"spec":{"replicas":1,"selector":{"matchLabels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-cluster-master","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"cluster-master","app.kubernetes.io/part-of":"splunk-stack1-indexer"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-cluster-master","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"cluster-master","app.kubernetes.io/part-of":"splunk-stack1-indexer"},"annotations":{"traffic.sidecar.istio.io/excludeOutboundPorts":"8089,8191,9997,7777,9000,17000,17500,19000","traffic.sidecar.istio.io/includeInboundPorts":"8000"}},"spec":{"volumes":[{"name":"mnt-splunk-secrets","secret":{"secretName":"splunk-stack1-cluster-master-secret-v1","defaultMode":420}}],"containers":[{"name":"splunk","image":"splunk/splunk","ports":[{"name":"http-splunkweb","containerPort":8000,"protocol":"TCP"},{"name":"https-splunkd","containerPort":8089,"protocol":"TCP"}],"env":[{"name":"SPLUNK_HOME","value":"/opt/splunk"},{"name":"SPLUNK_START_ARGS","value":"--accept-license"},{"name":"SPLUNK_DEFAULTS_URL","value":"/mnt/splunk-secrets/default.yml"},{"name":"SPLUNK_HOME_OWNERSHIP_ENFORCEMENT","value":"false"},{"name":"SPLUNK_ROLE","value":"splunk_cluster_master"},{"name":"SPLUNK_DECLARATIVE_ADMIN_PASSWORD","value":"true"},{"name":"SPLUNK_LICENSE_MASTER_URL","value":"splunk-stack1-license-master-service.test.svc.cluster.local"},{"name":"SPLUNK_CLUSTER_MASTER_URL","value":"localhost"}],"resources":{"limits":{"cpu":"4","memory":"8Gi"},"requests":{"cpu":"100m","memory":"512Mi"}},"volumeMounts":[{"name":"pvc-etc","mountPath":"/opt/splunk/etc"},{"name":"pvc-var","mountPath":"/opt/splunk/var"},{"name":"mnt-splunk-secrets","mountPath":"/mnt/splunk-secrets"}],"livenessProbe":{"exec":{"command":["/sbin/checkstate.sh"]},"initialDelaySeconds":300,"timeoutSeconds":30,"periodSeconds":30},"readinessProbe":{"exec":{"command":["/bin/grep","started","/opt/container_artifact/splunk-container.state"]},"initialDelaySeconds":10,"timeoutSeconds":5,"periodSeconds":5},"imagePullPolicy":"IfNotPresent"}],"securityContext":{"runAsUser":41812,"fsGroup":41812},"affinity":{"podAntiAffinity":{"preferredDuringSchedulingIgnoredDuringExecution":[{"weight":100,"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app.kubernetes.io/instance","operator":"In","values":["splunk-stack1-cluster-master"]}]},"topologyKey":"kubernetes.io/hostname"}}]}},"schedulerName":"default-scheduler"}},"volumeClaimTemplates":[{"metadata":{"name":"pvc-etc","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-cluster-master","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"cluster-master","app.kubernetes.io/part-of":"splunk-stack1-indexer"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"10Gi"}}},"status":{}},{"metadata":{"name":"pvc-var","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-cluster-master","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"cluster-master","app.kubernetes.io/part-of":"splunk-stack1-indexer"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"100Gi"}}},"status":{}}],"serviceName":"splunk-stack1-cluster-master-headless","podManagementPolicy":"Parallel","updateStrategy":{"type":"OnDelete"}},"status":{"replicas":0}}`) cr.Spec.LicenseMasterRef.Name = "" cr.Spec.LicenseURL = "/mnt/splunk.lic" - test(`{"kind":"StatefulSet","apiVersion":"apps/v1","metadata":{"name":"splunk-stack1-cluster-master","namespace":"test","creationTimestamp":null,"ownerReferences":[{"apiVersion":"","kind":"","name":"stack1","uid":"","controller":true}]},"spec":{"replicas":1,"selector":{"matchLabels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-cluster-master","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"cluster-master","app.kubernetes.io/part-of":"splunk-stack1-indexer"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-cluster-master","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"cluster-master","app.kubernetes.io/part-of":"splunk-stack1-indexer"},"annotations":{"traffic.sidecar.istio.io/excludeOutboundPorts":"8089,8191,9997,7777,9000,17000,17500,19000","traffic.sidecar.istio.io/includeInboundPorts":"8000"}},"spec":{"volumes":[{"name":"mnt-splunk-secrets","secret":{"secretName":"splunk-stack1-cluster-master-secret-v1","defaultMode":420}}],"containers":[{"name":"splunk","image":"splunk/splunk","ports":[{"name":"splunkweb","containerPort":8000,"protocol":"TCP"},{"name":"splunkd","containerPort":8089,"protocol":"TCP"}],"env":[{"name":"SPLUNK_HOME","value":"/opt/splunk"},{"name":"SPLUNK_START_ARGS","value":"--accept-license"},{"name":"SPLUNK_DEFAULTS_URL","value":"/mnt/splunk-secrets/default.yml"},{"name":"SPLUNK_HOME_OWNERSHIP_ENFORCEMENT","value":"false"},{"name":"SPLUNK_ROLE","value":"splunk_cluster_master"},{"name":"SPLUNK_DECLARATIVE_ADMIN_PASSWORD","value":"true"},{"name":"SPLUNK_LICENSE_URI","value":"/mnt/splunk.lic"},{"name":"SPLUNK_CLUSTER_MASTER_URL","value":"localhost"}],"resources":{"limits":{"cpu":"4","memory":"8Gi"},"requests":{"cpu":"100m","memory":"512Mi"}},"volumeMounts":[{"name":"pvc-etc","mountPath":"/opt/splunk/etc"},{"name":"pvc-var","mountPath":"/opt/splunk/var"},{"name":"mnt-splunk-secrets","mountPath":"/mnt/splunk-secrets"}],"livenessProbe":{"exec":{"command":["/sbin/checkstate.sh"]},"initialDelaySeconds":300,"timeoutSeconds":30,"periodSeconds":30},"readinessProbe":{"exec":{"command":["/bin/grep","started","/opt/container_artifact/splunk-container.state"]},"initialDelaySeconds":10,"timeoutSeconds":5,"periodSeconds":5},"imagePullPolicy":"IfNotPresent"}],"securityContext":{"runAsUser":41812,"fsGroup":41812},"affinity":{"podAntiAffinity":{"preferredDuringSchedulingIgnoredDuringExecution":[{"weight":100,"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app.kubernetes.io/instance","operator":"In","values":["splunk-stack1-cluster-master"]}]},"topologyKey":"kubernetes.io/hostname"}}]}},"schedulerName":"default-scheduler"}},"volumeClaimTemplates":[{"metadata":{"name":"pvc-etc","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-cluster-master","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"cluster-master","app.kubernetes.io/part-of":"splunk-stack1-indexer"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"10Gi"}}},"status":{}},{"metadata":{"name":"pvc-var","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-cluster-master","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"cluster-master","app.kubernetes.io/part-of":"splunk-stack1-indexer"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"100Gi"}}},"status":{}}],"serviceName":"splunk-stack1-cluster-master-headless","podManagementPolicy":"Parallel","updateStrategy":{"type":"OnDelete"}},"status":{"replicas":0}}`) + test(`{"kind":"StatefulSet","apiVersion":"apps/v1","metadata":{"name":"splunk-stack1-cluster-master","namespace":"test","creationTimestamp":null,"ownerReferences":[{"apiVersion":"","kind":"","name":"stack1","uid":"","controller":true}]},"spec":{"replicas":1,"selector":{"matchLabels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-cluster-master","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"cluster-master","app.kubernetes.io/part-of":"splunk-stack1-indexer"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-cluster-master","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"cluster-master","app.kubernetes.io/part-of":"splunk-stack1-indexer"},"annotations":{"traffic.sidecar.istio.io/excludeOutboundPorts":"8089,8191,9997,7777,9000,17000,17500,19000","traffic.sidecar.istio.io/includeInboundPorts":"8000"}},"spec":{"volumes":[{"name":"mnt-splunk-secrets","secret":{"secretName":"splunk-stack1-cluster-master-secret-v1","defaultMode":420}}],"containers":[{"name":"splunk","image":"splunk/splunk","ports":[{"name":"http-splunkweb","containerPort":8000,"protocol":"TCP"},{"name":"https-splunkd","containerPort":8089,"protocol":"TCP"}],"env":[{"name":"SPLUNK_HOME","value":"/opt/splunk"},{"name":"SPLUNK_START_ARGS","value":"--accept-license"},{"name":"SPLUNK_DEFAULTS_URL","value":"/mnt/splunk-secrets/default.yml"},{"name":"SPLUNK_HOME_OWNERSHIP_ENFORCEMENT","value":"false"},{"name":"SPLUNK_ROLE","value":"splunk_cluster_master"},{"name":"SPLUNK_DECLARATIVE_ADMIN_PASSWORD","value":"true"},{"name":"SPLUNK_LICENSE_URI","value":"/mnt/splunk.lic"},{"name":"SPLUNK_CLUSTER_MASTER_URL","value":"localhost"}],"resources":{"limits":{"cpu":"4","memory":"8Gi"},"requests":{"cpu":"100m","memory":"512Mi"}},"volumeMounts":[{"name":"pvc-etc","mountPath":"/opt/splunk/etc"},{"name":"pvc-var","mountPath":"/opt/splunk/var"},{"name":"mnt-splunk-secrets","mountPath":"/mnt/splunk-secrets"}],"livenessProbe":{"exec":{"command":["/sbin/checkstate.sh"]},"initialDelaySeconds":300,"timeoutSeconds":30,"periodSeconds":30},"readinessProbe":{"exec":{"command":["/bin/grep","started","/opt/container_artifact/splunk-container.state"]},"initialDelaySeconds":10,"timeoutSeconds":5,"periodSeconds":5},"imagePullPolicy":"IfNotPresent"}],"securityContext":{"runAsUser":41812,"fsGroup":41812},"affinity":{"podAntiAffinity":{"preferredDuringSchedulingIgnoredDuringExecution":[{"weight":100,"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app.kubernetes.io/instance","operator":"In","values":["splunk-stack1-cluster-master"]}]},"topologyKey":"kubernetes.io/hostname"}}]}},"schedulerName":"default-scheduler"}},"volumeClaimTemplates":[{"metadata":{"name":"pvc-etc","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-cluster-master","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"cluster-master","app.kubernetes.io/part-of":"splunk-stack1-indexer"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"10Gi"}}},"status":{}},{"metadata":{"name":"pvc-var","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-cluster-master","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"cluster-master","app.kubernetes.io/part-of":"splunk-stack1-indexer"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"100Gi"}}},"status":{}}],"serviceName":"splunk-stack1-cluster-master-headless","podManagementPolicy":"Parallel","updateStrategy":{"type":"OnDelete"}},"status":{"replicas":0}}`) } func TestApplyClusterMasterWithSmartstore(t *testing.T) { diff --git a/pkg/splunk/enterprise/configuration.go b/pkg/splunk/enterprise/configuration.go index 48f2a4fce..1f3c83861 100644 --- a/pkg/splunk/enterprise/configuration.go +++ b/pkg/splunk/enterprise/configuration.go @@ -246,27 +246,27 @@ func prepareSplunkSmartstoreConfigMap(identifier, namespace string, crKind strin // getSplunkPorts returns a map of ports to use for Splunk instances. func getSplunkPorts(instanceType InstanceType) map[string]int { result := map[string]int{ - "splunkweb": 8000, - "splunkd": 8089, + "http-splunkweb": 8000, + "https-splunkd": 8089, } switch instanceType { case SplunkMonitoringConsole: - result["hec"] = 8088 - result["s2s"] = 9997 + result["http-hec"] = 8088 + result["tcp-s2s"] = 9997 case SplunkStandalone: - result["dfccontrol"] = 17000 - result["datareceive"] = 19000 - result["dfsmaster"] = 9000 - result["hec"] = 8088 - result["s2s"] = 9997 + result["tcp-dfccontrol"] = 17000 + result["tcp-datareceive"] = 19000 + result["tcp-dfsmaster"] = 9000 + result["http-hec"] = 8088 + result["tcp-s2s"] = 9997 case SplunkSearchHead: - result["dfccontrol"] = 17000 - result["datareceive"] = 19000 - result["dfsmaster"] = 9000 + result["tcp-dfccontrol"] = 17000 + result["tcp-datareceive"] = 19000 + result["tcp-dfsmaster"] = 9000 case SplunkIndexer: - result["hec"] = 8088 - result["s2s"] = 9997 + result["http-hec"] = 8088 + result["tcp-s2s"] = 9997 } return result diff --git a/pkg/splunk/enterprise/configuration_test.go b/pkg/splunk/enterprise/configuration_test.go index ec37dfb13..d469ac333 100644 --- a/pkg/splunk/enterprise/configuration_test.go +++ b/pkg/splunk/enterprise/configuration_test.go @@ -58,11 +58,11 @@ func TestGetSplunkService(t *testing.T) { configTester(t, fmt.Sprintf("getSplunkService(\"%s\",%t)", instanceType, isHeadless), f, want) } - test(SplunkIndexer, false, `{"kind":"Service","apiVersion":"v1","metadata":{"name":"splunk-stack1-indexer-service","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-stack1-indexer"},"ownerReferences":[{"apiVersion":"","kind":"","name":"stack1","uid":"","controller":true}]},"spec":{"ports":[{"name":"splunkweb","protocol":"TCP","port":8000,"targetPort":8000},{"name":"hec","protocol":"TCP","port":8088,"targetPort":8088},{"name":"splunkd","protocol":"TCP","port":8089,"targetPort":8089},{"name":"s2s","protocol":"TCP","port":9997,"targetPort":9997}],"selector":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-stack1-indexer"}},"status":{"loadBalancer":{}}}`) - test(SplunkIndexer, true, `{"kind":"Service","apiVersion":"v1","metadata":{"name":"splunk-stack1-indexer-headless","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-stack1-indexer"},"ownerReferences":[{"apiVersion":"","kind":"","name":"stack1","uid":"","controller":true}]},"spec":{"ports":[{"name":"splunkweb","protocol":"TCP","port":8000,"targetPort":8000},{"name":"hec","protocol":"TCP","port":8088,"targetPort":8088},{"name":"splunkd","protocol":"TCP","port":8089,"targetPort":8089},{"name":"s2s","protocol":"TCP","port":9997,"targetPort":9997}],"selector":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-stack1-indexer"},"clusterIP":"None","type":"ClusterIP"},"status":{"loadBalancer":{}}}`) + test(SplunkIndexer, false, `{"kind":"Service","apiVersion":"v1","metadata":{"name":"splunk-stack1-indexer-service","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-stack1-indexer"},"ownerReferences":[{"apiVersion":"","kind":"","name":"stack1","uid":"","controller":true}]},"spec":{"ports":[{"name":"http-splunkweb","protocol":"TCP","port":8000,"targetPort":8000},{"name":"http-hec","protocol":"TCP","port":8088,"targetPort":8088},{"name":"https-splunkd","protocol":"TCP","port":8089,"targetPort":8089},{"name":"tcp-s2s","protocol":"TCP","port":9997,"targetPort":9997}],"selector":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-stack1-indexer"}},"status":{"loadBalancer":{}}}`) + test(SplunkIndexer, true, `{"kind":"Service","apiVersion":"v1","metadata":{"name":"splunk-stack1-indexer-headless","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-stack1-indexer"},"ownerReferences":[{"apiVersion":"","kind":"","name":"stack1","uid":"","controller":true}]},"spec":{"ports":[{"name":"http-splunkweb","protocol":"TCP","port":8000,"targetPort":8000},{"name":"http-hec","protocol":"TCP","port":8088,"targetPort":8088},{"name":"https-splunkd","protocol":"TCP","port":8089,"targetPort":8089},{"name":"tcp-s2s","protocol":"TCP","port":9997,"targetPort":9997}],"selector":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-stack1-indexer"},"clusterIP":"None","type":"ClusterIP"},"status":{"loadBalancer":{}}}`) // Multipart IndexerCluster - test part-of and instance labels for child part cr.Spec.ClusterMasterRef.Name = "cluster1" - test(SplunkIndexer, false, `{"kind":"Service","apiVersion":"v1","metadata":{"name":"splunk-stack1-indexer-service","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-cluster1-indexer"},"ownerReferences":[{"apiVersion":"","kind":"","name":"stack1","uid":"","controller":true}]},"spec":{"ports":[{"name":"splunkweb","protocol":"TCP","port":8000,"targetPort":8000},{"name":"hec","protocol":"TCP","port":8088,"targetPort":8088},{"name":"splunkd","protocol":"TCP","port":8089,"targetPort":8089},{"name":"s2s","protocol":"TCP","port":9997,"targetPort":9997}],"selector":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-cluster1-indexer"}},"status":{"loadBalancer":{}}}`) + test(SplunkIndexer, false, `{"kind":"Service","apiVersion":"v1","metadata":{"name":"splunk-stack1-indexer-service","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-cluster1-indexer"},"ownerReferences":[{"apiVersion":"","kind":"","name":"stack1","uid":"","controller":true}]},"spec":{"ports":[{"name":"http-splunkweb","protocol":"TCP","port":8000,"targetPort":8000},{"name":"http-hec","protocol":"TCP","port":8088,"targetPort":8088},{"name":"https-splunkd","protocol":"TCP","port":8089,"targetPort":8089},{"name":"tcp-s2s","protocol":"TCP","port":9997,"targetPort":9997}],"selector":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-cluster1-indexer"}},"status":{"loadBalancer":{}}}`) cr.Spec.ClusterMasterRef.Name = "" cr.Spec.ServiceTemplate.Spec.Type = "LoadBalancer" @@ -70,8 +70,8 @@ func TestGetSplunkService(t *testing.T) { cr.ObjectMeta.Labels = map[string]string{"one": "two"} cr.ObjectMeta.Annotations = map[string]string{"a": "b"} - test(SplunkSearchHead, false, `{"kind":"Service","apiVersion":"v1","metadata":{"name":"splunk-stack1-search-head-service","namespace":"test","creationTimestamp":null,"labels":{"1":"2","app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-search-head","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"search-head","app.kubernetes.io/part-of":"splunk-stack1-search-head","one":"two"},"annotations":{"a":"b"},"ownerReferences":[{"apiVersion":"","kind":"","name":"stack1","uid":"","controller":true}]},"spec":{"ports":[{"name":"splunkweb","protocol":"TCP","port":8000,"targetPort":8000},{"name":"splunkd","protocol":"TCP","port":8089,"targetPort":8089},{"name":"dfsmaster","protocol":"TCP","port":9000,"targetPort":9000},{"name":"dfccontrol","protocol":"TCP","port":17000,"targetPort":17000},{"name":"datareceive","protocol":"TCP","port":19000,"targetPort":19000}],"selector":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-search-head","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"search-head","app.kubernetes.io/part-of":"splunk-stack1-search-head"},"type":"LoadBalancer"},"status":{"loadBalancer":{}}}`) - test(SplunkSearchHead, true, `{"kind":"Service","apiVersion":"v1","metadata":{"name":"splunk-stack1-search-head-headless","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-search-head","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"search-head","app.kubernetes.io/part-of":"splunk-stack1-search-head","one":"two"},"annotations":{"a":"b"},"ownerReferences":[{"apiVersion":"","kind":"","name":"stack1","uid":"","controller":true}]},"spec":{"ports":[{"name":"splunkweb","protocol":"TCP","port":8000,"targetPort":8000},{"name":"splunkd","protocol":"TCP","port":8089,"targetPort":8089},{"name":"dfsmaster","protocol":"TCP","port":9000,"targetPort":9000},{"name":"dfccontrol","protocol":"TCP","port":17000,"targetPort":17000},{"name":"datareceive","protocol":"TCP","port":19000,"targetPort":19000}],"selector":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-search-head","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"search-head","app.kubernetes.io/part-of":"splunk-stack1-search-head"},"clusterIP":"None","type":"ClusterIP","publishNotReadyAddresses":true},"status":{"loadBalancer":{}}}`) + test(SplunkSearchHead, false, `{"kind":"Service","apiVersion":"v1","metadata":{"name":"splunk-stack1-search-head-service","namespace":"test","creationTimestamp":null,"labels":{"1":"2","app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-search-head","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"search-head","app.kubernetes.io/part-of":"splunk-stack1-search-head","one":"two"},"annotations":{"a":"b"},"ownerReferences":[{"apiVersion":"","kind":"","name":"stack1","uid":"","controller":true}]},"spec":{"ports":[{"name":"http-splunkweb","protocol":"TCP","port":8000,"targetPort":8000},{"name":"https-splunkd","protocol":"TCP","port":8089,"targetPort":8089},{"name":"tcp-dfsmaster","protocol":"TCP","port":9000,"targetPort":9000},{"name":"tcp-dfccontrol","protocol":"TCP","port":17000,"targetPort":17000},{"name":"tcp-datareceive","protocol":"TCP","port":19000,"targetPort":19000}],"selector":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-search-head","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"search-head","app.kubernetes.io/part-of":"splunk-stack1-search-head"},"type":"LoadBalancer"},"status":{"loadBalancer":{}}}`) + test(SplunkSearchHead, true, `{"kind":"Service","apiVersion":"v1","metadata":{"name":"splunk-stack1-search-head-headless","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-search-head","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"search-head","app.kubernetes.io/part-of":"splunk-stack1-search-head","one":"two"},"annotations":{"a":"b"},"ownerReferences":[{"apiVersion":"","kind":"","name":"stack1","uid":"","controller":true}]},"spec":{"ports":[{"name":"http-splunkweb","protocol":"TCP","port":8000,"targetPort":8000},{"name":"https-splunkd","protocol":"TCP","port":8089,"targetPort":8089},{"name":"tcp-dfsmaster","protocol":"TCP","port":9000,"targetPort":9000},{"name":"tcp-dfccontrol","protocol":"TCP","port":17000,"targetPort":17000},{"name":"tcp-datareceive","protocol":"TCP","port":19000,"targetPort":19000}],"selector":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-search-head","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"search-head","app.kubernetes.io/part-of":"splunk-stack1-search-head"},"clusterIP":"None","type":"ClusterIP","publishNotReadyAddresses":true},"status":{"loadBalancer":{}}}`) } func TestGetSplunkDefaults(t *testing.T) { @@ -122,7 +122,7 @@ func TestGetService(t *testing.T) { configTester(t, "getSplunkService()", f, want) } - test(SplunkIndexer, `{"kind":"Service","apiVersion":"v1","metadata":{"name":"splunk-stack1-indexer-service","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-stack1-indexer"},"ownerReferences":[{"apiVersion":"","kind":"","name":"stack1","uid":"","controller":true}]},"spec":{"ports":[{"name":"user-defined","port":32000,"targetPort":6443},{"name":"splunkweb","protocol":"TCP","port":8000,"targetPort":8000},{"name":"hec","protocol":"TCP","port":8088,"targetPort":8088},{"name":"splunkd","protocol":"TCP","port":8089,"targetPort":8089},{"name":"s2s","protocol":"TCP","port":9997,"targetPort":9997}],"selector":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-stack1-indexer"}},"status":{"loadBalancer":{}}}`) + test(SplunkIndexer, `{"kind":"Service","apiVersion":"v1","metadata":{"name":"splunk-stack1-indexer-service","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-stack1-indexer"},"ownerReferences":[{"apiVersion":"","kind":"","name":"stack1","uid":"","controller":true}]},"spec":{"ports":[{"name":"user-defined","port":32000,"targetPort":6443},{"name":"http-splunkweb","protocol":"TCP","port":8000,"targetPort":8000},{"name":"http-hec","protocol":"TCP","port":8088,"targetPort":8088},{"name":"https-splunkd","protocol":"TCP","port":8089,"targetPort":8089},{"name":"tcp-s2s","protocol":"TCP","port":9997,"targetPort":9997}],"selector":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-stack1-indexer"}},"status":{"loadBalancer":{}}}`) } func TestSetVolumeDefault(t *testing.T) { diff --git a/pkg/splunk/enterprise/indexercluster_test.go b/pkg/splunk/enterprise/indexercluster_test.go index 770036790..a762946e4 100644 --- a/pkg/splunk/enterprise/indexercluster_test.go +++ b/pkg/splunk/enterprise/indexercluster_test.go @@ -1022,14 +1022,14 @@ func TestGetIndexerStatefulSet(t *testing.T) { } cr.Spec.Replicas = 0 - test(`{"kind":"StatefulSet","apiVersion":"apps/v1","metadata":{"name":"splunk-stack1-indexer","namespace":"test","creationTimestamp":null,"ownerReferences":[{"apiVersion":"","kind":"","name":"stack1","uid":"","controller":true}]},"spec":{"replicas":1,"selector":{"matchLabels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-master1-indexer"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-master1-indexer"},"annotations":{"traffic.sidecar.istio.io/excludeOutboundPorts":"8089,8191,9997,7777,9000,17000,17500,19000","traffic.sidecar.istio.io/includeInboundPorts":"8000,8088"}},"spec":{"volumes":[{"name":"mnt-splunk-secrets","secret":{"secretName":"splunk-stack1-indexer-secret-v1","defaultMode":420}}],"containers":[{"name":"splunk","image":"splunk/splunk","ports":[{"name":"splunkweb","containerPort":8000,"protocol":"TCP"},{"name":"hec","containerPort":8088,"protocol":"TCP"},{"name":"splunkd","containerPort":8089,"protocol":"TCP"},{"name":"s2s","containerPort":9997,"protocol":"TCP"}],"env":[{"name":"SPLUNK_HOME","value":"/opt/splunk"},{"name":"SPLUNK_START_ARGS","value":"--accept-license"},{"name":"SPLUNK_DEFAULTS_URL","value":"/mnt/splunk-secrets/default.yml"},{"name":"SPLUNK_HOME_OWNERSHIP_ENFORCEMENT","value":"false"},{"name":"SPLUNK_ROLE","value":"splunk_indexer"},{"name":"SPLUNK_DECLARATIVE_ADMIN_PASSWORD","value":"true"},{"name":"SPLUNK_INDEXER_URL","value":"splunk-stack1-indexer-0.splunk-stack1-indexer-headless.test.svc.cluster.local"},{"name":"SPLUNK_CLUSTER_MASTER_URL","value":"splunk-master1-cluster-master-service"}],"resources":{"limits":{"cpu":"4","memory":"8Gi"},"requests":{"cpu":"100m","memory":"512Mi"}},"volumeMounts":[{"name":"pvc-etc","mountPath":"/opt/splunk/etc"},{"name":"pvc-var","mountPath":"/opt/splunk/var"},{"name":"mnt-splunk-secrets","mountPath":"/mnt/splunk-secrets"}],"livenessProbe":{"exec":{"command":["/sbin/checkstate.sh"]},"initialDelaySeconds":300,"timeoutSeconds":30,"periodSeconds":30},"readinessProbe":{"exec":{"command":["/bin/grep","started","/opt/container_artifact/splunk-container.state"]},"initialDelaySeconds":10,"timeoutSeconds":5,"periodSeconds":5},"imagePullPolicy":"IfNotPresent"}],"securityContext":{"runAsUser":41812,"fsGroup":41812},"affinity":{"podAntiAffinity":{"preferredDuringSchedulingIgnoredDuringExecution":[{"weight":100,"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app.kubernetes.io/instance","operator":"In","values":["splunk-stack1-indexer"]}]},"topologyKey":"kubernetes.io/hostname"}}]}},"schedulerName":"default-scheduler"}},"volumeClaimTemplates":[{"metadata":{"name":"pvc-etc","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-master1-indexer"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"10Gi"}}},"status":{}},{"metadata":{"name":"pvc-var","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-master1-indexer"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"100Gi"}}},"status":{}}],"serviceName":"splunk-stack1-indexer-headless","podManagementPolicy":"Parallel","updateStrategy":{"type":"OnDelete"}},"status":{"replicas":0}}`) + test(`{"kind":"StatefulSet","apiVersion":"apps/v1","metadata":{"name":"splunk-stack1-indexer","namespace":"test","creationTimestamp":null,"ownerReferences":[{"apiVersion":"","kind":"","name":"stack1","uid":"","controller":true}]},"spec":{"replicas":1,"selector":{"matchLabels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-master1-indexer"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-master1-indexer"},"annotations":{"traffic.sidecar.istio.io/excludeOutboundPorts":"8089,8191,9997,7777,9000,17000,17500,19000","traffic.sidecar.istio.io/includeInboundPorts":"8000,8088"}},"spec":{"volumes":[{"name":"mnt-splunk-secrets","secret":{"secretName":"splunk-stack1-indexer-secret-v1","defaultMode":420}}],"containers":[{"name":"splunk","image":"splunk/splunk","ports":[{"name":"http-splunkweb","containerPort":8000,"protocol":"TCP"},{"name":"http-hec","containerPort":8088,"protocol":"TCP"},{"name":"https-splunkd","containerPort":8089,"protocol":"TCP"},{"name":"tcp-s2s","containerPort":9997,"protocol":"TCP"}],"env":[{"name":"SPLUNK_HOME","value":"/opt/splunk"},{"name":"SPLUNK_START_ARGS","value":"--accept-license"},{"name":"SPLUNK_DEFAULTS_URL","value":"/mnt/splunk-secrets/default.yml"},{"name":"SPLUNK_HOME_OWNERSHIP_ENFORCEMENT","value":"false"},{"name":"SPLUNK_ROLE","value":"splunk_indexer"},{"name":"SPLUNK_DECLARATIVE_ADMIN_PASSWORD","value":"true"},{"name":"SPLUNK_INDEXER_URL","value":"splunk-stack1-indexer-0.splunk-stack1-indexer-headless.test.svc.cluster.local"},{"name":"SPLUNK_CLUSTER_MASTER_URL","value":"splunk-master1-cluster-master-service"}],"resources":{"limits":{"cpu":"4","memory":"8Gi"},"requests":{"cpu":"100m","memory":"512Mi"}},"volumeMounts":[{"name":"pvc-etc","mountPath":"/opt/splunk/etc"},{"name":"pvc-var","mountPath":"/opt/splunk/var"},{"name":"mnt-splunk-secrets","mountPath":"/mnt/splunk-secrets"}],"livenessProbe":{"exec":{"command":["/sbin/checkstate.sh"]},"initialDelaySeconds":300,"timeoutSeconds":30,"periodSeconds":30},"readinessProbe":{"exec":{"command":["/bin/grep","started","/opt/container_artifact/splunk-container.state"]},"initialDelaySeconds":10,"timeoutSeconds":5,"periodSeconds":5},"imagePullPolicy":"IfNotPresent"}],"securityContext":{"runAsUser":41812,"fsGroup":41812},"affinity":{"podAntiAffinity":{"preferredDuringSchedulingIgnoredDuringExecution":[{"weight":100,"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app.kubernetes.io/instance","operator":"In","values":["splunk-stack1-indexer"]}]},"topologyKey":"kubernetes.io/hostname"}}]}},"schedulerName":"default-scheduler"}},"volumeClaimTemplates":[{"metadata":{"name":"pvc-etc","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-master1-indexer"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"10Gi"}}},"status":{}},{"metadata":{"name":"pvc-var","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-master1-indexer"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"100Gi"}}},"status":{}}],"serviceName":"splunk-stack1-indexer-headless","podManagementPolicy":"Parallel","updateStrategy":{"type":"OnDelete"}},"status":{"replicas":0}}`) cr.Spec.Replicas = 1 - test(`{"kind":"StatefulSet","apiVersion":"apps/v1","metadata":{"name":"splunk-stack1-indexer","namespace":"test","creationTimestamp":null,"ownerReferences":[{"apiVersion":"","kind":"","name":"stack1","uid":"","controller":true}]},"spec":{"replicas":1,"selector":{"matchLabels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-master1-indexer"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-master1-indexer"},"annotations":{"traffic.sidecar.istio.io/excludeOutboundPorts":"8089,8191,9997,7777,9000,17000,17500,19000","traffic.sidecar.istio.io/includeInboundPorts":"8000,8088"}},"spec":{"volumes":[{"name":"mnt-splunk-secrets","secret":{"secretName":"splunk-stack1-indexer-secret-v1","defaultMode":420}}],"containers":[{"name":"splunk","image":"splunk/splunk","ports":[{"name":"splunkweb","containerPort":8000,"protocol":"TCP"},{"name":"hec","containerPort":8088,"protocol":"TCP"},{"name":"splunkd","containerPort":8089,"protocol":"TCP"},{"name":"s2s","containerPort":9997,"protocol":"TCP"}],"env":[{"name":"SPLUNK_HOME","value":"/opt/splunk"},{"name":"SPLUNK_START_ARGS","value":"--accept-license"},{"name":"SPLUNK_DEFAULTS_URL","value":"/mnt/splunk-secrets/default.yml"},{"name":"SPLUNK_HOME_OWNERSHIP_ENFORCEMENT","value":"false"},{"name":"SPLUNK_ROLE","value":"splunk_indexer"},{"name":"SPLUNK_DECLARATIVE_ADMIN_PASSWORD","value":"true"},{"name":"SPLUNK_INDEXER_URL","value":"splunk-stack1-indexer-0.splunk-stack1-indexer-headless.test.svc.cluster.local"},{"name":"SPLUNK_CLUSTER_MASTER_URL","value":"splunk-master1-cluster-master-service"}],"resources":{"limits":{"cpu":"4","memory":"8Gi"},"requests":{"cpu":"100m","memory":"512Mi"}},"volumeMounts":[{"name":"pvc-etc","mountPath":"/opt/splunk/etc"},{"name":"pvc-var","mountPath":"/opt/splunk/var"},{"name":"mnt-splunk-secrets","mountPath":"/mnt/splunk-secrets"}],"livenessProbe":{"exec":{"command":["/sbin/checkstate.sh"]},"initialDelaySeconds":300,"timeoutSeconds":30,"periodSeconds":30},"readinessProbe":{"exec":{"command":["/bin/grep","started","/opt/container_artifact/splunk-container.state"]},"initialDelaySeconds":10,"timeoutSeconds":5,"periodSeconds":5},"imagePullPolicy":"IfNotPresent"}],"securityContext":{"runAsUser":41812,"fsGroup":41812},"affinity":{"podAntiAffinity":{"preferredDuringSchedulingIgnoredDuringExecution":[{"weight":100,"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app.kubernetes.io/instance","operator":"In","values":["splunk-stack1-indexer"]}]},"topologyKey":"kubernetes.io/hostname"}}]}},"schedulerName":"default-scheduler"}},"volumeClaimTemplates":[{"metadata":{"name":"pvc-etc","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-master1-indexer"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"10Gi"}}},"status":{}},{"metadata":{"name":"pvc-var","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-master1-indexer"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"100Gi"}}},"status":{}}],"serviceName":"splunk-stack1-indexer-headless","podManagementPolicy":"Parallel","updateStrategy":{"type":"OnDelete"}},"status":{"replicas":0}}`) + test(`{"kind":"StatefulSet","apiVersion":"apps/v1","metadata":{"name":"splunk-stack1-indexer","namespace":"test","creationTimestamp":null,"ownerReferences":[{"apiVersion":"","kind":"","name":"stack1","uid":"","controller":true}]},"spec":{"replicas":1,"selector":{"matchLabels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-master1-indexer"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-master1-indexer"},"annotations":{"traffic.sidecar.istio.io/excludeOutboundPorts":"8089,8191,9997,7777,9000,17000,17500,19000","traffic.sidecar.istio.io/includeInboundPorts":"8000,8088"}},"spec":{"volumes":[{"name":"mnt-splunk-secrets","secret":{"secretName":"splunk-stack1-indexer-secret-v1","defaultMode":420}}],"containers":[{"name":"splunk","image":"splunk/splunk","ports":[{"name":"http-splunkweb","containerPort":8000,"protocol":"TCP"},{"name":"http-hec","containerPort":8088,"protocol":"TCP"},{"name":"https-splunkd","containerPort":8089,"protocol":"TCP"},{"name":"tcp-s2s","containerPort":9997,"protocol":"TCP"}],"env":[{"name":"SPLUNK_HOME","value":"/opt/splunk"},{"name":"SPLUNK_START_ARGS","value":"--accept-license"},{"name":"SPLUNK_DEFAULTS_URL","value":"/mnt/splunk-secrets/default.yml"},{"name":"SPLUNK_HOME_OWNERSHIP_ENFORCEMENT","value":"false"},{"name":"SPLUNK_ROLE","value":"splunk_indexer"},{"name":"SPLUNK_DECLARATIVE_ADMIN_PASSWORD","value":"true"},{"name":"SPLUNK_INDEXER_URL","value":"splunk-stack1-indexer-0.splunk-stack1-indexer-headless.test.svc.cluster.local"},{"name":"SPLUNK_CLUSTER_MASTER_URL","value":"splunk-master1-cluster-master-service"}],"resources":{"limits":{"cpu":"4","memory":"8Gi"},"requests":{"cpu":"100m","memory":"512Mi"}},"volumeMounts":[{"name":"pvc-etc","mountPath":"/opt/splunk/etc"},{"name":"pvc-var","mountPath":"/opt/splunk/var"},{"name":"mnt-splunk-secrets","mountPath":"/mnt/splunk-secrets"}],"livenessProbe":{"exec":{"command":["/sbin/checkstate.sh"]},"initialDelaySeconds":300,"timeoutSeconds":30,"periodSeconds":30},"readinessProbe":{"exec":{"command":["/bin/grep","started","/opt/container_artifact/splunk-container.state"]},"initialDelaySeconds":10,"timeoutSeconds":5,"periodSeconds":5},"imagePullPolicy":"IfNotPresent"}],"securityContext":{"runAsUser":41812,"fsGroup":41812},"affinity":{"podAntiAffinity":{"preferredDuringSchedulingIgnoredDuringExecution":[{"weight":100,"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app.kubernetes.io/instance","operator":"In","values":["splunk-stack1-indexer"]}]},"topologyKey":"kubernetes.io/hostname"}}]}},"schedulerName":"default-scheduler"}},"volumeClaimTemplates":[{"metadata":{"name":"pvc-etc","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-master1-indexer"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"10Gi"}}},"status":{}},{"metadata":{"name":"pvc-var","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-master1-indexer"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"100Gi"}}},"status":{}}],"serviceName":"splunk-stack1-indexer-headless","podManagementPolicy":"Parallel","updateStrategy":{"type":"OnDelete"}},"status":{"replicas":0}}`) // Define additional service port in CR and verified the statefulset has the new port cr.Spec.ServiceTemplate.Spec.Ports = []corev1.ServicePort{{Name: "user-defined", Port: 32000, Protocol: "UDP"}} - test(`{"kind":"StatefulSet","apiVersion":"apps/v1","metadata":{"name":"splunk-stack1-indexer","namespace":"test","creationTimestamp":null,"ownerReferences":[{"apiVersion":"","kind":"","name":"stack1","uid":"","controller":true}]},"spec":{"replicas":1,"selector":{"matchLabels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-master1-indexer"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-master1-indexer"},"annotations":{"traffic.sidecar.istio.io/excludeOutboundPorts":"8089,8191,9997,7777,9000,17000,17500,19000","traffic.sidecar.istio.io/includeInboundPorts":"8000,8088"}},"spec":{"volumes":[{"name":"mnt-splunk-secrets","secret":{"secretName":"splunk-stack1-indexer-secret-v1","defaultMode":420}}],"containers":[{"name":"splunk","image":"splunk/splunk","ports":[{"name":"splunkweb","containerPort":8000,"protocol":"TCP"},{"name":"hec","containerPort":8088,"protocol":"TCP"},{"name":"splunkd","containerPort":8089,"protocol":"TCP"},{"name":"s2s","containerPort":9997,"protocol":"TCP"},{"name":"user-defined","containerPort":32000,"protocol":"UDP"}],"env":[{"name":"SPLUNK_HOME","value":"/opt/splunk"},{"name":"SPLUNK_START_ARGS","value":"--accept-license"},{"name":"SPLUNK_DEFAULTS_URL","value":"/mnt/splunk-secrets/default.yml"},{"name":"SPLUNK_HOME_OWNERSHIP_ENFORCEMENT","value":"false"},{"name":"SPLUNK_ROLE","value":"splunk_indexer"},{"name":"SPLUNK_DECLARATIVE_ADMIN_PASSWORD","value":"true"},{"name":"SPLUNK_INDEXER_URL","value":"splunk-stack1-indexer-0.splunk-stack1-indexer-headless.test.svc.cluster.local"},{"name":"SPLUNK_CLUSTER_MASTER_URL","value":"splunk-master1-cluster-master-service"}],"resources":{"limits":{"cpu":"4","memory":"8Gi"},"requests":{"cpu":"100m","memory":"512Mi"}},"volumeMounts":[{"name":"pvc-etc","mountPath":"/opt/splunk/etc"},{"name":"pvc-var","mountPath":"/opt/splunk/var"},{"name":"mnt-splunk-secrets","mountPath":"/mnt/splunk-secrets"}],"livenessProbe":{"exec":{"command":["/sbin/checkstate.sh"]},"initialDelaySeconds":300,"timeoutSeconds":30,"periodSeconds":30},"readinessProbe":{"exec":{"command":["/bin/grep","started","/opt/container_artifact/splunk-container.state"]},"initialDelaySeconds":10,"timeoutSeconds":5,"periodSeconds":5},"imagePullPolicy":"IfNotPresent"}],"securityContext":{"runAsUser":41812,"fsGroup":41812},"affinity":{"podAntiAffinity":{"preferredDuringSchedulingIgnoredDuringExecution":[{"weight":100,"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app.kubernetes.io/instance","operator":"In","values":["splunk-stack1-indexer"]}]},"topologyKey":"kubernetes.io/hostname"}}]}},"schedulerName":"default-scheduler"}},"volumeClaimTemplates":[{"metadata":{"name":"pvc-etc","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-master1-indexer"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"10Gi"}}},"status":{}},{"metadata":{"name":"pvc-var","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-master1-indexer"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"100Gi"}}},"status":{}}],"serviceName":"splunk-stack1-indexer-headless","podManagementPolicy":"Parallel","updateStrategy":{"type":"OnDelete"}},"status":{"replicas":0}}`) + test(`{"kind":"StatefulSet","apiVersion":"apps/v1","metadata":{"name":"splunk-stack1-indexer","namespace":"test","creationTimestamp":null,"ownerReferences":[{"apiVersion":"","kind":"","name":"stack1","uid":"","controller":true}]},"spec":{"replicas":1,"selector":{"matchLabels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-master1-indexer"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-master1-indexer"},"annotations":{"traffic.sidecar.istio.io/excludeOutboundPorts":"8089,8191,9997,7777,9000,17000,17500,19000","traffic.sidecar.istio.io/includeInboundPorts":"8000,8088"}},"spec":{"volumes":[{"name":"mnt-splunk-secrets","secret":{"secretName":"splunk-stack1-indexer-secret-v1","defaultMode":420}}],"containers":[{"name":"splunk","image":"splunk/splunk","ports":[{"name":"http-splunkweb","containerPort":8000,"protocol":"TCP"},{"name":"http-hec","containerPort":8088,"protocol":"TCP"},{"name":"https-splunkd","containerPort":8089,"protocol":"TCP"},{"name":"tcp-s2s","containerPort":9997,"protocol":"TCP"},{"name":"user-defined","containerPort":32000,"protocol":"UDP"}],"env":[{"name":"SPLUNK_HOME","value":"/opt/splunk"},{"name":"SPLUNK_START_ARGS","value":"--accept-license"},{"name":"SPLUNK_DEFAULTS_URL","value":"/mnt/splunk-secrets/default.yml"},{"name":"SPLUNK_HOME_OWNERSHIP_ENFORCEMENT","value":"false"},{"name":"SPLUNK_ROLE","value":"splunk_indexer"},{"name":"SPLUNK_DECLARATIVE_ADMIN_PASSWORD","value":"true"},{"name":"SPLUNK_INDEXER_URL","value":"splunk-stack1-indexer-0.splunk-stack1-indexer-headless.test.svc.cluster.local"},{"name":"SPLUNK_CLUSTER_MASTER_URL","value":"splunk-master1-cluster-master-service"}],"resources":{"limits":{"cpu":"4","memory":"8Gi"},"requests":{"cpu":"100m","memory":"512Mi"}},"volumeMounts":[{"name":"pvc-etc","mountPath":"/opt/splunk/etc"},{"name":"pvc-var","mountPath":"/opt/splunk/var"},{"name":"mnt-splunk-secrets","mountPath":"/mnt/splunk-secrets"}],"livenessProbe":{"exec":{"command":["/sbin/checkstate.sh"]},"initialDelaySeconds":300,"timeoutSeconds":30,"periodSeconds":30},"readinessProbe":{"exec":{"command":["/bin/grep","started","/opt/container_artifact/splunk-container.state"]},"initialDelaySeconds":10,"timeoutSeconds":5,"periodSeconds":5},"imagePullPolicy":"IfNotPresent"}],"securityContext":{"runAsUser":41812,"fsGroup":41812},"affinity":{"podAntiAffinity":{"preferredDuringSchedulingIgnoredDuringExecution":[{"weight":100,"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app.kubernetes.io/instance","operator":"In","values":["splunk-stack1-indexer"]}]},"topologyKey":"kubernetes.io/hostname"}}]}},"schedulerName":"default-scheduler"}},"volumeClaimTemplates":[{"metadata":{"name":"pvc-etc","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-master1-indexer"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"10Gi"}}},"status":{}},{"metadata":{"name":"pvc-var","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-master1-indexer"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"100Gi"}}},"status":{}}],"serviceName":"splunk-stack1-indexer-headless","podManagementPolicy":"Parallel","updateStrategy":{"type":"OnDelete"}},"status":{"replicas":0}}`) cr.Spec.ClusterMasterRef.Namespace = "other" if err := validateIndexerClusterSpec(&cr); err == nil { diff --git a/pkg/splunk/enterprise/licensemaster_test.go b/pkg/splunk/enterprise/licensemaster_test.go index 772f154b3..162e22c72 100644 --- a/pkg/splunk/enterprise/licensemaster_test.go +++ b/pkg/splunk/enterprise/licensemaster_test.go @@ -100,8 +100,8 @@ func TestGetLicenseMasterStatefulSet(t *testing.T) { configTester(t, "getLicenseMasterStatefulSet()", f, want) } - test(`{"kind":"StatefulSet","apiVersion":"apps/v1","metadata":{"name":"splunk-stack1-license-master","namespace":"test","creationTimestamp":null,"ownerReferences":[{"apiVersion":"","kind":"","name":"stack1","uid":"","controller":true}]},"spec":{"replicas":1,"selector":{"matchLabels":{"app.kubernetes.io/component":"license-master","app.kubernetes.io/instance":"splunk-stack1-license-master","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"license-master","app.kubernetes.io/part-of":"splunk-stack1-license-master"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app.kubernetes.io/component":"license-master","app.kubernetes.io/instance":"splunk-stack1-license-master","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"license-master","app.kubernetes.io/part-of":"splunk-stack1-license-master"},"annotations":{"traffic.sidecar.istio.io/excludeOutboundPorts":"8089,8191,9997,7777,9000,17000,17500,19000","traffic.sidecar.istio.io/includeInboundPorts":"8000"}},"spec":{"volumes":[{"name":"mnt-splunk-secrets","secret":{"secretName":"splunk-stack1-license-master-secret-v1","defaultMode":420}}],"containers":[{"name":"splunk","image":"splunk/splunk","ports":[{"name":"splunkweb","containerPort":8000,"protocol":"TCP"},{"name":"splunkd","containerPort":8089,"protocol":"TCP"}],"env":[{"name":"SPLUNK_HOME","value":"/opt/splunk"},{"name":"SPLUNK_START_ARGS","value":"--accept-license"},{"name":"SPLUNK_DEFAULTS_URL","value":"/mnt/splunk-secrets/default.yml"},{"name":"SPLUNK_HOME_OWNERSHIP_ENFORCEMENT","value":"false"},{"name":"SPLUNK_ROLE","value":"splunk_license_master"},{"name":"SPLUNK_DECLARATIVE_ADMIN_PASSWORD","value":"true"}],"resources":{"limits":{"cpu":"4","memory":"8Gi"},"requests":{"cpu":"100m","memory":"512Mi"}},"volumeMounts":[{"name":"pvc-etc","mountPath":"/opt/splunk/etc"},{"name":"pvc-var","mountPath":"/opt/splunk/var"},{"name":"mnt-splunk-secrets","mountPath":"/mnt/splunk-secrets"}],"livenessProbe":{"exec":{"command":["/sbin/checkstate.sh"]},"initialDelaySeconds":300,"timeoutSeconds":30,"periodSeconds":30},"readinessProbe":{"exec":{"command":["/bin/grep","started","/opt/container_artifact/splunk-container.state"]},"initialDelaySeconds":10,"timeoutSeconds":5,"periodSeconds":5},"imagePullPolicy":"IfNotPresent"}],"securityContext":{"runAsUser":41812,"fsGroup":41812},"affinity":{"podAntiAffinity":{"preferredDuringSchedulingIgnoredDuringExecution":[{"weight":100,"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app.kubernetes.io/instance","operator":"In","values":["splunk-stack1-license-master"]}]},"topologyKey":"kubernetes.io/hostname"}}]}},"schedulerName":"default-scheduler"}},"volumeClaimTemplates":[{"metadata":{"name":"pvc-etc","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"license-master","app.kubernetes.io/instance":"splunk-stack1-license-master","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"license-master","app.kubernetes.io/part-of":"splunk-stack1-license-master"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"10Gi"}}},"status":{}},{"metadata":{"name":"pvc-var","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"license-master","app.kubernetes.io/instance":"splunk-stack1-license-master","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"license-master","app.kubernetes.io/part-of":"splunk-stack1-license-master"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"100Gi"}}},"status":{}}],"serviceName":"splunk-stack1-license-master-headless","podManagementPolicy":"Parallel","updateStrategy":{"type":"OnDelete"}},"status":{"replicas":0}}`) + test(`{"kind":"StatefulSet","apiVersion":"apps/v1","metadata":{"name":"splunk-stack1-license-master","namespace":"test","creationTimestamp":null,"ownerReferences":[{"apiVersion":"","kind":"","name":"stack1","uid":"","controller":true}]},"spec":{"replicas":1,"selector":{"matchLabels":{"app.kubernetes.io/component":"license-master","app.kubernetes.io/instance":"splunk-stack1-license-master","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"license-master","app.kubernetes.io/part-of":"splunk-stack1-license-master"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app.kubernetes.io/component":"license-master","app.kubernetes.io/instance":"splunk-stack1-license-master","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"license-master","app.kubernetes.io/part-of":"splunk-stack1-license-master"},"annotations":{"traffic.sidecar.istio.io/excludeOutboundPorts":"8089,8191,9997,7777,9000,17000,17500,19000","traffic.sidecar.istio.io/includeInboundPorts":"8000"}},"spec":{"volumes":[{"name":"mnt-splunk-secrets","secret":{"secretName":"splunk-stack1-license-master-secret-v1","defaultMode":420}}],"containers":[{"name":"splunk","image":"splunk/splunk","ports":[{"name":"http-splunkweb","containerPort":8000,"protocol":"TCP"},{"name":"https-splunkd","containerPort":8089,"protocol":"TCP"}],"env":[{"name":"SPLUNK_HOME","value":"/opt/splunk"},{"name":"SPLUNK_START_ARGS","value":"--accept-license"},{"name":"SPLUNK_DEFAULTS_URL","value":"/mnt/splunk-secrets/default.yml"},{"name":"SPLUNK_HOME_OWNERSHIP_ENFORCEMENT","value":"false"},{"name":"SPLUNK_ROLE","value":"splunk_license_master"},{"name":"SPLUNK_DECLARATIVE_ADMIN_PASSWORD","value":"true"}],"resources":{"limits":{"cpu":"4","memory":"8Gi"},"requests":{"cpu":"100m","memory":"512Mi"}},"volumeMounts":[{"name":"pvc-etc","mountPath":"/opt/splunk/etc"},{"name":"pvc-var","mountPath":"/opt/splunk/var"},{"name":"mnt-splunk-secrets","mountPath":"/mnt/splunk-secrets"}],"livenessProbe":{"exec":{"command":["/sbin/checkstate.sh"]},"initialDelaySeconds":300,"timeoutSeconds":30,"periodSeconds":30},"readinessProbe":{"exec":{"command":["/bin/grep","started","/opt/container_artifact/splunk-container.state"]},"initialDelaySeconds":10,"timeoutSeconds":5,"periodSeconds":5},"imagePullPolicy":"IfNotPresent"}],"securityContext":{"runAsUser":41812,"fsGroup":41812},"affinity":{"podAntiAffinity":{"preferredDuringSchedulingIgnoredDuringExecution":[{"weight":100,"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app.kubernetes.io/instance","operator":"In","values":["splunk-stack1-license-master"]}]},"topologyKey":"kubernetes.io/hostname"}}]}},"schedulerName":"default-scheduler"}},"volumeClaimTemplates":[{"metadata":{"name":"pvc-etc","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"license-master","app.kubernetes.io/instance":"splunk-stack1-license-master","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"license-master","app.kubernetes.io/part-of":"splunk-stack1-license-master"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"10Gi"}}},"status":{}},{"metadata":{"name":"pvc-var","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"license-master","app.kubernetes.io/instance":"splunk-stack1-license-master","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"license-master","app.kubernetes.io/part-of":"splunk-stack1-license-master"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"100Gi"}}},"status":{}}],"serviceName":"splunk-stack1-license-master-headless","podManagementPolicy":"Parallel","updateStrategy":{"type":"OnDelete"}},"status":{"replicas":0}}`) cr.Spec.LicenseURL = "/mnt/splunk.lic" - test(`{"kind":"StatefulSet","apiVersion":"apps/v1","metadata":{"name":"splunk-stack1-license-master","namespace":"test","creationTimestamp":null,"ownerReferences":[{"apiVersion":"","kind":"","name":"stack1","uid":"","controller":true}]},"spec":{"replicas":1,"selector":{"matchLabels":{"app.kubernetes.io/component":"license-master","app.kubernetes.io/instance":"splunk-stack1-license-master","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"license-master","app.kubernetes.io/part-of":"splunk-stack1-license-master"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app.kubernetes.io/component":"license-master","app.kubernetes.io/instance":"splunk-stack1-license-master","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"license-master","app.kubernetes.io/part-of":"splunk-stack1-license-master"},"annotations":{"traffic.sidecar.istio.io/excludeOutboundPorts":"8089,8191,9997,7777,9000,17000,17500,19000","traffic.sidecar.istio.io/includeInboundPorts":"8000"}},"spec":{"volumes":[{"name":"mnt-splunk-secrets","secret":{"secretName":"splunk-stack1-license-master-secret-v1","defaultMode":420}}],"containers":[{"name":"splunk","image":"splunk/splunk","ports":[{"name":"splunkweb","containerPort":8000,"protocol":"TCP"},{"name":"splunkd","containerPort":8089,"protocol":"TCP"}],"env":[{"name":"SPLUNK_HOME","value":"/opt/splunk"},{"name":"SPLUNK_START_ARGS","value":"--accept-license"},{"name":"SPLUNK_DEFAULTS_URL","value":"/mnt/splunk-secrets/default.yml"},{"name":"SPLUNK_HOME_OWNERSHIP_ENFORCEMENT","value":"false"},{"name":"SPLUNK_ROLE","value":"splunk_license_master"},{"name":"SPLUNK_DECLARATIVE_ADMIN_PASSWORD","value":"true"},{"name":"SPLUNK_LICENSE_URI","value":"/mnt/splunk.lic"}],"resources":{"limits":{"cpu":"4","memory":"8Gi"},"requests":{"cpu":"100m","memory":"512Mi"}},"volumeMounts":[{"name":"pvc-etc","mountPath":"/opt/splunk/etc"},{"name":"pvc-var","mountPath":"/opt/splunk/var"},{"name":"mnt-splunk-secrets","mountPath":"/mnt/splunk-secrets"}],"livenessProbe":{"exec":{"command":["/sbin/checkstate.sh"]},"initialDelaySeconds":300,"timeoutSeconds":30,"periodSeconds":30},"readinessProbe":{"exec":{"command":["/bin/grep","started","/opt/container_artifact/splunk-container.state"]},"initialDelaySeconds":10,"timeoutSeconds":5,"periodSeconds":5},"imagePullPolicy":"IfNotPresent"}],"securityContext":{"runAsUser":41812,"fsGroup":41812},"affinity":{"podAntiAffinity":{"preferredDuringSchedulingIgnoredDuringExecution":[{"weight":100,"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app.kubernetes.io/instance","operator":"In","values":["splunk-stack1-license-master"]}]},"topologyKey":"kubernetes.io/hostname"}}]}},"schedulerName":"default-scheduler"}},"volumeClaimTemplates":[{"metadata":{"name":"pvc-etc","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"license-master","app.kubernetes.io/instance":"splunk-stack1-license-master","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"license-master","app.kubernetes.io/part-of":"splunk-stack1-license-master"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"10Gi"}}},"status":{}},{"metadata":{"name":"pvc-var","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"license-master","app.kubernetes.io/instance":"splunk-stack1-license-master","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"license-master","app.kubernetes.io/part-of":"splunk-stack1-license-master"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"100Gi"}}},"status":{}}],"serviceName":"splunk-stack1-license-master-headless","podManagementPolicy":"Parallel","updateStrategy":{"type":"OnDelete"}},"status":{"replicas":0}}`) + test(`{"kind":"StatefulSet","apiVersion":"apps/v1","metadata":{"name":"splunk-stack1-license-master","namespace":"test","creationTimestamp":null,"ownerReferences":[{"apiVersion":"","kind":"","name":"stack1","uid":"","controller":true}]},"spec":{"replicas":1,"selector":{"matchLabels":{"app.kubernetes.io/component":"license-master","app.kubernetes.io/instance":"splunk-stack1-license-master","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"license-master","app.kubernetes.io/part-of":"splunk-stack1-license-master"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app.kubernetes.io/component":"license-master","app.kubernetes.io/instance":"splunk-stack1-license-master","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"license-master","app.kubernetes.io/part-of":"splunk-stack1-license-master"},"annotations":{"traffic.sidecar.istio.io/excludeOutboundPorts":"8089,8191,9997,7777,9000,17000,17500,19000","traffic.sidecar.istio.io/includeInboundPorts":"8000"}},"spec":{"volumes":[{"name":"mnt-splunk-secrets","secret":{"secretName":"splunk-stack1-license-master-secret-v1","defaultMode":420}}],"containers":[{"name":"splunk","image":"splunk/splunk","ports":[{"name":"http-splunkweb","containerPort":8000,"protocol":"TCP"},{"name":"https-splunkd","containerPort":8089,"protocol":"TCP"}],"env":[{"name":"SPLUNK_HOME","value":"/opt/splunk"},{"name":"SPLUNK_START_ARGS","value":"--accept-license"},{"name":"SPLUNK_DEFAULTS_URL","value":"/mnt/splunk-secrets/default.yml"},{"name":"SPLUNK_HOME_OWNERSHIP_ENFORCEMENT","value":"false"},{"name":"SPLUNK_ROLE","value":"splunk_license_master"},{"name":"SPLUNK_DECLARATIVE_ADMIN_PASSWORD","value":"true"},{"name":"SPLUNK_LICENSE_URI","value":"/mnt/splunk.lic"}],"resources":{"limits":{"cpu":"4","memory":"8Gi"},"requests":{"cpu":"100m","memory":"512Mi"}},"volumeMounts":[{"name":"pvc-etc","mountPath":"/opt/splunk/etc"},{"name":"pvc-var","mountPath":"/opt/splunk/var"},{"name":"mnt-splunk-secrets","mountPath":"/mnt/splunk-secrets"}],"livenessProbe":{"exec":{"command":["/sbin/checkstate.sh"]},"initialDelaySeconds":300,"timeoutSeconds":30,"periodSeconds":30},"readinessProbe":{"exec":{"command":["/bin/grep","started","/opt/container_artifact/splunk-container.state"]},"initialDelaySeconds":10,"timeoutSeconds":5,"periodSeconds":5},"imagePullPolicy":"IfNotPresent"}],"securityContext":{"runAsUser":41812,"fsGroup":41812},"affinity":{"podAntiAffinity":{"preferredDuringSchedulingIgnoredDuringExecution":[{"weight":100,"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app.kubernetes.io/instance","operator":"In","values":["splunk-stack1-license-master"]}]},"topologyKey":"kubernetes.io/hostname"}}]}},"schedulerName":"default-scheduler"}},"volumeClaimTemplates":[{"metadata":{"name":"pvc-etc","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"license-master","app.kubernetes.io/instance":"splunk-stack1-license-master","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"license-master","app.kubernetes.io/part-of":"splunk-stack1-license-master"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"10Gi"}}},"status":{}},{"metadata":{"name":"pvc-var","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"license-master","app.kubernetes.io/instance":"splunk-stack1-license-master","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"license-master","app.kubernetes.io/part-of":"splunk-stack1-license-master"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"100Gi"}}},"status":{}}],"serviceName":"splunk-stack1-license-master-headless","podManagementPolicy":"Parallel","updateStrategy":{"type":"OnDelete"}},"status":{"replicas":0}}`) } diff --git a/pkg/splunk/enterprise/searchheadcluster_test.go b/pkg/splunk/enterprise/searchheadcluster_test.go index cf48380ad..3cb106aec 100644 --- a/pkg/splunk/enterprise/searchheadcluster_test.go +++ b/pkg/splunk/enterprise/searchheadcluster_test.go @@ -544,23 +544,23 @@ func TestGetSearchHeadStatefulSet(t *testing.T) { } cr.Spec.Replicas = 3 - test(`{"kind":"StatefulSet","apiVersion":"apps/v1","metadata":{"name":"splunk-stack1-search-head","namespace":"test","creationTimestamp":null,"ownerReferences":[{"apiVersion":"","kind":"","name":"stack1","uid":"","controller":true}]},"spec":{"replicas":3,"selector":{"matchLabels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-search-head","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"search-head","app.kubernetes.io/part-of":"splunk-stack1-search-head"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-search-head","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"search-head","app.kubernetes.io/part-of":"splunk-stack1-search-head"},"annotations":{"traffic.sidecar.istio.io/excludeOutboundPorts":"8089,8191,9997,7777,9000,17000,17500,19000","traffic.sidecar.istio.io/includeInboundPorts":"8000"}},"spec":{"volumes":[{"name":"mnt-splunk-secrets","secret":{"secretName":"splunk-stack1-search-head-secret-v1","defaultMode":420}}],"containers":[{"name":"splunk","image":"splunk/splunk","ports":[{"name":"splunkweb","containerPort":8000,"protocol":"TCP"},{"name":"splunkd","containerPort":8089,"protocol":"TCP"},{"name":"dfsmaster","containerPort":9000,"protocol":"TCP"},{"name":"dfccontrol","containerPort":17000,"protocol":"TCP"},{"name":"datareceive","containerPort":19000,"protocol":"TCP"}],"env":[{"name":"SPLUNK_HOME","value":"/opt/splunk"},{"name":"SPLUNK_START_ARGS","value":"--accept-license"},{"name":"SPLUNK_DEFAULTS_URL","value":"/mnt/splunk-secrets/default.yml"},{"name":"SPLUNK_HOME_OWNERSHIP_ENFORCEMENT","value":"false"},{"name":"SPLUNK_ROLE","value":"splunk_search_head"},{"name":"SPLUNK_DECLARATIVE_ADMIN_PASSWORD","value":"true"},{"name":"SPLUNK_SEARCH_HEAD_URL","value":"splunk-stack1-search-head-0.splunk-stack1-search-head-headless.test.svc.cluster.local,splunk-stack1-search-head-1.splunk-stack1-search-head-headless.test.svc.cluster.local,splunk-stack1-search-head-2.splunk-stack1-search-head-headless.test.svc.cluster.local"},{"name":"SPLUNK_SEARCH_HEAD_CAPTAIN_URL","value":"splunk-stack1-search-head-0.splunk-stack1-search-head-headless.test.svc.cluster.local"},{"name":"SPLUNK_DEPLOYER_URL","value":"splunk-stack1-deployer-service"}],"resources":{"limits":{"cpu":"4","memory":"8Gi"},"requests":{"cpu":"100m","memory":"512Mi"}},"volumeMounts":[{"name":"pvc-etc","mountPath":"/opt/splunk/etc"},{"name":"pvc-var","mountPath":"/opt/splunk/var"},{"name":"mnt-splunk-secrets","mountPath":"/mnt/splunk-secrets"}],"livenessProbe":{"exec":{"command":["/sbin/checkstate.sh"]},"initialDelaySeconds":300,"timeoutSeconds":30,"periodSeconds":30},"readinessProbe":{"exec":{"command":["/bin/grep","started","/opt/container_artifact/splunk-container.state"]},"initialDelaySeconds":10,"timeoutSeconds":5,"periodSeconds":5},"imagePullPolicy":"IfNotPresent"}],"securityContext":{"runAsUser":41812,"fsGroup":41812},"affinity":{"podAntiAffinity":{"preferredDuringSchedulingIgnoredDuringExecution":[{"weight":100,"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app.kubernetes.io/instance","operator":"In","values":["splunk-stack1-search-head"]}]},"topologyKey":"kubernetes.io/hostname"}}]}},"schedulerName":"default-scheduler"}},"volumeClaimTemplates":[{"metadata":{"name":"pvc-etc","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-search-head","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"search-head","app.kubernetes.io/part-of":"splunk-stack1-search-head"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"10Gi"}}},"status":{}},{"metadata":{"name":"pvc-var","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-search-head","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"search-head","app.kubernetes.io/part-of":"splunk-stack1-search-head"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"100Gi"}}},"status":{}}],"serviceName":"splunk-stack1-search-head-headless","podManagementPolicy":"Parallel","updateStrategy":{"type":"OnDelete"}},"status":{"replicas":0}}`) + test(`{"kind":"StatefulSet","apiVersion":"apps/v1","metadata":{"name":"splunk-stack1-search-head","namespace":"test","creationTimestamp":null,"ownerReferences":[{"apiVersion":"","kind":"","name":"stack1","uid":"","controller":true}]},"spec":{"replicas":3,"selector":{"matchLabels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-search-head","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"search-head","app.kubernetes.io/part-of":"splunk-stack1-search-head"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-search-head","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"search-head","app.kubernetes.io/part-of":"splunk-stack1-search-head"},"annotations":{"traffic.sidecar.istio.io/excludeOutboundPorts":"8089,8191,9997,7777,9000,17000,17500,19000","traffic.sidecar.istio.io/includeInboundPorts":"8000"}},"spec":{"volumes":[{"name":"mnt-splunk-secrets","secret":{"secretName":"splunk-stack1-search-head-secret-v1","defaultMode":420}}],"containers":[{"name":"splunk","image":"splunk/splunk","ports":[{"name":"http-splunkweb","containerPort":8000,"protocol":"TCP"},{"name":"https-splunkd","containerPort":8089,"protocol":"TCP"},{"name":"tcp-dfsmaster","containerPort":9000,"protocol":"TCP"},{"name":"tcp-dfccontrol","containerPort":17000,"protocol":"TCP"},{"name":"tcp-datareceive","containerPort":19000,"protocol":"TCP"}],"env":[{"name":"SPLUNK_HOME","value":"/opt/splunk"},{"name":"SPLUNK_START_ARGS","value":"--accept-license"},{"name":"SPLUNK_DEFAULTS_URL","value":"/mnt/splunk-secrets/default.yml"},{"name":"SPLUNK_HOME_OWNERSHIP_ENFORCEMENT","value":"false"},{"name":"SPLUNK_ROLE","value":"splunk_search_head"},{"name":"SPLUNK_DECLARATIVE_ADMIN_PASSWORD","value":"true"},{"name":"SPLUNK_SEARCH_HEAD_URL","value":"splunk-stack1-search-head-0.splunk-stack1-search-head-headless.test.svc.cluster.local,splunk-stack1-search-head-1.splunk-stack1-search-head-headless.test.svc.cluster.local,splunk-stack1-search-head-2.splunk-stack1-search-head-headless.test.svc.cluster.local"},{"name":"SPLUNK_SEARCH_HEAD_CAPTAIN_URL","value":"splunk-stack1-search-head-0.splunk-stack1-search-head-headless.test.svc.cluster.local"},{"name":"SPLUNK_DEPLOYER_URL","value":"splunk-stack1-deployer-service"}],"resources":{"limits":{"cpu":"4","memory":"8Gi"},"requests":{"cpu":"100m","memory":"512Mi"}},"volumeMounts":[{"name":"pvc-etc","mountPath":"/opt/splunk/etc"},{"name":"pvc-var","mountPath":"/opt/splunk/var"},{"name":"mnt-splunk-secrets","mountPath":"/mnt/splunk-secrets"}],"livenessProbe":{"exec":{"command":["/sbin/checkstate.sh"]},"initialDelaySeconds":300,"timeoutSeconds":30,"periodSeconds":30},"readinessProbe":{"exec":{"command":["/bin/grep","started","/opt/container_artifact/splunk-container.state"]},"initialDelaySeconds":10,"timeoutSeconds":5,"periodSeconds":5},"imagePullPolicy":"IfNotPresent"}],"securityContext":{"runAsUser":41812,"fsGroup":41812},"affinity":{"podAntiAffinity":{"preferredDuringSchedulingIgnoredDuringExecution":[{"weight":100,"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app.kubernetes.io/instance","operator":"In","values":["splunk-stack1-search-head"]}]},"topologyKey":"kubernetes.io/hostname"}}]}},"schedulerName":"default-scheduler"}},"volumeClaimTemplates":[{"metadata":{"name":"pvc-etc","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-search-head","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"search-head","app.kubernetes.io/part-of":"splunk-stack1-search-head"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"10Gi"}}},"status":{}},{"metadata":{"name":"pvc-var","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-search-head","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"search-head","app.kubernetes.io/part-of":"splunk-stack1-search-head"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"100Gi"}}},"status":{}}],"serviceName":"splunk-stack1-search-head-headless","podManagementPolicy":"Parallel","updateStrategy":{"type":"OnDelete"}},"status":{"replicas":0}}`) cr.Spec.Replicas = 4 - test(`{"kind":"StatefulSet","apiVersion":"apps/v1","metadata":{"name":"splunk-stack1-search-head","namespace":"test","creationTimestamp":null,"ownerReferences":[{"apiVersion":"","kind":"","name":"stack1","uid":"","controller":true}]},"spec":{"replicas":4,"selector":{"matchLabels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-search-head","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"search-head","app.kubernetes.io/part-of":"splunk-stack1-search-head"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-search-head","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"search-head","app.kubernetes.io/part-of":"splunk-stack1-search-head"},"annotations":{"traffic.sidecar.istio.io/excludeOutboundPorts":"8089,8191,9997,7777,9000,17000,17500,19000","traffic.sidecar.istio.io/includeInboundPorts":"8000"}},"spec":{"volumes":[{"name":"mnt-splunk-secrets","secret":{"secretName":"splunk-stack1-search-head-secret-v1","defaultMode":420}}],"containers":[{"name":"splunk","image":"splunk/splunk","ports":[{"name":"splunkweb","containerPort":8000,"protocol":"TCP"},{"name":"splunkd","containerPort":8089,"protocol":"TCP"},{"name":"dfsmaster","containerPort":9000,"protocol":"TCP"},{"name":"dfccontrol","containerPort":17000,"protocol":"TCP"},{"name":"datareceive","containerPort":19000,"protocol":"TCP"}],"env":[{"name":"SPLUNK_HOME","value":"/opt/splunk"},{"name":"SPLUNK_START_ARGS","value":"--accept-license"},{"name":"SPLUNK_DEFAULTS_URL","value":"/mnt/splunk-secrets/default.yml"},{"name":"SPLUNK_HOME_OWNERSHIP_ENFORCEMENT","value":"false"},{"name":"SPLUNK_ROLE","value":"splunk_search_head"},{"name":"SPLUNK_DECLARATIVE_ADMIN_PASSWORD","value":"true"},{"name":"SPLUNK_SEARCH_HEAD_URL","value":"splunk-stack1-search-head-0.splunk-stack1-search-head-headless.test.svc.cluster.local,splunk-stack1-search-head-1.splunk-stack1-search-head-headless.test.svc.cluster.local,splunk-stack1-search-head-2.splunk-stack1-search-head-headless.test.svc.cluster.local,splunk-stack1-search-head-3.splunk-stack1-search-head-headless.test.svc.cluster.local"},{"name":"SPLUNK_SEARCH_HEAD_CAPTAIN_URL","value":"splunk-stack1-search-head-0.splunk-stack1-search-head-headless.test.svc.cluster.local"},{"name":"SPLUNK_DEPLOYER_URL","value":"splunk-stack1-deployer-service"}],"resources":{"limits":{"cpu":"4","memory":"8Gi"},"requests":{"cpu":"100m","memory":"512Mi"}},"volumeMounts":[{"name":"pvc-etc","mountPath":"/opt/splunk/etc"},{"name":"pvc-var","mountPath":"/opt/splunk/var"},{"name":"mnt-splunk-secrets","mountPath":"/mnt/splunk-secrets"}],"livenessProbe":{"exec":{"command":["/sbin/checkstate.sh"]},"initialDelaySeconds":300,"timeoutSeconds":30,"periodSeconds":30},"readinessProbe":{"exec":{"command":["/bin/grep","started","/opt/container_artifact/splunk-container.state"]},"initialDelaySeconds":10,"timeoutSeconds":5,"periodSeconds":5},"imagePullPolicy":"IfNotPresent"}],"securityContext":{"runAsUser":41812,"fsGroup":41812},"affinity":{"podAntiAffinity":{"preferredDuringSchedulingIgnoredDuringExecution":[{"weight":100,"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app.kubernetes.io/instance","operator":"In","values":["splunk-stack1-search-head"]}]},"topologyKey":"kubernetes.io/hostname"}}]}},"schedulerName":"default-scheduler"}},"volumeClaimTemplates":[{"metadata":{"name":"pvc-etc","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-search-head","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"search-head","app.kubernetes.io/part-of":"splunk-stack1-search-head"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"10Gi"}}},"status":{}},{"metadata":{"name":"pvc-var","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-search-head","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"search-head","app.kubernetes.io/part-of":"splunk-stack1-search-head"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"100Gi"}}},"status":{}}],"serviceName":"splunk-stack1-search-head-headless","podManagementPolicy":"Parallel","updateStrategy":{"type":"OnDelete"}},"status":{"replicas":0}}`) + test(`{"kind":"StatefulSet","apiVersion":"apps/v1","metadata":{"name":"splunk-stack1-search-head","namespace":"test","creationTimestamp":null,"ownerReferences":[{"apiVersion":"","kind":"","name":"stack1","uid":"","controller":true}]},"spec":{"replicas":4,"selector":{"matchLabels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-search-head","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"search-head","app.kubernetes.io/part-of":"splunk-stack1-search-head"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-search-head","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"search-head","app.kubernetes.io/part-of":"splunk-stack1-search-head"},"annotations":{"traffic.sidecar.istio.io/excludeOutboundPorts":"8089,8191,9997,7777,9000,17000,17500,19000","traffic.sidecar.istio.io/includeInboundPorts":"8000"}},"spec":{"volumes":[{"name":"mnt-splunk-secrets","secret":{"secretName":"splunk-stack1-search-head-secret-v1","defaultMode":420}}],"containers":[{"name":"splunk","image":"splunk/splunk","ports":[{"name":"http-splunkweb","containerPort":8000,"protocol":"TCP"},{"name":"https-splunkd","containerPort":8089,"protocol":"TCP"},{"name":"tcp-dfsmaster","containerPort":9000,"protocol":"TCP"},{"name":"tcp-dfccontrol","containerPort":17000,"protocol":"TCP"},{"name":"tcp-datareceive","containerPort":19000,"protocol":"TCP"}],"env":[{"name":"SPLUNK_HOME","value":"/opt/splunk"},{"name":"SPLUNK_START_ARGS","value":"--accept-license"},{"name":"SPLUNK_DEFAULTS_URL","value":"/mnt/splunk-secrets/default.yml"},{"name":"SPLUNK_HOME_OWNERSHIP_ENFORCEMENT","value":"false"},{"name":"SPLUNK_ROLE","value":"splunk_search_head"},{"name":"SPLUNK_DECLARATIVE_ADMIN_PASSWORD","value":"true"},{"name":"SPLUNK_SEARCH_HEAD_URL","value":"splunk-stack1-search-head-0.splunk-stack1-search-head-headless.test.svc.cluster.local,splunk-stack1-search-head-1.splunk-stack1-search-head-headless.test.svc.cluster.local,splunk-stack1-search-head-2.splunk-stack1-search-head-headless.test.svc.cluster.local,splunk-stack1-search-head-3.splunk-stack1-search-head-headless.test.svc.cluster.local"},{"name":"SPLUNK_SEARCH_HEAD_CAPTAIN_URL","value":"splunk-stack1-search-head-0.splunk-stack1-search-head-headless.test.svc.cluster.local"},{"name":"SPLUNK_DEPLOYER_URL","value":"splunk-stack1-deployer-service"}],"resources":{"limits":{"cpu":"4","memory":"8Gi"},"requests":{"cpu":"100m","memory":"512Mi"}},"volumeMounts":[{"name":"pvc-etc","mountPath":"/opt/splunk/etc"},{"name":"pvc-var","mountPath":"/opt/splunk/var"},{"name":"mnt-splunk-secrets","mountPath":"/mnt/splunk-secrets"}],"livenessProbe":{"exec":{"command":["/sbin/checkstate.sh"]},"initialDelaySeconds":300,"timeoutSeconds":30,"periodSeconds":30},"readinessProbe":{"exec":{"command":["/bin/grep","started","/opt/container_artifact/splunk-container.state"]},"initialDelaySeconds":10,"timeoutSeconds":5,"periodSeconds":5},"imagePullPolicy":"IfNotPresent"}],"securityContext":{"runAsUser":41812,"fsGroup":41812},"affinity":{"podAntiAffinity":{"preferredDuringSchedulingIgnoredDuringExecution":[{"weight":100,"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app.kubernetes.io/instance","operator":"In","values":["splunk-stack1-search-head"]}]},"topologyKey":"kubernetes.io/hostname"}}]}},"schedulerName":"default-scheduler"}},"volumeClaimTemplates":[{"metadata":{"name":"pvc-etc","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-search-head","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"search-head","app.kubernetes.io/part-of":"splunk-stack1-search-head"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"10Gi"}}},"status":{}},{"metadata":{"name":"pvc-var","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-search-head","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"search-head","app.kubernetes.io/part-of":"splunk-stack1-search-head"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"100Gi"}}},"status":{}}],"serviceName":"splunk-stack1-search-head-headless","podManagementPolicy":"Parallel","updateStrategy":{"type":"OnDelete"}},"status":{"replicas":0}}`) cr.Spec.Replicas = 5 cr.Spec.ClusterMasterRef.Name = "stack1" - test(`{"kind":"StatefulSet","apiVersion":"apps/v1","metadata":{"name":"splunk-stack1-search-head","namespace":"test","creationTimestamp":null,"ownerReferences":[{"apiVersion":"","kind":"","name":"stack1","uid":"","controller":true}]},"spec":{"replicas":5,"selector":{"matchLabels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-search-head","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"search-head","app.kubernetes.io/part-of":"splunk-stack1-search-head"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-search-head","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"search-head","app.kubernetes.io/part-of":"splunk-stack1-search-head"},"annotations":{"traffic.sidecar.istio.io/excludeOutboundPorts":"8089,8191,9997,7777,9000,17000,17500,19000","traffic.sidecar.istio.io/includeInboundPorts":"8000"}},"spec":{"volumes":[{"name":"mnt-splunk-secrets","secret":{"secretName":"splunk-stack1-search-head-secret-v1","defaultMode":420}}],"containers":[{"name":"splunk","image":"splunk/splunk","ports":[{"name":"splunkweb","containerPort":8000,"protocol":"TCP"},{"name":"splunkd","containerPort":8089,"protocol":"TCP"},{"name":"dfsmaster","containerPort":9000,"protocol":"TCP"},{"name":"dfccontrol","containerPort":17000,"protocol":"TCP"},{"name":"datareceive","containerPort":19000,"protocol":"TCP"}],"env":[{"name":"SPLUNK_HOME","value":"/opt/splunk"},{"name":"SPLUNK_START_ARGS","value":"--accept-license"},{"name":"SPLUNK_DEFAULTS_URL","value":"/mnt/splunk-secrets/default.yml"},{"name":"SPLUNK_HOME_OWNERSHIP_ENFORCEMENT","value":"false"},{"name":"SPLUNK_ROLE","value":"splunk_search_head"},{"name":"SPLUNK_DECLARATIVE_ADMIN_PASSWORD","value":"true"},{"name":"SPLUNK_SEARCH_HEAD_URL","value":"splunk-stack1-search-head-0.splunk-stack1-search-head-headless.test.svc.cluster.local,splunk-stack1-search-head-1.splunk-stack1-search-head-headless.test.svc.cluster.local,splunk-stack1-search-head-2.splunk-stack1-search-head-headless.test.svc.cluster.local,splunk-stack1-search-head-3.splunk-stack1-search-head-headless.test.svc.cluster.local,splunk-stack1-search-head-4.splunk-stack1-search-head-headless.test.svc.cluster.local"},{"name":"SPLUNK_SEARCH_HEAD_CAPTAIN_URL","value":"splunk-stack1-search-head-0.splunk-stack1-search-head-headless.test.svc.cluster.local"},{"name":"SPLUNK_DEPLOYER_URL","value":"splunk-stack1-deployer-service"},{"name":"SPLUNK_CLUSTER_MASTER_URL","value":"splunk-stack1-cluster-master-service"}],"resources":{"limits":{"cpu":"4","memory":"8Gi"},"requests":{"cpu":"100m","memory":"512Mi"}},"volumeMounts":[{"name":"pvc-etc","mountPath":"/opt/splunk/etc"},{"name":"pvc-var","mountPath":"/opt/splunk/var"},{"name":"mnt-splunk-secrets","mountPath":"/mnt/splunk-secrets"}],"livenessProbe":{"exec":{"command":["/sbin/checkstate.sh"]},"initialDelaySeconds":300,"timeoutSeconds":30,"periodSeconds":30},"readinessProbe":{"exec":{"command":["/bin/grep","started","/opt/container_artifact/splunk-container.state"]},"initialDelaySeconds":10,"timeoutSeconds":5,"periodSeconds":5},"imagePullPolicy":"IfNotPresent"}],"securityContext":{"runAsUser":41812,"fsGroup":41812},"affinity":{"podAntiAffinity":{"preferredDuringSchedulingIgnoredDuringExecution":[{"weight":100,"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app.kubernetes.io/instance","operator":"In","values":["splunk-stack1-search-head"]}]},"topologyKey":"kubernetes.io/hostname"}}]}},"schedulerName":"default-scheduler"}},"volumeClaimTemplates":[{"metadata":{"name":"pvc-etc","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-search-head","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"search-head","app.kubernetes.io/part-of":"splunk-stack1-search-head"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"10Gi"}}},"status":{}},{"metadata":{"name":"pvc-var","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-search-head","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"search-head","app.kubernetes.io/part-of":"splunk-stack1-search-head"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"100Gi"}}},"status":{}}],"serviceName":"splunk-stack1-search-head-headless","podManagementPolicy":"Parallel","updateStrategy":{"type":"OnDelete"}},"status":{"replicas":0}}`) + test(`{"kind":"StatefulSet","apiVersion":"apps/v1","metadata":{"name":"splunk-stack1-search-head","namespace":"test","creationTimestamp":null,"ownerReferences":[{"apiVersion":"","kind":"","name":"stack1","uid":"","controller":true}]},"spec":{"replicas":5,"selector":{"matchLabels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-search-head","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"search-head","app.kubernetes.io/part-of":"splunk-stack1-search-head"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-search-head","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"search-head","app.kubernetes.io/part-of":"splunk-stack1-search-head"},"annotations":{"traffic.sidecar.istio.io/excludeOutboundPorts":"8089,8191,9997,7777,9000,17000,17500,19000","traffic.sidecar.istio.io/includeInboundPorts":"8000"}},"spec":{"volumes":[{"name":"mnt-splunk-secrets","secret":{"secretName":"splunk-stack1-search-head-secret-v1","defaultMode":420}}],"containers":[{"name":"splunk","image":"splunk/splunk","ports":[{"name":"http-splunkweb","containerPort":8000,"protocol":"TCP"},{"name":"https-splunkd","containerPort":8089,"protocol":"TCP"},{"name":"tcp-dfsmaster","containerPort":9000,"protocol":"TCP"},{"name":"tcp-dfccontrol","containerPort":17000,"protocol":"TCP"},{"name":"tcp-datareceive","containerPort":19000,"protocol":"TCP"}],"env":[{"name":"SPLUNK_HOME","value":"/opt/splunk"},{"name":"SPLUNK_START_ARGS","value":"--accept-license"},{"name":"SPLUNK_DEFAULTS_URL","value":"/mnt/splunk-secrets/default.yml"},{"name":"SPLUNK_HOME_OWNERSHIP_ENFORCEMENT","value":"false"},{"name":"SPLUNK_ROLE","value":"splunk_search_head"},{"name":"SPLUNK_DECLARATIVE_ADMIN_PASSWORD","value":"true"},{"name":"SPLUNK_SEARCH_HEAD_URL","value":"splunk-stack1-search-head-0.splunk-stack1-search-head-headless.test.svc.cluster.local,splunk-stack1-search-head-1.splunk-stack1-search-head-headless.test.svc.cluster.local,splunk-stack1-search-head-2.splunk-stack1-search-head-headless.test.svc.cluster.local,splunk-stack1-search-head-3.splunk-stack1-search-head-headless.test.svc.cluster.local,splunk-stack1-search-head-4.splunk-stack1-search-head-headless.test.svc.cluster.local"},{"name":"SPLUNK_SEARCH_HEAD_CAPTAIN_URL","value":"splunk-stack1-search-head-0.splunk-stack1-search-head-headless.test.svc.cluster.local"},{"name":"SPLUNK_DEPLOYER_URL","value":"splunk-stack1-deployer-service"},{"name":"SPLUNK_CLUSTER_MASTER_URL","value":"splunk-stack1-cluster-master-service"}],"resources":{"limits":{"cpu":"4","memory":"8Gi"},"requests":{"cpu":"100m","memory":"512Mi"}},"volumeMounts":[{"name":"pvc-etc","mountPath":"/opt/splunk/etc"},{"name":"pvc-var","mountPath":"/opt/splunk/var"},{"name":"mnt-splunk-secrets","mountPath":"/mnt/splunk-secrets"}],"livenessProbe":{"exec":{"command":["/sbin/checkstate.sh"]},"initialDelaySeconds":300,"timeoutSeconds":30,"periodSeconds":30},"readinessProbe":{"exec":{"command":["/bin/grep","started","/opt/container_artifact/splunk-container.state"]},"initialDelaySeconds":10,"timeoutSeconds":5,"periodSeconds":5},"imagePullPolicy":"IfNotPresent"}],"securityContext":{"runAsUser":41812,"fsGroup":41812},"affinity":{"podAntiAffinity":{"preferredDuringSchedulingIgnoredDuringExecution":[{"weight":100,"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app.kubernetes.io/instance","operator":"In","values":["splunk-stack1-search-head"]}]},"topologyKey":"kubernetes.io/hostname"}}]}},"schedulerName":"default-scheduler"}},"volumeClaimTemplates":[{"metadata":{"name":"pvc-etc","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-search-head","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"search-head","app.kubernetes.io/part-of":"splunk-stack1-search-head"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"10Gi"}}},"status":{}},{"metadata":{"name":"pvc-var","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-search-head","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"search-head","app.kubernetes.io/part-of":"splunk-stack1-search-head"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"100Gi"}}},"status":{}}],"serviceName":"splunk-stack1-search-head-headless","podManagementPolicy":"Parallel","updateStrategy":{"type":"OnDelete"}},"status":{"replicas":0}}`) cr.Spec.Replicas = 6 cr.Spec.SparkRef.Name = cr.GetName() cr.Spec.ClusterMasterRef.Namespace = "test2" - test(`{"kind":"StatefulSet","apiVersion":"apps/v1","metadata":{"name":"splunk-stack1-search-head","namespace":"test","creationTimestamp":null,"ownerReferences":[{"apiVersion":"","kind":"","name":"stack1","uid":"","controller":true}]},"spec":{"replicas":6,"selector":{"matchLabels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-search-head","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"search-head","app.kubernetes.io/part-of":"splunk-stack1-search-head"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-search-head","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"search-head","app.kubernetes.io/part-of":"splunk-stack1-search-head"},"annotations":{"traffic.sidecar.istio.io/excludeOutboundPorts":"8089,8191,9997,7777,9000,17000,17500,19000","traffic.sidecar.istio.io/includeInboundPorts":"8000"}},"spec":{"volumes":[{"name":"mnt-splunk-secrets","secret":{"secretName":"splunk-stack1-search-head-secret-v1","defaultMode":420}},{"name":"mnt-splunk-jdk","emptyDir":{}},{"name":"mnt-splunk-spark","emptyDir":{}}],"initContainers":[{"name":"init","image":"splunk/spark","command":["bash","-c","cp -r /opt/jdk /mnt \u0026\u0026 cp -r /opt/spark /mnt"],"resources":{"limits":{"cpu":"1","memory":"512Mi"},"requests":{"cpu":"250m","memory":"128Mi"}},"volumeMounts":[{"name":"mnt-splunk-jdk","mountPath":"/mnt/jdk"},{"name":"mnt-splunk-spark","mountPath":"/mnt/spark"}],"imagePullPolicy":"IfNotPresent"}],"containers":[{"name":"splunk","image":"splunk/splunk","ports":[{"name":"splunkweb","containerPort":8000,"protocol":"TCP"},{"name":"splunkd","containerPort":8089,"protocol":"TCP"},{"name":"dfsmaster","containerPort":9000,"protocol":"TCP"},{"name":"dfccontrol","containerPort":17000,"protocol":"TCP"},{"name":"datareceive","containerPort":19000,"protocol":"TCP"}],"env":[{"name":"SPLUNK_HOME","value":"/opt/splunk"},{"name":"SPLUNK_START_ARGS","value":"--accept-license"},{"name":"SPLUNK_DEFAULTS_URL","value":"/mnt/splunk-secrets/default.yml"},{"name":"SPLUNK_HOME_OWNERSHIP_ENFORCEMENT","value":"false"},{"name":"SPLUNK_ROLE","value":"splunk_search_head"},{"name":"SPLUNK_DECLARATIVE_ADMIN_PASSWORD","value":"true"},{"name":"SPLUNK_SEARCH_HEAD_URL","value":"splunk-stack1-search-head-0.splunk-stack1-search-head-headless.test.svc.cluster.local,splunk-stack1-search-head-1.splunk-stack1-search-head-headless.test.svc.cluster.local,splunk-stack1-search-head-2.splunk-stack1-search-head-headless.test.svc.cluster.local,splunk-stack1-search-head-3.splunk-stack1-search-head-headless.test.svc.cluster.local,splunk-stack1-search-head-4.splunk-stack1-search-head-headless.test.svc.cluster.local,splunk-stack1-search-head-5.splunk-stack1-search-head-headless.test.svc.cluster.local"},{"name":"SPLUNK_SEARCH_HEAD_CAPTAIN_URL","value":"splunk-stack1-search-head-0.splunk-stack1-search-head-headless.test.svc.cluster.local"},{"name":"SPLUNK_DEPLOYER_URL","value":"splunk-stack1-deployer-service"},{"name":"SPLUNK_CLUSTER_MASTER_URL","value":"splunk-stack1-cluster-master-service.test2.svc.cluster.local"},{"name":"SPLUNK_ENABLE_DFS","value":"true"},{"name":"SPARK_MASTER_HOST","value":"splunk-stack1-spark-master-service"},{"name":"SPARK_MASTER_WEBUI_PORT","value":"8009"},{"name":"SPARK_HOME","value":"/mnt/splunk-spark"},{"name":"JAVA_HOME","value":"/mnt/splunk-jdk"},{"name":"SPLUNK_DFW_NUM_SLOTS_ENABLED","value":"true"}],"resources":{"limits":{"cpu":"4","memory":"8Gi"},"requests":{"cpu":"100m","memory":"512Mi"}},"volumeMounts":[{"name":"pvc-etc","mountPath":"/opt/splunk/etc"},{"name":"pvc-var","mountPath":"/opt/splunk/var"},{"name":"mnt-splunk-secrets","mountPath":"/mnt/splunk-secrets"},{"name":"mnt-splunk-jdk","mountPath":"/mnt/splunk-jdk"},{"name":"mnt-splunk-spark","mountPath":"/mnt/splunk-spark"}],"livenessProbe":{"exec":{"command":["/sbin/checkstate.sh"]},"initialDelaySeconds":300,"timeoutSeconds":30,"periodSeconds":30},"readinessProbe":{"exec":{"command":["/bin/grep","started","/opt/container_artifact/splunk-container.state"]},"initialDelaySeconds":10,"timeoutSeconds":5,"periodSeconds":5},"imagePullPolicy":"IfNotPresent"}],"securityContext":{"runAsUser":41812,"fsGroup":41812},"affinity":{"podAntiAffinity":{"preferredDuringSchedulingIgnoredDuringExecution":[{"weight":100,"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app.kubernetes.io/instance","operator":"In","values":["splunk-stack1-search-head"]}]},"topologyKey":"kubernetes.io/hostname"}}]}},"schedulerName":"default-scheduler"}},"volumeClaimTemplates":[{"metadata":{"name":"pvc-etc","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-search-head","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"search-head","app.kubernetes.io/part-of":"splunk-stack1-search-head"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"10Gi"}}},"status":{}},{"metadata":{"name":"pvc-var","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-search-head","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"search-head","app.kubernetes.io/part-of":"splunk-stack1-search-head"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"100Gi"}}},"status":{}}],"serviceName":"splunk-stack1-search-head-headless","podManagementPolicy":"Parallel","updateStrategy":{"type":"OnDelete"}},"status":{"replicas":0}}`) + test(`{"kind":"StatefulSet","apiVersion":"apps/v1","metadata":{"name":"splunk-stack1-search-head","namespace":"test","creationTimestamp":null,"ownerReferences":[{"apiVersion":"","kind":"","name":"stack1","uid":"","controller":true}]},"spec":{"replicas":6,"selector":{"matchLabels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-search-head","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"search-head","app.kubernetes.io/part-of":"splunk-stack1-search-head"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-search-head","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"search-head","app.kubernetes.io/part-of":"splunk-stack1-search-head"},"annotations":{"traffic.sidecar.istio.io/excludeOutboundPorts":"8089,8191,9997,7777,9000,17000,17500,19000","traffic.sidecar.istio.io/includeInboundPorts":"8000"}},"spec":{"volumes":[{"name":"mnt-splunk-secrets","secret":{"secretName":"splunk-stack1-search-head-secret-v1","defaultMode":420}},{"name":"mnt-splunk-jdk","emptyDir":{}},{"name":"mnt-splunk-spark","emptyDir":{}}],"initContainers":[{"name":"init","image":"splunk/spark","command":["bash","-c","cp -r /opt/jdk /mnt \u0026\u0026 cp -r /opt/spark /mnt"],"resources":{"limits":{"cpu":"1","memory":"512Mi"},"requests":{"cpu":"250m","memory":"128Mi"}},"volumeMounts":[{"name":"mnt-splunk-jdk","mountPath":"/mnt/jdk"},{"name":"mnt-splunk-spark","mountPath":"/mnt/spark"}],"imagePullPolicy":"IfNotPresent"}],"containers":[{"name":"splunk","image":"splunk/splunk","ports":[{"name":"http-splunkweb","containerPort":8000,"protocol":"TCP"},{"name":"https-splunkd","containerPort":8089,"protocol":"TCP"},{"name":"tcp-dfsmaster","containerPort":9000,"protocol":"TCP"},{"name":"tcp-dfccontrol","containerPort":17000,"protocol":"TCP"},{"name":"tcp-datareceive","containerPort":19000,"protocol":"TCP"}],"env":[{"name":"SPLUNK_HOME","value":"/opt/splunk"},{"name":"SPLUNK_START_ARGS","value":"--accept-license"},{"name":"SPLUNK_DEFAULTS_URL","value":"/mnt/splunk-secrets/default.yml"},{"name":"SPLUNK_HOME_OWNERSHIP_ENFORCEMENT","value":"false"},{"name":"SPLUNK_ROLE","value":"splunk_search_head"},{"name":"SPLUNK_DECLARATIVE_ADMIN_PASSWORD","value":"true"},{"name":"SPLUNK_SEARCH_HEAD_URL","value":"splunk-stack1-search-head-0.splunk-stack1-search-head-headless.test.svc.cluster.local,splunk-stack1-search-head-1.splunk-stack1-search-head-headless.test.svc.cluster.local,splunk-stack1-search-head-2.splunk-stack1-search-head-headless.test.svc.cluster.local,splunk-stack1-search-head-3.splunk-stack1-search-head-headless.test.svc.cluster.local,splunk-stack1-search-head-4.splunk-stack1-search-head-headless.test.svc.cluster.local,splunk-stack1-search-head-5.splunk-stack1-search-head-headless.test.svc.cluster.local"},{"name":"SPLUNK_SEARCH_HEAD_CAPTAIN_URL","value":"splunk-stack1-search-head-0.splunk-stack1-search-head-headless.test.svc.cluster.local"},{"name":"SPLUNK_DEPLOYER_URL","value":"splunk-stack1-deployer-service"},{"name":"SPLUNK_CLUSTER_MASTER_URL","value":"splunk-stack1-cluster-master-service.test2.svc.cluster.local"},{"name":"SPLUNK_ENABLE_DFS","value":"true"},{"name":"SPARK_MASTER_HOST","value":"splunk-stack1-spark-master-service"},{"name":"SPARK_MASTER_WEBUI_PORT","value":"8009"},{"name":"SPARK_HOME","value":"/mnt/splunk-spark"},{"name":"JAVA_HOME","value":"/mnt/splunk-jdk"},{"name":"SPLUNK_DFW_NUM_SLOTS_ENABLED","value":"true"}],"resources":{"limits":{"cpu":"4","memory":"8Gi"},"requests":{"cpu":"100m","memory":"512Mi"}},"volumeMounts":[{"name":"pvc-etc","mountPath":"/opt/splunk/etc"},{"name":"pvc-var","mountPath":"/opt/splunk/var"},{"name":"mnt-splunk-secrets","mountPath":"/mnt/splunk-secrets"},{"name":"mnt-splunk-jdk","mountPath":"/mnt/splunk-jdk"},{"name":"mnt-splunk-spark","mountPath":"/mnt/splunk-spark"}],"livenessProbe":{"exec":{"command":["/sbin/checkstate.sh"]},"initialDelaySeconds":300,"timeoutSeconds":30,"periodSeconds":30},"readinessProbe":{"exec":{"command":["/bin/grep","started","/opt/container_artifact/splunk-container.state"]},"initialDelaySeconds":10,"timeoutSeconds":5,"periodSeconds":5},"imagePullPolicy":"IfNotPresent"}],"securityContext":{"runAsUser":41812,"fsGroup":41812},"affinity":{"podAntiAffinity":{"preferredDuringSchedulingIgnoredDuringExecution":[{"weight":100,"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app.kubernetes.io/instance","operator":"In","values":["splunk-stack1-search-head"]}]},"topologyKey":"kubernetes.io/hostname"}}]}},"schedulerName":"default-scheduler"}},"volumeClaimTemplates":[{"metadata":{"name":"pvc-etc","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-search-head","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"search-head","app.kubernetes.io/part-of":"splunk-stack1-search-head"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"10Gi"}}},"status":{}},{"metadata":{"name":"pvc-var","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-search-head","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"search-head","app.kubernetes.io/part-of":"splunk-stack1-search-head"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"100Gi"}}},"status":{}}],"serviceName":"splunk-stack1-search-head-headless","podManagementPolicy":"Parallel","updateStrategy":{"type":"OnDelete"}},"status":{"replicas":0}}`) // Define additional service port in CR and verified the statefulset has the new port cr.Spec.ServiceTemplate.Spec.Ports = []corev1.ServicePort{{Name: "user-defined", Port: 32000, Protocol: "UDP"}} - test(`{"kind":"StatefulSet","apiVersion":"apps/v1","metadata":{"name":"splunk-stack1-search-head","namespace":"test","creationTimestamp":null,"ownerReferences":[{"apiVersion":"","kind":"","name":"stack1","uid":"","controller":true}]},"spec":{"replicas":6,"selector":{"matchLabels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-search-head","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"search-head","app.kubernetes.io/part-of":"splunk-stack1-search-head"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-search-head","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"search-head","app.kubernetes.io/part-of":"splunk-stack1-search-head"},"annotations":{"traffic.sidecar.istio.io/excludeOutboundPorts":"8089,8191,9997,7777,9000,17000,17500,19000","traffic.sidecar.istio.io/includeInboundPorts":"8000"}},"spec":{"volumes":[{"name":"mnt-splunk-secrets","secret":{"secretName":"splunk-stack1-search-head-secret-v1","defaultMode":420}},{"name":"mnt-splunk-jdk","emptyDir":{}},{"name":"mnt-splunk-spark","emptyDir":{}}],"initContainers":[{"name":"init","image":"splunk/spark","command":["bash","-c","cp -r /opt/jdk /mnt \u0026\u0026 cp -r /opt/spark /mnt"],"resources":{"limits":{"cpu":"1","memory":"512Mi"},"requests":{"cpu":"250m","memory":"128Mi"}},"volumeMounts":[{"name":"mnt-splunk-jdk","mountPath":"/mnt/jdk"},{"name":"mnt-splunk-spark","mountPath":"/mnt/spark"}],"imagePullPolicy":"IfNotPresent"}],"containers":[{"name":"splunk","image":"splunk/splunk","ports":[{"name":"splunkweb","containerPort":8000,"protocol":"TCP"},{"name":"splunkd","containerPort":8089,"protocol":"TCP"},{"name":"dfsmaster","containerPort":9000,"protocol":"TCP"},{"name":"dfccontrol","containerPort":17000,"protocol":"TCP"},{"name":"datareceive","containerPort":19000,"protocol":"TCP"},{"name":"user-defined","containerPort":32000,"protocol":"UDP"}],"env":[{"name":"SPLUNK_HOME","value":"/opt/splunk"},{"name":"SPLUNK_START_ARGS","value":"--accept-license"},{"name":"SPLUNK_DEFAULTS_URL","value":"/mnt/splunk-secrets/default.yml"},{"name":"SPLUNK_HOME_OWNERSHIP_ENFORCEMENT","value":"false"},{"name":"SPLUNK_ROLE","value":"splunk_search_head"},{"name":"SPLUNK_DECLARATIVE_ADMIN_PASSWORD","value":"true"},{"name":"SPLUNK_SEARCH_HEAD_URL","value":"splunk-stack1-search-head-0.splunk-stack1-search-head-headless.test.svc.cluster.local,splunk-stack1-search-head-1.splunk-stack1-search-head-headless.test.svc.cluster.local,splunk-stack1-search-head-2.splunk-stack1-search-head-headless.test.svc.cluster.local,splunk-stack1-search-head-3.splunk-stack1-search-head-headless.test.svc.cluster.local,splunk-stack1-search-head-4.splunk-stack1-search-head-headless.test.svc.cluster.local,splunk-stack1-search-head-5.splunk-stack1-search-head-headless.test.svc.cluster.local"},{"name":"SPLUNK_SEARCH_HEAD_CAPTAIN_URL","value":"splunk-stack1-search-head-0.splunk-stack1-search-head-headless.test.svc.cluster.local"},{"name":"SPLUNK_DEPLOYER_URL","value":"splunk-stack1-deployer-service"},{"name":"SPLUNK_CLUSTER_MASTER_URL","value":"splunk-stack1-cluster-master-service.test2.svc.cluster.local"},{"name":"SPLUNK_ENABLE_DFS","value":"true"},{"name":"SPARK_MASTER_HOST","value":"splunk-stack1-spark-master-service"},{"name":"SPARK_MASTER_WEBUI_PORT","value":"8009"},{"name":"SPARK_HOME","value":"/mnt/splunk-spark"},{"name":"JAVA_HOME","value":"/mnt/splunk-jdk"},{"name":"SPLUNK_DFW_NUM_SLOTS_ENABLED","value":"true"}],"resources":{"limits":{"cpu":"4","memory":"8Gi"},"requests":{"cpu":"100m","memory":"512Mi"}},"volumeMounts":[{"name":"pvc-etc","mountPath":"/opt/splunk/etc"},{"name":"pvc-var","mountPath":"/opt/splunk/var"},{"name":"mnt-splunk-secrets","mountPath":"/mnt/splunk-secrets"},{"name":"mnt-splunk-jdk","mountPath":"/mnt/splunk-jdk"},{"name":"mnt-splunk-spark","mountPath":"/mnt/splunk-spark"}],"livenessProbe":{"exec":{"command":["/sbin/checkstate.sh"]},"initialDelaySeconds":300,"timeoutSeconds":30,"periodSeconds":30},"readinessProbe":{"exec":{"command":["/bin/grep","started","/opt/container_artifact/splunk-container.state"]},"initialDelaySeconds":10,"timeoutSeconds":5,"periodSeconds":5},"imagePullPolicy":"IfNotPresent"}],"securityContext":{"runAsUser":41812,"fsGroup":41812},"affinity":{"podAntiAffinity":{"preferredDuringSchedulingIgnoredDuringExecution":[{"weight":100,"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app.kubernetes.io/instance","operator":"In","values":["splunk-stack1-search-head"]}]},"topologyKey":"kubernetes.io/hostname"}}]}},"schedulerName":"default-scheduler"}},"volumeClaimTemplates":[{"metadata":{"name":"pvc-etc","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-search-head","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"search-head","app.kubernetes.io/part-of":"splunk-stack1-search-head"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"10Gi"}}},"status":{}},{"metadata":{"name":"pvc-var","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-search-head","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"search-head","app.kubernetes.io/part-of":"splunk-stack1-search-head"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"100Gi"}}},"status":{}}],"serviceName":"splunk-stack1-search-head-headless","podManagementPolicy":"Parallel","updateStrategy":{"type":"OnDelete"}},"status":{"replicas":0}}`) + test(`{"kind":"StatefulSet","apiVersion":"apps/v1","metadata":{"name":"splunk-stack1-search-head","namespace":"test","creationTimestamp":null,"ownerReferences":[{"apiVersion":"","kind":"","name":"stack1","uid":"","controller":true}]},"spec":{"replicas":6,"selector":{"matchLabels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-search-head","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"search-head","app.kubernetes.io/part-of":"splunk-stack1-search-head"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-search-head","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"search-head","app.kubernetes.io/part-of":"splunk-stack1-search-head"},"annotations":{"traffic.sidecar.istio.io/excludeOutboundPorts":"8089,8191,9997,7777,9000,17000,17500,19000","traffic.sidecar.istio.io/includeInboundPorts":"8000"}},"spec":{"volumes":[{"name":"mnt-splunk-secrets","secret":{"secretName":"splunk-stack1-search-head-secret-v1","defaultMode":420}},{"name":"mnt-splunk-jdk","emptyDir":{}},{"name":"mnt-splunk-spark","emptyDir":{}}],"initContainers":[{"name":"init","image":"splunk/spark","command":["bash","-c","cp -r /opt/jdk /mnt \u0026\u0026 cp -r /opt/spark /mnt"],"resources":{"limits":{"cpu":"1","memory":"512Mi"},"requests":{"cpu":"250m","memory":"128Mi"}},"volumeMounts":[{"name":"mnt-splunk-jdk","mountPath":"/mnt/jdk"},{"name":"mnt-splunk-spark","mountPath":"/mnt/spark"}],"imagePullPolicy":"IfNotPresent"}],"containers":[{"name":"splunk","image":"splunk/splunk","ports":[{"name":"http-splunkweb","containerPort":8000,"protocol":"TCP"},{"name":"https-splunkd","containerPort":8089,"protocol":"TCP"},{"name":"tcp-dfsmaster","containerPort":9000,"protocol":"TCP"},{"name":"tcp-dfccontrol","containerPort":17000,"protocol":"TCP"},{"name":"tcp-datareceive","containerPort":19000,"protocol":"TCP"},{"name":"user-defined","containerPort":32000,"protocol":"UDP"}],"env":[{"name":"SPLUNK_HOME","value":"/opt/splunk"},{"name":"SPLUNK_START_ARGS","value":"--accept-license"},{"name":"SPLUNK_DEFAULTS_URL","value":"/mnt/splunk-secrets/default.yml"},{"name":"SPLUNK_HOME_OWNERSHIP_ENFORCEMENT","value":"false"},{"name":"SPLUNK_ROLE","value":"splunk_search_head"},{"name":"SPLUNK_DECLARATIVE_ADMIN_PASSWORD","value":"true"},{"name":"SPLUNK_SEARCH_HEAD_URL","value":"splunk-stack1-search-head-0.splunk-stack1-search-head-headless.test.svc.cluster.local,splunk-stack1-search-head-1.splunk-stack1-search-head-headless.test.svc.cluster.local,splunk-stack1-search-head-2.splunk-stack1-search-head-headless.test.svc.cluster.local,splunk-stack1-search-head-3.splunk-stack1-search-head-headless.test.svc.cluster.local,splunk-stack1-search-head-4.splunk-stack1-search-head-headless.test.svc.cluster.local,splunk-stack1-search-head-5.splunk-stack1-search-head-headless.test.svc.cluster.local"},{"name":"SPLUNK_SEARCH_HEAD_CAPTAIN_URL","value":"splunk-stack1-search-head-0.splunk-stack1-search-head-headless.test.svc.cluster.local"},{"name":"SPLUNK_DEPLOYER_URL","value":"splunk-stack1-deployer-service"},{"name":"SPLUNK_CLUSTER_MASTER_URL","value":"splunk-stack1-cluster-master-service.test2.svc.cluster.local"},{"name":"SPLUNK_ENABLE_DFS","value":"true"},{"name":"SPARK_MASTER_HOST","value":"splunk-stack1-spark-master-service"},{"name":"SPARK_MASTER_WEBUI_PORT","value":"8009"},{"name":"SPARK_HOME","value":"/mnt/splunk-spark"},{"name":"JAVA_HOME","value":"/mnt/splunk-jdk"},{"name":"SPLUNK_DFW_NUM_SLOTS_ENABLED","value":"true"}],"resources":{"limits":{"cpu":"4","memory":"8Gi"},"requests":{"cpu":"100m","memory":"512Mi"}},"volumeMounts":[{"name":"pvc-etc","mountPath":"/opt/splunk/etc"},{"name":"pvc-var","mountPath":"/opt/splunk/var"},{"name":"mnt-splunk-secrets","mountPath":"/mnt/splunk-secrets"},{"name":"mnt-splunk-jdk","mountPath":"/mnt/splunk-jdk"},{"name":"mnt-splunk-spark","mountPath":"/mnt/splunk-spark"}],"livenessProbe":{"exec":{"command":["/sbin/checkstate.sh"]},"initialDelaySeconds":300,"timeoutSeconds":30,"periodSeconds":30},"readinessProbe":{"exec":{"command":["/bin/grep","started","/opt/container_artifact/splunk-container.state"]},"initialDelaySeconds":10,"timeoutSeconds":5,"periodSeconds":5},"imagePullPolicy":"IfNotPresent"}],"securityContext":{"runAsUser":41812,"fsGroup":41812},"affinity":{"podAntiAffinity":{"preferredDuringSchedulingIgnoredDuringExecution":[{"weight":100,"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app.kubernetes.io/instance","operator":"In","values":["splunk-stack1-search-head"]}]},"topologyKey":"kubernetes.io/hostname"}}]}},"schedulerName":"default-scheduler"}},"volumeClaimTemplates":[{"metadata":{"name":"pvc-etc","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-search-head","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"search-head","app.kubernetes.io/part-of":"splunk-stack1-search-head"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"10Gi"}}},"status":{}},{"metadata":{"name":"pvc-var","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-search-head","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"search-head","app.kubernetes.io/part-of":"splunk-stack1-search-head"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"100Gi"}}},"status":{}}],"serviceName":"splunk-stack1-search-head-headless","podManagementPolicy":"Parallel","updateStrategy":{"type":"OnDelete"}},"status":{"replicas":0}}`) } func TestGetDeployerStatefulSet(t *testing.T) { @@ -588,5 +588,5 @@ func TestGetDeployerStatefulSet(t *testing.T) { } cr.Spec.Replicas = 3 - test(`{"kind":"StatefulSet","apiVersion":"apps/v1","metadata":{"name":"splunk-stack1-deployer","namespace":"test","creationTimestamp":null,"ownerReferences":[{"apiVersion":"","kind":"","name":"stack1","uid":"","controller":true}]},"spec":{"replicas":1,"selector":{"matchLabels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-deployer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"deployer","app.kubernetes.io/part-of":"splunk-stack1-search-head"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-deployer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"deployer","app.kubernetes.io/part-of":"splunk-stack1-search-head"},"annotations":{"traffic.sidecar.istio.io/excludeOutboundPorts":"8089,8191,9997,7777,9000,17000,17500,19000","traffic.sidecar.istio.io/includeInboundPorts":"8000"}},"spec":{"volumes":[{"name":"mnt-splunk-secrets","secret":{"secretName":"splunk-stack1-deployer-secret-v1","defaultMode":420}}],"containers":[{"name":"splunk","image":"splunk/splunk","ports":[{"name":"splunkweb","containerPort":8000,"protocol":"TCP"},{"name":"splunkd","containerPort":8089,"protocol":"TCP"}],"env":[{"name":"SPLUNK_HOME","value":"/opt/splunk"},{"name":"SPLUNK_START_ARGS","value":"--accept-license"},{"name":"SPLUNK_DEFAULTS_URL","value":"/mnt/splunk-secrets/default.yml"},{"name":"SPLUNK_HOME_OWNERSHIP_ENFORCEMENT","value":"false"},{"name":"SPLUNK_ROLE","value":"splunk_deployer"},{"name":"SPLUNK_DECLARATIVE_ADMIN_PASSWORD","value":"true"},{"name":"SPLUNK_SEARCH_HEAD_URL","value":"splunk-stack1-search-head-0.splunk-stack1-search-head-headless.test.svc.cluster.local,splunk-stack1-search-head-1.splunk-stack1-search-head-headless.test.svc.cluster.local,splunk-stack1-search-head-2.splunk-stack1-search-head-headless.test.svc.cluster.local"},{"name":"SPLUNK_SEARCH_HEAD_CAPTAIN_URL","value":"splunk-stack1-search-head-0.splunk-stack1-search-head-headless.test.svc.cluster.local"}],"resources":{"limits":{"cpu":"4","memory":"8Gi"},"requests":{"cpu":"100m","memory":"512Mi"}},"volumeMounts":[{"name":"pvc-etc","mountPath":"/opt/splunk/etc"},{"name":"pvc-var","mountPath":"/opt/splunk/var"},{"name":"mnt-splunk-secrets","mountPath":"/mnt/splunk-secrets"}],"livenessProbe":{"exec":{"command":["/sbin/checkstate.sh"]},"initialDelaySeconds":300,"timeoutSeconds":30,"periodSeconds":30},"readinessProbe":{"exec":{"command":["/bin/grep","started","/opt/container_artifact/splunk-container.state"]},"initialDelaySeconds":10,"timeoutSeconds":5,"periodSeconds":5},"imagePullPolicy":"IfNotPresent"}],"securityContext":{"runAsUser":41812,"fsGroup":41812},"affinity":{"podAntiAffinity":{"preferredDuringSchedulingIgnoredDuringExecution":[{"weight":100,"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app.kubernetes.io/instance","operator":"In","values":["splunk-stack1-deployer"]}]},"topologyKey":"kubernetes.io/hostname"}}]}},"schedulerName":"default-scheduler"}},"volumeClaimTemplates":[{"metadata":{"name":"pvc-etc","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-deployer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"deployer","app.kubernetes.io/part-of":"splunk-stack1-search-head"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"10Gi"}}},"status":{}},{"metadata":{"name":"pvc-var","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-deployer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"deployer","app.kubernetes.io/part-of":"splunk-stack1-search-head"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"100Gi"}}},"status":{}}],"serviceName":"splunk-stack1-deployer-headless","podManagementPolicy":"Parallel","updateStrategy":{"type":"OnDelete"}},"status":{"replicas":0}}`) + test(`{"kind":"StatefulSet","apiVersion":"apps/v1","metadata":{"name":"splunk-stack1-deployer","namespace":"test","creationTimestamp":null,"ownerReferences":[{"apiVersion":"","kind":"","name":"stack1","uid":"","controller":true}]},"spec":{"replicas":1,"selector":{"matchLabels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-deployer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"deployer","app.kubernetes.io/part-of":"splunk-stack1-search-head"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-deployer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"deployer","app.kubernetes.io/part-of":"splunk-stack1-search-head"},"annotations":{"traffic.sidecar.istio.io/excludeOutboundPorts":"8089,8191,9997,7777,9000,17000,17500,19000","traffic.sidecar.istio.io/includeInboundPorts":"8000"}},"spec":{"volumes":[{"name":"mnt-splunk-secrets","secret":{"secretName":"splunk-stack1-deployer-secret-v1","defaultMode":420}}],"containers":[{"name":"splunk","image":"splunk/splunk","ports":[{"name":"http-splunkweb","containerPort":8000,"protocol":"TCP"},{"name":"https-splunkd","containerPort":8089,"protocol":"TCP"}],"env":[{"name":"SPLUNK_HOME","value":"/opt/splunk"},{"name":"SPLUNK_START_ARGS","value":"--accept-license"},{"name":"SPLUNK_DEFAULTS_URL","value":"/mnt/splunk-secrets/default.yml"},{"name":"SPLUNK_HOME_OWNERSHIP_ENFORCEMENT","value":"false"},{"name":"SPLUNK_ROLE","value":"splunk_deployer"},{"name":"SPLUNK_DECLARATIVE_ADMIN_PASSWORD","value":"true"},{"name":"SPLUNK_SEARCH_HEAD_URL","value":"splunk-stack1-search-head-0.splunk-stack1-search-head-headless.test.svc.cluster.local,splunk-stack1-search-head-1.splunk-stack1-search-head-headless.test.svc.cluster.local,splunk-stack1-search-head-2.splunk-stack1-search-head-headless.test.svc.cluster.local"},{"name":"SPLUNK_SEARCH_HEAD_CAPTAIN_URL","value":"splunk-stack1-search-head-0.splunk-stack1-search-head-headless.test.svc.cluster.local"}],"resources":{"limits":{"cpu":"4","memory":"8Gi"},"requests":{"cpu":"100m","memory":"512Mi"}},"volumeMounts":[{"name":"pvc-etc","mountPath":"/opt/splunk/etc"},{"name":"pvc-var","mountPath":"/opt/splunk/var"},{"name":"mnt-splunk-secrets","mountPath":"/mnt/splunk-secrets"}],"livenessProbe":{"exec":{"command":["/sbin/checkstate.sh"]},"initialDelaySeconds":300,"timeoutSeconds":30,"periodSeconds":30},"readinessProbe":{"exec":{"command":["/bin/grep","started","/opt/container_artifact/splunk-container.state"]},"initialDelaySeconds":10,"timeoutSeconds":5,"periodSeconds":5},"imagePullPolicy":"IfNotPresent"}],"securityContext":{"runAsUser":41812,"fsGroup":41812},"affinity":{"podAntiAffinity":{"preferredDuringSchedulingIgnoredDuringExecution":[{"weight":100,"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app.kubernetes.io/instance","operator":"In","values":["splunk-stack1-deployer"]}]},"topologyKey":"kubernetes.io/hostname"}}]}},"schedulerName":"default-scheduler"}},"volumeClaimTemplates":[{"metadata":{"name":"pvc-etc","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-deployer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"deployer","app.kubernetes.io/part-of":"splunk-stack1-search-head"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"10Gi"}}},"status":{}},{"metadata":{"name":"pvc-var","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-deployer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"deployer","app.kubernetes.io/part-of":"splunk-stack1-search-head"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"100Gi"}}},"status":{}}],"serviceName":"splunk-stack1-deployer-headless","podManagementPolicy":"Parallel","updateStrategy":{"type":"OnDelete"}},"status":{"replicas":0}}`) } diff --git a/pkg/splunk/enterprise/standalone_test.go b/pkg/splunk/enterprise/standalone_test.go index 717af19b0..9187d45c9 100644 --- a/pkg/splunk/enterprise/standalone_test.go +++ b/pkg/splunk/enterprise/standalone_test.go @@ -198,14 +198,14 @@ func TestGetStandaloneStatefulSet(t *testing.T) { configTester(t, "getStandaloneStatefulSet()", f, want) } - test(`{"kind":"StatefulSet","apiVersion":"apps/v1","metadata":{"name":"splunk-stack1-standalone","namespace":"test","creationTimestamp":null,"ownerReferences":[{"apiVersion":"","kind":"","name":"stack1","uid":"","controller":true}]},"spec":{"replicas":1,"selector":{"matchLabels":{"app.kubernetes.io/component":"standalone","app.kubernetes.io/instance":"splunk-stack1-standalone","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"standalone","app.kubernetes.io/part-of":"splunk-stack1-standalone"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app.kubernetes.io/component":"standalone","app.kubernetes.io/instance":"splunk-stack1-standalone","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"standalone","app.kubernetes.io/part-of":"splunk-stack1-standalone"},"annotations":{"traffic.sidecar.istio.io/excludeOutboundPorts":"8089,8191,9997,7777,9000,17000,17500,19000","traffic.sidecar.istio.io/includeInboundPorts":"8000,8088"}},"spec":{"volumes":[{"name":"mnt-splunk-secrets","secret":{"secretName":"splunk-stack1-standalone-secret-v1","defaultMode":420}}],"containers":[{"name":"splunk","image":"splunk/splunk","ports":[{"name":"splunkweb","containerPort":8000,"protocol":"TCP"},{"name":"hec","containerPort":8088,"protocol":"TCP"},{"name":"splunkd","containerPort":8089,"protocol":"TCP"},{"name":"dfsmaster","containerPort":9000,"protocol":"TCP"},{"name":"s2s","containerPort":9997,"protocol":"TCP"},{"name":"dfccontrol","containerPort":17000,"protocol":"TCP"},{"name":"datareceive","containerPort":19000,"protocol":"TCP"}],"env":[{"name":"SPLUNK_HOME","value":"/opt/splunk"},{"name":"SPLUNK_START_ARGS","value":"--accept-license"},{"name":"SPLUNK_DEFAULTS_URL","value":"/mnt/splunk-secrets/default.yml"},{"name":"SPLUNK_HOME_OWNERSHIP_ENFORCEMENT","value":"false"},{"name":"SPLUNK_ROLE","value":"splunk_standalone"},{"name":"SPLUNK_DECLARATIVE_ADMIN_PASSWORD","value":"true"}],"resources":{"limits":{"cpu":"4","memory":"8Gi"},"requests":{"cpu":"100m","memory":"512Mi"}},"volumeMounts":[{"name":"pvc-etc","mountPath":"/opt/splunk/etc"},{"name":"pvc-var","mountPath":"/opt/splunk/var"},{"name":"mnt-splunk-secrets","mountPath":"/mnt/splunk-secrets"}],"livenessProbe":{"exec":{"command":["/sbin/checkstate.sh"]},"initialDelaySeconds":300,"timeoutSeconds":30,"periodSeconds":30},"readinessProbe":{"exec":{"command":["/bin/grep","started","/opt/container_artifact/splunk-container.state"]},"initialDelaySeconds":10,"timeoutSeconds":5,"periodSeconds":5},"imagePullPolicy":"IfNotPresent"}],"securityContext":{"runAsUser":41812,"fsGroup":41812},"affinity":{"podAntiAffinity":{"preferredDuringSchedulingIgnoredDuringExecution":[{"weight":100,"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app.kubernetes.io/instance","operator":"In","values":["splunk-stack1-standalone"]}]},"topologyKey":"kubernetes.io/hostname"}}]}},"schedulerName":"default-scheduler"}},"volumeClaimTemplates":[{"metadata":{"name":"pvc-etc","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"standalone","app.kubernetes.io/instance":"splunk-stack1-standalone","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"standalone","app.kubernetes.io/part-of":"splunk-stack1-standalone"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"10Gi"}}},"status":{}},{"metadata":{"name":"pvc-var","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"standalone","app.kubernetes.io/instance":"splunk-stack1-standalone","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"standalone","app.kubernetes.io/part-of":"splunk-stack1-standalone"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"100Gi"}}},"status":{}}],"serviceName":"splunk-stack1-standalone-headless","podManagementPolicy":"Parallel","updateStrategy":{"type":"OnDelete"}},"status":{"replicas":0}}`) + test(`{"kind":"StatefulSet","apiVersion":"apps/v1","metadata":{"name":"splunk-stack1-standalone","namespace":"test","creationTimestamp":null,"ownerReferences":[{"apiVersion":"","kind":"","name":"stack1","uid":"","controller":true}]},"spec":{"replicas":1,"selector":{"matchLabels":{"app.kubernetes.io/component":"standalone","app.kubernetes.io/instance":"splunk-stack1-standalone","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"standalone","app.kubernetes.io/part-of":"splunk-stack1-standalone"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app.kubernetes.io/component":"standalone","app.kubernetes.io/instance":"splunk-stack1-standalone","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"standalone","app.kubernetes.io/part-of":"splunk-stack1-standalone"},"annotations":{"traffic.sidecar.istio.io/excludeOutboundPorts":"8089,8191,9997,7777,9000,17000,17500,19000","traffic.sidecar.istio.io/includeInboundPorts":"8000,8088"}},"spec":{"volumes":[{"name":"mnt-splunk-secrets","secret":{"secretName":"splunk-stack1-standalone-secret-v1","defaultMode":420}}],"containers":[{"name":"splunk","image":"splunk/splunk","ports":[{"name":"http-splunkweb","containerPort":8000,"protocol":"TCP"},{"name":"http-hec","containerPort":8088,"protocol":"TCP"},{"name":"https-splunkd","containerPort":8089,"protocol":"TCP"},{"name":"tcp-dfsmaster","containerPort":9000,"protocol":"TCP"},{"name":"tcp-s2s","containerPort":9997,"protocol":"TCP"},{"name":"tcp-dfccontrol","containerPort":17000,"protocol":"TCP"},{"name":"tcp-datareceive","containerPort":19000,"protocol":"TCP"}],"env":[{"name":"SPLUNK_HOME","value":"/opt/splunk"},{"name":"SPLUNK_START_ARGS","value":"--accept-license"},{"name":"SPLUNK_DEFAULTS_URL","value":"/mnt/splunk-secrets/default.yml"},{"name":"SPLUNK_HOME_OWNERSHIP_ENFORCEMENT","value":"false"},{"name":"SPLUNK_ROLE","value":"splunk_standalone"},{"name":"SPLUNK_DECLARATIVE_ADMIN_PASSWORD","value":"true"}],"resources":{"limits":{"cpu":"4","memory":"8Gi"},"requests":{"cpu":"100m","memory":"512Mi"}},"volumeMounts":[{"name":"pvc-etc","mountPath":"/opt/splunk/etc"},{"name":"pvc-var","mountPath":"/opt/splunk/var"},{"name":"mnt-splunk-secrets","mountPath":"/mnt/splunk-secrets"}],"livenessProbe":{"exec":{"command":["/sbin/checkstate.sh"]},"initialDelaySeconds":300,"timeoutSeconds":30,"periodSeconds":30},"readinessProbe":{"exec":{"command":["/bin/grep","started","/opt/container_artifact/splunk-container.state"]},"initialDelaySeconds":10,"timeoutSeconds":5,"periodSeconds":5},"imagePullPolicy":"IfNotPresent"}],"securityContext":{"runAsUser":41812,"fsGroup":41812},"affinity":{"podAntiAffinity":{"preferredDuringSchedulingIgnoredDuringExecution":[{"weight":100,"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app.kubernetes.io/instance","operator":"In","values":["splunk-stack1-standalone"]}]},"topologyKey":"kubernetes.io/hostname"}}]}},"schedulerName":"default-scheduler"}},"volumeClaimTemplates":[{"metadata":{"name":"pvc-etc","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"standalone","app.kubernetes.io/instance":"splunk-stack1-standalone","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"standalone","app.kubernetes.io/part-of":"splunk-stack1-standalone"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"10Gi"}}},"status":{}},{"metadata":{"name":"pvc-var","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"standalone","app.kubernetes.io/instance":"splunk-stack1-standalone","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"standalone","app.kubernetes.io/part-of":"splunk-stack1-standalone"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"100Gi"}}},"status":{}}],"serviceName":"splunk-stack1-standalone-headless","podManagementPolicy":"Parallel","updateStrategy":{"type":"OnDelete"}},"status":{"replicas":0}}`) cr.Spec.EphemeralStorage = true - test(`{"kind":"StatefulSet","apiVersion":"apps/v1","metadata":{"name":"splunk-stack1-standalone","namespace":"test","creationTimestamp":null,"ownerReferences":[{"apiVersion":"","kind":"","name":"stack1","uid":"","controller":true}]},"spec":{"replicas":1,"selector":{"matchLabels":{"app.kubernetes.io/component":"standalone","app.kubernetes.io/instance":"splunk-stack1-standalone","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"standalone","app.kubernetes.io/part-of":"splunk-stack1-standalone"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app.kubernetes.io/component":"standalone","app.kubernetes.io/instance":"splunk-stack1-standalone","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"standalone","app.kubernetes.io/part-of":"splunk-stack1-standalone"},"annotations":{"traffic.sidecar.istio.io/excludeOutboundPorts":"8089,8191,9997,7777,9000,17000,17500,19000","traffic.sidecar.istio.io/includeInboundPorts":"8000,8088"}},"spec":{"volumes":[{"name":"mnt-splunk-etc","emptyDir":{}},{"name":"mnt-splunk-var","emptyDir":{}},{"name":"mnt-splunk-secrets","secret":{"secretName":"splunk-stack1-standalone-secret-v1","defaultMode":420}}],"containers":[{"name":"splunk","image":"splunk/splunk","ports":[{"name":"splunkweb","containerPort":8000,"protocol":"TCP"},{"name":"hec","containerPort":8088,"protocol":"TCP"},{"name":"splunkd","containerPort":8089,"protocol":"TCP"},{"name":"dfsmaster","containerPort":9000,"protocol":"TCP"},{"name":"s2s","containerPort":9997,"protocol":"TCP"},{"name":"dfccontrol","containerPort":17000,"protocol":"TCP"},{"name":"datareceive","containerPort":19000,"protocol":"TCP"}],"env":[{"name":"SPLUNK_HOME","value":"/opt/splunk"},{"name":"SPLUNK_START_ARGS","value":"--accept-license"},{"name":"SPLUNK_DEFAULTS_URL","value":"/mnt/splunk-secrets/default.yml"},{"name":"SPLUNK_HOME_OWNERSHIP_ENFORCEMENT","value":"false"},{"name":"SPLUNK_ROLE","value":"splunk_standalone"},{"name":"SPLUNK_DECLARATIVE_ADMIN_PASSWORD","value":"true"}],"resources":{"limits":{"cpu":"4","memory":"8Gi"},"requests":{"cpu":"100m","memory":"512Mi"}},"volumeMounts":[{"name":"mnt-splunk-etc","mountPath":"/opt/splunk/etc"},{"name":"mnt-splunk-var","mountPath":"/opt/splunk/var"},{"name":"mnt-splunk-secrets","mountPath":"/mnt/splunk-secrets"}],"livenessProbe":{"exec":{"command":["/sbin/checkstate.sh"]},"initialDelaySeconds":300,"timeoutSeconds":30,"periodSeconds":30},"readinessProbe":{"exec":{"command":["/bin/grep","started","/opt/container_artifact/splunk-container.state"]},"initialDelaySeconds":10,"timeoutSeconds":5,"periodSeconds":5},"imagePullPolicy":"IfNotPresent"}],"securityContext":{"runAsUser":41812,"fsGroup":41812},"affinity":{"podAntiAffinity":{"preferredDuringSchedulingIgnoredDuringExecution":[{"weight":100,"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app.kubernetes.io/instance","operator":"In","values":["splunk-stack1-standalone"]}]},"topologyKey":"kubernetes.io/hostname"}}]}},"schedulerName":"default-scheduler"}},"serviceName":"splunk-stack1-standalone-headless","podManagementPolicy":"Parallel","updateStrategy":{"type":"OnDelete"}},"status":{"replicas":0}}`) + test(`{"kind":"StatefulSet","apiVersion":"apps/v1","metadata":{"name":"splunk-stack1-standalone","namespace":"test","creationTimestamp":null,"ownerReferences":[{"apiVersion":"","kind":"","name":"stack1","uid":"","controller":true}]},"spec":{"replicas":1,"selector":{"matchLabels":{"app.kubernetes.io/component":"standalone","app.kubernetes.io/instance":"splunk-stack1-standalone","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"standalone","app.kubernetes.io/part-of":"splunk-stack1-standalone"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app.kubernetes.io/component":"standalone","app.kubernetes.io/instance":"splunk-stack1-standalone","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"standalone","app.kubernetes.io/part-of":"splunk-stack1-standalone"},"annotations":{"traffic.sidecar.istio.io/excludeOutboundPorts":"8089,8191,9997,7777,9000,17000,17500,19000","traffic.sidecar.istio.io/includeInboundPorts":"8000,8088"}},"spec":{"volumes":[{"name":"mnt-splunk-etc","emptyDir":{}},{"name":"mnt-splunk-var","emptyDir":{}},{"name":"mnt-splunk-secrets","secret":{"secretName":"splunk-stack1-standalone-secret-v1","defaultMode":420}}],"containers":[{"name":"splunk","image":"splunk/splunk","ports":[{"name":"http-splunkweb","containerPort":8000,"protocol":"TCP"},{"name":"http-hec","containerPort":8088,"protocol":"TCP"},{"name":"https-splunkd","containerPort":8089,"protocol":"TCP"},{"name":"tcp-dfsmaster","containerPort":9000,"protocol":"TCP"},{"name":"tcp-s2s","containerPort":9997,"protocol":"TCP"},{"name":"tcp-dfccontrol","containerPort":17000,"protocol":"TCP"},{"name":"tcp-datareceive","containerPort":19000,"protocol":"TCP"}],"env":[{"name":"SPLUNK_HOME","value":"/opt/splunk"},{"name":"SPLUNK_START_ARGS","value":"--accept-license"},{"name":"SPLUNK_DEFAULTS_URL","value":"/mnt/splunk-secrets/default.yml"},{"name":"SPLUNK_HOME_OWNERSHIP_ENFORCEMENT","value":"false"},{"name":"SPLUNK_ROLE","value":"splunk_standalone"},{"name":"SPLUNK_DECLARATIVE_ADMIN_PASSWORD","value":"true"}],"resources":{"limits":{"cpu":"4","memory":"8Gi"},"requests":{"cpu":"100m","memory":"512Mi"}},"volumeMounts":[{"name":"mnt-splunk-etc","mountPath":"/opt/splunk/etc"},{"name":"mnt-splunk-var","mountPath":"/opt/splunk/var"},{"name":"mnt-splunk-secrets","mountPath":"/mnt/splunk-secrets"}],"livenessProbe":{"exec":{"command":["/sbin/checkstate.sh"]},"initialDelaySeconds":300,"timeoutSeconds":30,"periodSeconds":30},"readinessProbe":{"exec":{"command":["/bin/grep","started","/opt/container_artifact/splunk-container.state"]},"initialDelaySeconds":10,"timeoutSeconds":5,"periodSeconds":5},"imagePullPolicy":"IfNotPresent"}],"securityContext":{"runAsUser":41812,"fsGroup":41812},"affinity":{"podAntiAffinity":{"preferredDuringSchedulingIgnoredDuringExecution":[{"weight":100,"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app.kubernetes.io/instance","operator":"In","values":["splunk-stack1-standalone"]}]},"topologyKey":"kubernetes.io/hostname"}}]}},"schedulerName":"default-scheduler"}},"serviceName":"splunk-stack1-standalone-headless","podManagementPolicy":"Parallel","updateStrategy":{"type":"OnDelete"}},"status":{"replicas":0}}`) cr.Spec.EphemeralStorage = false cr.Spec.SparkRef.Name = cr.GetName() - test(`{"kind":"StatefulSet","apiVersion":"apps/v1","metadata":{"name":"splunk-stack1-standalone","namespace":"test","creationTimestamp":null,"ownerReferences":[{"apiVersion":"","kind":"","name":"stack1","uid":"","controller":true}]},"spec":{"replicas":1,"selector":{"matchLabels":{"app.kubernetes.io/component":"standalone","app.kubernetes.io/instance":"splunk-stack1-standalone","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"standalone","app.kubernetes.io/part-of":"splunk-stack1-standalone"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app.kubernetes.io/component":"standalone","app.kubernetes.io/instance":"splunk-stack1-standalone","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"standalone","app.kubernetes.io/part-of":"splunk-stack1-standalone"},"annotations":{"traffic.sidecar.istio.io/excludeOutboundPorts":"8089,8191,9997,7777,9000,17000,17500,19000","traffic.sidecar.istio.io/includeInboundPorts":"8000,8088"}},"spec":{"volumes":[{"name":"mnt-splunk-secrets","secret":{"secretName":"splunk-stack1-standalone-secret-v1","defaultMode":420}},{"name":"mnt-splunk-jdk","emptyDir":{}},{"name":"mnt-splunk-spark","emptyDir":{}}],"initContainers":[{"name":"init","image":"splunk/spark","command":["bash","-c","cp -r /opt/jdk /mnt \u0026\u0026 cp -r /opt/spark /mnt"],"resources":{"limits":{"cpu":"1","memory":"512Mi"},"requests":{"cpu":"250m","memory":"128Mi"}},"volumeMounts":[{"name":"mnt-splunk-jdk","mountPath":"/mnt/jdk"},{"name":"mnt-splunk-spark","mountPath":"/mnt/spark"}],"imagePullPolicy":"IfNotPresent"}],"containers":[{"name":"splunk","image":"splunk/splunk","ports":[{"name":"splunkweb","containerPort":8000,"protocol":"TCP"},{"name":"hec","containerPort":8088,"protocol":"TCP"},{"name":"splunkd","containerPort":8089,"protocol":"TCP"},{"name":"dfsmaster","containerPort":9000,"protocol":"TCP"},{"name":"s2s","containerPort":9997,"protocol":"TCP"},{"name":"dfccontrol","containerPort":17000,"protocol":"TCP"},{"name":"datareceive","containerPort":19000,"protocol":"TCP"}],"env":[{"name":"SPLUNK_HOME","value":"/opt/splunk"},{"name":"SPLUNK_START_ARGS","value":"--accept-license"},{"name":"SPLUNK_DEFAULTS_URL","value":"/mnt/splunk-secrets/default.yml"},{"name":"SPLUNK_HOME_OWNERSHIP_ENFORCEMENT","value":"false"},{"name":"SPLUNK_ROLE","value":"splunk_standalone"},{"name":"SPLUNK_DECLARATIVE_ADMIN_PASSWORD","value":"true"},{"name":"SPLUNK_ENABLE_DFS","value":"true"},{"name":"SPARK_MASTER_HOST","value":"splunk-stack1-spark-master-service"},{"name":"SPARK_MASTER_WEBUI_PORT","value":"8009"},{"name":"SPARK_HOME","value":"/mnt/splunk-spark"},{"name":"JAVA_HOME","value":"/mnt/splunk-jdk"},{"name":"SPLUNK_DFW_NUM_SLOTS_ENABLED","value":"false"}],"resources":{"limits":{"cpu":"4","memory":"8Gi"},"requests":{"cpu":"100m","memory":"512Mi"}},"volumeMounts":[{"name":"pvc-etc","mountPath":"/opt/splunk/etc"},{"name":"pvc-var","mountPath":"/opt/splunk/var"},{"name":"mnt-splunk-secrets","mountPath":"/mnt/splunk-secrets"},{"name":"mnt-splunk-jdk","mountPath":"/mnt/splunk-jdk"},{"name":"mnt-splunk-spark","mountPath":"/mnt/splunk-spark"}],"livenessProbe":{"exec":{"command":["/sbin/checkstate.sh"]},"initialDelaySeconds":300,"timeoutSeconds":30,"periodSeconds":30},"readinessProbe":{"exec":{"command":["/bin/grep","started","/opt/container_artifact/splunk-container.state"]},"initialDelaySeconds":10,"timeoutSeconds":5,"periodSeconds":5},"imagePullPolicy":"IfNotPresent"}],"securityContext":{"runAsUser":41812,"fsGroup":41812},"affinity":{"podAntiAffinity":{"preferredDuringSchedulingIgnoredDuringExecution":[{"weight":100,"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app.kubernetes.io/instance","operator":"In","values":["splunk-stack1-standalone"]}]},"topologyKey":"kubernetes.io/hostname"}}]}},"schedulerName":"default-scheduler"}},"volumeClaimTemplates":[{"metadata":{"name":"pvc-etc","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"standalone","app.kubernetes.io/instance":"splunk-stack1-standalone","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"standalone","app.kubernetes.io/part-of":"splunk-stack1-standalone"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"10Gi"}}},"status":{}},{"metadata":{"name":"pvc-var","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"standalone","app.kubernetes.io/instance":"splunk-stack1-standalone","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"standalone","app.kubernetes.io/part-of":"splunk-stack1-standalone"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"100Gi"}}},"status":{}}],"serviceName":"splunk-stack1-standalone-headless","podManagementPolicy":"Parallel","updateStrategy":{"type":"OnDelete"}},"status":{"replicas":0}}`) + test(`{"kind":"StatefulSet","apiVersion":"apps/v1","metadata":{"name":"splunk-stack1-standalone","namespace":"test","creationTimestamp":null,"ownerReferences":[{"apiVersion":"","kind":"","name":"stack1","uid":"","controller":true}]},"spec":{"replicas":1,"selector":{"matchLabels":{"app.kubernetes.io/component":"standalone","app.kubernetes.io/instance":"splunk-stack1-standalone","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"standalone","app.kubernetes.io/part-of":"splunk-stack1-standalone"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app.kubernetes.io/component":"standalone","app.kubernetes.io/instance":"splunk-stack1-standalone","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"standalone","app.kubernetes.io/part-of":"splunk-stack1-standalone"},"annotations":{"traffic.sidecar.istio.io/excludeOutboundPorts":"8089,8191,9997,7777,9000,17000,17500,19000","traffic.sidecar.istio.io/includeInboundPorts":"8000,8088"}},"spec":{"volumes":[{"name":"mnt-splunk-secrets","secret":{"secretName":"splunk-stack1-standalone-secret-v1","defaultMode":420}},{"name":"mnt-splunk-jdk","emptyDir":{}},{"name":"mnt-splunk-spark","emptyDir":{}}],"initContainers":[{"name":"init","image":"splunk/spark","command":["bash","-c","cp -r /opt/jdk /mnt \u0026\u0026 cp -r /opt/spark /mnt"],"resources":{"limits":{"cpu":"1","memory":"512Mi"},"requests":{"cpu":"250m","memory":"128Mi"}},"volumeMounts":[{"name":"mnt-splunk-jdk","mountPath":"/mnt/jdk"},{"name":"mnt-splunk-spark","mountPath":"/mnt/spark"}],"imagePullPolicy":"IfNotPresent"}],"containers":[{"name":"splunk","image":"splunk/splunk","ports":[{"name":"http-splunkweb","containerPort":8000,"protocol":"TCP"},{"name":"http-hec","containerPort":8088,"protocol":"TCP"},{"name":"https-splunkd","containerPort":8089,"protocol":"TCP"},{"name":"tcp-dfsmaster","containerPort":9000,"protocol":"TCP"},{"name":"tcp-s2s","containerPort":9997,"protocol":"TCP"},{"name":"tcp-dfccontrol","containerPort":17000,"protocol":"TCP"},{"name":"tcp-datareceive","containerPort":19000,"protocol":"TCP"}],"env":[{"name":"SPLUNK_HOME","value":"/opt/splunk"},{"name":"SPLUNK_START_ARGS","value":"--accept-license"},{"name":"SPLUNK_DEFAULTS_URL","value":"/mnt/splunk-secrets/default.yml"},{"name":"SPLUNK_HOME_OWNERSHIP_ENFORCEMENT","value":"false"},{"name":"SPLUNK_ROLE","value":"splunk_standalone"},{"name":"SPLUNK_DECLARATIVE_ADMIN_PASSWORD","value":"true"},{"name":"SPLUNK_ENABLE_DFS","value":"true"},{"name":"SPARK_MASTER_HOST","value":"splunk-stack1-spark-master-service"},{"name":"SPARK_MASTER_WEBUI_PORT","value":"8009"},{"name":"SPARK_HOME","value":"/mnt/splunk-spark"},{"name":"JAVA_HOME","value":"/mnt/splunk-jdk"},{"name":"SPLUNK_DFW_NUM_SLOTS_ENABLED","value":"false"}],"resources":{"limits":{"cpu":"4","memory":"8Gi"},"requests":{"cpu":"100m","memory":"512Mi"}},"volumeMounts":[{"name":"pvc-etc","mountPath":"/opt/splunk/etc"},{"name":"pvc-var","mountPath":"/opt/splunk/var"},{"name":"mnt-splunk-secrets","mountPath":"/mnt/splunk-secrets"},{"name":"mnt-splunk-jdk","mountPath":"/mnt/splunk-jdk"},{"name":"mnt-splunk-spark","mountPath":"/mnt/splunk-spark"}],"livenessProbe":{"exec":{"command":["/sbin/checkstate.sh"]},"initialDelaySeconds":300,"timeoutSeconds":30,"periodSeconds":30},"readinessProbe":{"exec":{"command":["/bin/grep","started","/opt/container_artifact/splunk-container.state"]},"initialDelaySeconds":10,"timeoutSeconds":5,"periodSeconds":5},"imagePullPolicy":"IfNotPresent"}],"securityContext":{"runAsUser":41812,"fsGroup":41812},"affinity":{"podAntiAffinity":{"preferredDuringSchedulingIgnoredDuringExecution":[{"weight":100,"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app.kubernetes.io/instance","operator":"In","values":["splunk-stack1-standalone"]}]},"topologyKey":"kubernetes.io/hostname"}}]}},"schedulerName":"default-scheduler"}},"volumeClaimTemplates":[{"metadata":{"name":"pvc-etc","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"standalone","app.kubernetes.io/instance":"splunk-stack1-standalone","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"standalone","app.kubernetes.io/part-of":"splunk-stack1-standalone"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"10Gi"}}},"status":{}},{"metadata":{"name":"pvc-var","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"standalone","app.kubernetes.io/instance":"splunk-stack1-standalone","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"standalone","app.kubernetes.io/part-of":"splunk-stack1-standalone"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"100Gi"}}},"status":{}}],"serviceName":"splunk-stack1-standalone-headless","podManagementPolicy":"Parallel","updateStrategy":{"type":"OnDelete"}},"status":{"replicas":0}}`) cr.Spec.ClusterMasterRef.Name = "stack2" cr.Spec.StorageClassName = "gp2" @@ -215,7 +215,7 @@ func TestGetStandaloneStatefulSet(t *testing.T) { cr.Spec.Volumes = []corev1.Volume{ {Name: "defaults"}, } - test(`{"kind":"StatefulSet","apiVersion":"apps/v1","metadata":{"name":"splunk-stack1-standalone","namespace":"test","creationTimestamp":null,"ownerReferences":[{"apiVersion":"","kind":"","name":"stack1","uid":"","controller":true}]},"spec":{"replicas":1,"selector":{"matchLabels":{"app.kubernetes.io/component":"standalone","app.kubernetes.io/instance":"splunk-stack1-standalone","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"standalone","app.kubernetes.io/part-of":"splunk-stack1-standalone"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app.kubernetes.io/component":"standalone","app.kubernetes.io/instance":"splunk-stack1-standalone","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"standalone","app.kubernetes.io/part-of":"splunk-stack1-standalone"},"annotations":{"traffic.sidecar.istio.io/excludeOutboundPorts":"8089,8191,9997,7777,9000,17000,17500,19000","traffic.sidecar.istio.io/includeInboundPorts":"8000,8088"}},"spec":{"volumes":[{"name":"defaults"},{"name":"mnt-splunk-secrets","secret":{"secretName":"splunk-stack1-standalone-secret-v1","defaultMode":420}},{"name":"mnt-splunk-defaults","configMap":{"name":"splunk-stack1-standalone-defaults","defaultMode":420}},{"name":"mnt-splunk-jdk","emptyDir":{}},{"name":"mnt-splunk-spark","emptyDir":{}}],"initContainers":[{"name":"init","image":"splunk/spark","command":["bash","-c","cp -r /opt/jdk /mnt \u0026\u0026 cp -r /opt/spark /mnt"],"resources":{"limits":{"cpu":"1","memory":"512Mi"},"requests":{"cpu":"250m","memory":"128Mi"}},"volumeMounts":[{"name":"mnt-splunk-jdk","mountPath":"/mnt/jdk"},{"name":"mnt-splunk-spark","mountPath":"/mnt/spark"}],"imagePullPolicy":"IfNotPresent"}],"containers":[{"name":"splunk","image":"splunk/splunk","ports":[{"name":"splunkweb","containerPort":8000,"protocol":"TCP"},{"name":"hec","containerPort":8088,"protocol":"TCP"},{"name":"splunkd","containerPort":8089,"protocol":"TCP"},{"name":"dfsmaster","containerPort":9000,"protocol":"TCP"},{"name":"s2s","containerPort":9997,"protocol":"TCP"},{"name":"dfccontrol","containerPort":17000,"protocol":"TCP"},{"name":"datareceive","containerPort":19000,"protocol":"TCP"}],"env":[{"name":"SPLUNK_HOME","value":"/opt/splunk"},{"name":"SPLUNK_START_ARGS","value":"--accept-license"},{"name":"SPLUNK_DEFAULTS_URL","value":"/mnt/splunk-defaults/default.yml,/mnt/defaults/defaults.yml,/mnt/splunk-secrets/default.yml"},{"name":"SPLUNK_HOME_OWNERSHIP_ENFORCEMENT","value":"false"},{"name":"SPLUNK_ROLE","value":"splunk_search_head"},{"name":"SPLUNK_DECLARATIVE_ADMIN_PASSWORD","value":"true"},{"name":"SPLUNK_CLUSTER_MASTER_URL","value":"splunk-stack2-cluster-master-service"},{"name":"SPLUNK_ENABLE_DFS","value":"true"},{"name":"SPARK_MASTER_HOST","value":"splunk-stack1-spark-master-service"},{"name":"SPARK_MASTER_WEBUI_PORT","value":"8009"},{"name":"SPARK_HOME","value":"/mnt/splunk-spark"},{"name":"JAVA_HOME","value":"/mnt/splunk-jdk"},{"name":"SPLUNK_DFW_NUM_SLOTS_ENABLED","value":"false"}],"resources":{"limits":{"cpu":"4","memory":"8Gi"},"requests":{"cpu":"100m","memory":"512Mi"}},"volumeMounts":[{"name":"pvc-etc","mountPath":"/opt/splunk/etc"},{"name":"pvc-var","mountPath":"/opt/splunk/var"},{"name":"defaults","mountPath":"/mnt/defaults"},{"name":"mnt-splunk-secrets","mountPath":"/mnt/splunk-secrets"},{"name":"mnt-splunk-defaults","mountPath":"/mnt/splunk-defaults"},{"name":"mnt-splunk-jdk","mountPath":"/mnt/splunk-jdk"},{"name":"mnt-splunk-spark","mountPath":"/mnt/splunk-spark"}],"livenessProbe":{"exec":{"command":["/sbin/checkstate.sh"]},"initialDelaySeconds":300,"timeoutSeconds":30,"periodSeconds":30},"readinessProbe":{"exec":{"command":["/bin/grep","started","/opt/container_artifact/splunk-container.state"]},"initialDelaySeconds":10,"timeoutSeconds":5,"periodSeconds":5},"imagePullPolicy":"IfNotPresent"}],"securityContext":{"runAsUser":41812,"fsGroup":41812},"affinity":{"podAntiAffinity":{"preferredDuringSchedulingIgnoredDuringExecution":[{"weight":100,"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app.kubernetes.io/instance","operator":"In","values":["splunk-stack1-standalone"]}]},"topologyKey":"kubernetes.io/hostname"}}]}},"schedulerName":"custom-scheduler"}},"volumeClaimTemplates":[{"metadata":{"name":"pvc-etc","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"standalone","app.kubernetes.io/instance":"splunk-stack1-standalone","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"standalone","app.kubernetes.io/part-of":"splunk-stack1-standalone"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"10Gi"}},"storageClassName":"gp2"},"status":{}},{"metadata":{"name":"pvc-var","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"standalone","app.kubernetes.io/instance":"splunk-stack1-standalone","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"standalone","app.kubernetes.io/part-of":"splunk-stack1-standalone"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"100Gi"}},"storageClassName":"gp2"},"status":{}}],"serviceName":"splunk-stack1-standalone-headless","podManagementPolicy":"Parallel","updateStrategy":{"type":"OnDelete"}},"status":{"replicas":0}}`) + test(`{"kind":"StatefulSet","apiVersion":"apps/v1","metadata":{"name":"splunk-stack1-standalone","namespace":"test","creationTimestamp":null,"ownerReferences":[{"apiVersion":"","kind":"","name":"stack1","uid":"","controller":true}]},"spec":{"replicas":1,"selector":{"matchLabels":{"app.kubernetes.io/component":"standalone","app.kubernetes.io/instance":"splunk-stack1-standalone","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"standalone","app.kubernetes.io/part-of":"splunk-stack1-standalone"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app.kubernetes.io/component":"standalone","app.kubernetes.io/instance":"splunk-stack1-standalone","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"standalone","app.kubernetes.io/part-of":"splunk-stack1-standalone"},"annotations":{"traffic.sidecar.istio.io/excludeOutboundPorts":"8089,8191,9997,7777,9000,17000,17500,19000","traffic.sidecar.istio.io/includeInboundPorts":"8000,8088"}},"spec":{"volumes":[{"name":"defaults"},{"name":"mnt-splunk-secrets","secret":{"secretName":"splunk-stack1-standalone-secret-v1","defaultMode":420}},{"name":"mnt-splunk-defaults","configMap":{"name":"splunk-stack1-standalone-defaults","defaultMode":420}},{"name":"mnt-splunk-jdk","emptyDir":{}},{"name":"mnt-splunk-spark","emptyDir":{}}],"initContainers":[{"name":"init","image":"splunk/spark","command":["bash","-c","cp -r /opt/jdk /mnt \u0026\u0026 cp -r /opt/spark /mnt"],"resources":{"limits":{"cpu":"1","memory":"512Mi"},"requests":{"cpu":"250m","memory":"128Mi"}},"volumeMounts":[{"name":"mnt-splunk-jdk","mountPath":"/mnt/jdk"},{"name":"mnt-splunk-spark","mountPath":"/mnt/spark"}],"imagePullPolicy":"IfNotPresent"}],"containers":[{"name":"splunk","image":"splunk/splunk","ports":[{"name":"http-splunkweb","containerPort":8000,"protocol":"TCP"},{"name":"http-hec","containerPort":8088,"protocol":"TCP"},{"name":"https-splunkd","containerPort":8089,"protocol":"TCP"},{"name":"tcp-dfsmaster","containerPort":9000,"protocol":"TCP"},{"name":"tcp-s2s","containerPort":9997,"protocol":"TCP"},{"name":"tcp-dfccontrol","containerPort":17000,"protocol":"TCP"},{"name":"tcp-datareceive","containerPort":19000,"protocol":"TCP"}],"env":[{"name":"SPLUNK_HOME","value":"/opt/splunk"},{"name":"SPLUNK_START_ARGS","value":"--accept-license"},{"name":"SPLUNK_DEFAULTS_URL","value":"/mnt/splunk-defaults/default.yml,/mnt/defaults/defaults.yml,/mnt/splunk-secrets/default.yml"},{"name":"SPLUNK_HOME_OWNERSHIP_ENFORCEMENT","value":"false"},{"name":"SPLUNK_ROLE","value":"splunk_search_head"},{"name":"SPLUNK_DECLARATIVE_ADMIN_PASSWORD","value":"true"},{"name":"SPLUNK_CLUSTER_MASTER_URL","value":"splunk-stack2-cluster-master-service"},{"name":"SPLUNK_ENABLE_DFS","value":"true"},{"name":"SPARK_MASTER_HOST","value":"splunk-stack1-spark-master-service"},{"name":"SPARK_MASTER_WEBUI_PORT","value":"8009"},{"name":"SPARK_HOME","value":"/mnt/splunk-spark"},{"name":"JAVA_HOME","value":"/mnt/splunk-jdk"},{"name":"SPLUNK_DFW_NUM_SLOTS_ENABLED","value":"false"}],"resources":{"limits":{"cpu":"4","memory":"8Gi"},"requests":{"cpu":"100m","memory":"512Mi"}},"volumeMounts":[{"name":"pvc-etc","mountPath":"/opt/splunk/etc"},{"name":"pvc-var","mountPath":"/opt/splunk/var"},{"name":"defaults","mountPath":"/mnt/defaults"},{"name":"mnt-splunk-secrets","mountPath":"/mnt/splunk-secrets"},{"name":"mnt-splunk-defaults","mountPath":"/mnt/splunk-defaults"},{"name":"mnt-splunk-jdk","mountPath":"/mnt/splunk-jdk"},{"name":"mnt-splunk-spark","mountPath":"/mnt/splunk-spark"}],"livenessProbe":{"exec":{"command":["/sbin/checkstate.sh"]},"initialDelaySeconds":300,"timeoutSeconds":30,"periodSeconds":30},"readinessProbe":{"exec":{"command":["/bin/grep","started","/opt/container_artifact/splunk-container.state"]},"initialDelaySeconds":10,"timeoutSeconds":5,"periodSeconds":5},"imagePullPolicy":"IfNotPresent"}],"securityContext":{"runAsUser":41812,"fsGroup":41812},"affinity":{"podAntiAffinity":{"preferredDuringSchedulingIgnoredDuringExecution":[{"weight":100,"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app.kubernetes.io/instance","operator":"In","values":["splunk-stack1-standalone"]}]},"topologyKey":"kubernetes.io/hostname"}}]}},"schedulerName":"custom-scheduler"}},"volumeClaimTemplates":[{"metadata":{"name":"pvc-etc","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"standalone","app.kubernetes.io/instance":"splunk-stack1-standalone","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"standalone","app.kubernetes.io/part-of":"splunk-stack1-standalone"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"10Gi"}},"storageClassName":"gp2"},"status":{}},{"metadata":{"name":"pvc-var","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"standalone","app.kubernetes.io/instance":"splunk-stack1-standalone","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"standalone","app.kubernetes.io/part-of":"splunk-stack1-standalone"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"100Gi"}},"storageClassName":"gp2"},"status":{}}],"serviceName":"splunk-stack1-standalone-headless","podManagementPolicy":"Parallel","updateStrategy":{"type":"OnDelete"}},"status":{"replicas":0}}`) } func TestApplyStandaloneSmartstoreKeyChangeDetection(t *testing.T) { From ab58f5b3b026cd5b7e27e40f59c7309c3087c83a Mon Sep 17 00:00:00 2001 From: akondur Date: Mon, 11 Jan 2021 13:44:39 -0800 Subject: [PATCH 17/43] Added unit tests for different storage volume combinations --- pkg/splunk/enterprise/configuration_test.go | 80 ++++++++++++++++++--- 1 file changed, 72 insertions(+), 8 deletions(-) diff --git a/pkg/splunk/enterprise/configuration_test.go b/pkg/splunk/enterprise/configuration_test.go index 26cd825d6..452405a4b 100644 --- a/pkg/splunk/enterprise/configuration_test.go +++ b/pkg/splunk/enterprise/configuration_test.go @@ -35,12 +35,18 @@ func configTester(t *testing.T, method string, f func() (interface{}, error), wa if err != nil { t.Errorf("%s returned error: %v", method, err) } - got, err := json.Marshal(result) + + // Marshall the result and compare + marshalAndCompare(t, result, method, want) +} + +func marshalAndCompare(t *testing.T, compare interface{}, method string, want string) { + got, err := json.Marshal(compare) if err != nil { - t.Errorf("%s failed to marshall: %v", method, err) + t.Errorf("%s failed to marshall", err) } if string(got) != want { - t.Errorf("%s = %s;\nwant %s", method, got, want) + t.Errorf("Method %s, got = %s;\nwant %s", method, got, want) } } @@ -884,12 +890,70 @@ func TestAddStorageVolumes(t *testing.T) { }, } - // Define valid common spec + // Default spec spec := &enterprisev1.CommonSplunkSpec{} - err := addStorageVolumes(&cr, spec, statefulSet, labels) - if err != nil { - t.Errorf("Unable to add storage volumes, error: %s", err.Error()) + + test := func(want string) { + ss := statefulSet.DeepCopy() + err := addStorageVolumes(&cr, spec, ss, labels) + if err != nil { + t.Errorf("Unable to add storage volumes, error: %s", err.Error()) + } + marshalAndCompare(t, ss, "TestAddStorageVolumes", want) + } + + // Test defaults - PVCs for etc & var with 10Gi and 100Gi storage capacity + test(`{"kind":"StatefulSet","apiVersion":"apps/v1","metadata":{"name":"test-statefulset","namespace":"test","creationTimestamp":null},"spec":{"replicas":1,"selector":null,"template":{"metadata":{"creationTimestamp":null},"spec":{"containers":[{"name":"splunk","image":"test","resources":{},"volumeMounts":[{"name":"pvc-etc","mountPath":"/opt/splunk/etc"},{"name":"pvc-var","mountPath":"/opt/splunk/var"}]}]}},"volumeClaimTemplates":[{"metadata":{"name":"pvc-etc","namespace":"test","creationTimestamp":null},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"10Gi"}}},"status":{}},{"metadata":{"name":"pvc-var","namespace":"test","creationTimestamp":null},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"100Gi"}}},"status":{}}],"serviceName":"","updateStrategy":{}},"status":{"replicas":0}}`) + + // Define PVCs for etc & var with storage capacity and storage class name defined + spec = &enterprisev1.CommonSplunkSpec{ + EtcVolumeStorageConfig: enterprisev1.StorageClassSpec{ + StorageCapacity: "25Gi", + StorageClassName: "gp2", + }, + VarVolumeStorageConfig: enterprisev1.StorageClassSpec{ + StorageCapacity: "35Gi", + StorageClassName: "gp3", + }, } + test(`{"kind":"StatefulSet","apiVersion":"apps/v1","metadata":{"name":"test-statefulset","namespace":"test","creationTimestamp":null},"spec":{"replicas":1,"selector":null,"template":{"metadata":{"creationTimestamp":null},"spec":{"containers":[{"name":"splunk","image":"test","resources":{},"volumeMounts":[{"name":"pvc-etc","mountPath":"/opt/splunk/etc"},{"name":"pvc-var","mountPath":"/opt/splunk/var"}]}]}},"volumeClaimTemplates":[{"metadata":{"name":"pvc-etc","namespace":"test","creationTimestamp":null},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"25Gi"}},"storageClassName":"gp2"},"status":{}},{"metadata":{"name":"pvc-var","namespace":"test","creationTimestamp":null},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"35Gi"}},"storageClassName":"gp3"},"status":{}}],"serviceName":"","updateStrategy":{}},"status":{"replicas":0}}`) + + // Define PVCs for etc & ephemeral for var + spec = &enterprisev1.CommonSplunkSpec{ + EtcVolumeStorageConfig: enterprisev1.StorageClassSpec{ + StorageCapacity: "25Gi", + StorageClassName: "gp2", + }, + VarVolumeStorageConfig: enterprisev1.StorageClassSpec{ + EphemeralStorage: true, + }, + } + test(`{"kind":"StatefulSet","apiVersion":"apps/v1","metadata":{"name":"test-statefulset","namespace":"test","creationTimestamp":null},"spec":{"replicas":1,"selector":null,"template":{"metadata":{"creationTimestamp":null},"spec":{"volumes":[{"name":"mnt-splunk-var","emptyDir":{}}],"containers":[{"name":"splunk","image":"test","resources":{},"volumeMounts":[{"name":"pvc-etc","mountPath":"/opt/splunk/etc"},{"name":"mnt-splunk-var","mountPath":"/opt/splunk/var"}]}]}},"volumeClaimTemplates":[{"metadata":{"name":"pvc-etc","namespace":"test","creationTimestamp":null},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"25Gi"}},"storageClassName":"gp2"},"status":{}}],"serviceName":"","updateStrategy":{}},"status":{"replicas":0}}`) + + // Define ephemeral for etc & PVCs for var + spec = &enterprisev1.CommonSplunkSpec{ + EtcVolumeStorageConfig: enterprisev1.StorageClassSpec{ + EphemeralStorage: true, + }, + VarVolumeStorageConfig: enterprisev1.StorageClassSpec{ + StorageCapacity: "25Gi", + StorageClassName: "gp2", + }, + } + test(`{"kind":"StatefulSet","apiVersion":"apps/v1","metadata":{"name":"test-statefulset","namespace":"test","creationTimestamp":null},"spec":{"replicas":1,"selector":null,"template":{"metadata":{"creationTimestamp":null},"spec":{"volumes":[{"name":"mnt-splunk-etc","emptyDir":{}}],"containers":[{"name":"splunk","image":"test","resources":{},"volumeMounts":[{"name":"mnt-splunk-etc","mountPath":"/opt/splunk/etc"},{"name":"pvc-var","mountPath":"/opt/splunk/var"}]}]}},"volumeClaimTemplates":[{"metadata":{"name":"pvc-var","namespace":"test","creationTimestamp":null},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"25Gi"}},"storageClassName":"gp2"},"status":{}}],"serviceName":"","updateStrategy":{}},"status":{"replicas":0}}`) + + // Define ephemeral for etc & var(should ignore storage capacity & storage class name) + spec = &enterprisev1.CommonSplunkSpec{ + EtcVolumeStorageConfig: enterprisev1.StorageClassSpec{ + EphemeralStorage: true, + }, + VarVolumeStorageConfig: enterprisev1.StorageClassSpec{ + EphemeralStorage: true, + StorageCapacity: "25Gi", + StorageClassName: "gp2", + }, + } + test(`{"kind":"StatefulSet","apiVersion":"apps/v1","metadata":{"name":"test-statefulset","namespace":"test","creationTimestamp":null},"spec":{"replicas":1,"selector":null,"template":{"metadata":{"creationTimestamp":null},"spec":{"volumes":[{"name":"mnt-splunk-etc","emptyDir":{}},{"name":"mnt-splunk-var","emptyDir":{}}],"containers":[{"name":"splunk","image":"test","resources":{},"volumeMounts":[{"name":"mnt-splunk-etc","mountPath":"/opt/splunk/etc"},{"name":"mnt-splunk-var","mountPath":"/opt/splunk/var"}]}]}},"serviceName":"","updateStrategy":{}},"status":{"replicas":0}}`) // Define invalid EtcVolumeStorageConfig spec = &enterprisev1.CommonSplunkSpec{ @@ -897,7 +961,7 @@ func TestAddStorageVolumes(t *testing.T) { StorageCapacity: "----", }, } - err = addStorageVolumes(&cr, spec, statefulSet, labels) + err := addStorageVolumes(&cr, spec, statefulSet, labels) if err == nil { t.Errorf("Unable to idenitfy incorrect EtcVolumeStorageConfig resource quantity") } From 2e3e5f7d8ea1a765eece621f224c2f2dd84ffdf0 Mon Sep 17 00:00:00 2001 From: Victor Ebken Date: Mon, 11 Jan 2021 12:59:14 -0800 Subject: [PATCH 18/43] templated port name string and made constants for port names, will be able to easily support different port naming conventions if necessary for other meshes such as linkerd --- pkg/splunk/enterprise/configuration.go | 28 +++++++++++++------------- pkg/splunk/enterprise/names.go | 20 ++++++++++++++++++ 2 files changed, 34 insertions(+), 14 deletions(-) diff --git a/pkg/splunk/enterprise/configuration.go b/pkg/splunk/enterprise/configuration.go index 1f3c83861..c4f3b73e4 100644 --- a/pkg/splunk/enterprise/configuration.go +++ b/pkg/splunk/enterprise/configuration.go @@ -246,27 +246,27 @@ func prepareSplunkSmartstoreConfigMap(identifier, namespace string, crKind strin // getSplunkPorts returns a map of ports to use for Splunk instances. func getSplunkPorts(instanceType InstanceType) map[string]int { result := map[string]int{ - "http-splunkweb": 8000, - "https-splunkd": 8089, + GetPortName(splunkwebPort, protoHTTP): 8000, + GetPortName(splunkdPort, protoHTTPS): 8089, } switch instanceType { case SplunkMonitoringConsole: - result["http-hec"] = 8088 - result["tcp-s2s"] = 9997 + result[GetPortName(hecPort, protoHTTP)] = 8088 + result[GetPortName(s2sPort, protoTCP)] = 9997 case SplunkStandalone: - result["tcp-dfccontrol"] = 17000 - result["tcp-datareceive"] = 19000 - result["tcp-dfsmaster"] = 9000 - result["http-hec"] = 8088 - result["tcp-s2s"] = 9997 + result[GetPortName(dfccontrolPort, protoTCP)] = 17000 + result[GetPortName(datareceivePort, protoTCP)] = 19000 + result[GetPortName(dfsmasterPort, protoTCP)] = 9000 + result[GetPortName(hecPort, protoHTTP)] = 8088 + result[GetPortName(s2sPort, protoTCP)] = 9997 case SplunkSearchHead: - result["tcp-dfccontrol"] = 17000 - result["tcp-datareceive"] = 19000 - result["tcp-dfsmaster"] = 9000 + result[GetPortName(dfccontrolPort, protoTCP)] = 17000 + result[GetPortName(datareceivePort, protoTCP)] = 19000 + result[GetPortName(dfsmasterPort, protoTCP)] = 9000 case SplunkIndexer: - result["http-hec"] = 8088 - result["tcp-s2s"] = 9997 + result[GetPortName(hecPort, protoHTTP)] = 8088 + result[GetPortName(s2sPort, protoTCP)] = 9997 } return result diff --git a/pkg/splunk/enterprise/names.go b/pkg/splunk/enterprise/names.go index 4c298e83d..a60d862f4 100644 --- a/pkg/splunk/enterprise/names.go +++ b/pkg/splunk/enterprise/names.go @@ -73,6 +73,21 @@ const ( //smartstoreconfigToken used to track if the config is reflecting on Pod or not configToken = "conftoken" + + // port names and templates and protocols + portNameTemplateStr = "%s-%s" + + splunkwebPort = "splunkweb" + splunkdPort = "splunkd" + s2sPort = "s2s" + hecPort = "hec" + dfccontrolPort = "dfccontrol" + datareceivePort = "datareceive" + dfsmasterPort = "dfsmaster" + + protoHTTP = "http" + protoHTTPS = "https" + protoTCP = "tcp" ) // GetSplunkDeploymentName uses a template to name a Kubernetes Deployment for Splunk instances. @@ -158,3 +173,8 @@ func GetSplunkImage(specImage string) string { return name } + +// GetPortName uses a template to enrich a port name with protocol information for usage with mesh services +func GetPortName(port string, protocol string) string { + return fmt.Sprintf(portNameTemplateStr, protocol, port) +} From d025400acca7d565886f1e360bb3c8f93fbea5b7 Mon Sep 17 00:00:00 2001 From: Julien Ambrosiano Date: Fri, 8 Jan 2021 15:47:48 -0800 Subject: [PATCH 19/43] CSPL-578: Adding testcase lm with standalone --- test/env.sh | 4 +- test/run-tests.sh | 10 ++++ test/smoke/smoke_test.go | 29 ++++++++++++ test/testenv/deployment.go | 22 +++++++++ test/testenv/lmutil.go | 77 +++++++++++++++++++++++++++++++ test/testenv/testenv.go | 8 ++++ test/testenv/util.go | 29 ++++++++++++ test/testenv/verificationutils.go | 33 +++++++++++++ 8 files changed, 211 insertions(+), 1 deletion(-) create mode 100644 test/testenv/lmutil.go diff --git a/test/env.sh b/test/env.sh index a9c092238..b025153d7 100644 --- a/test/env.sh +++ b/test/env.sh @@ -10,6 +10,8 @@ : "${ECR_REGISTRY:=}" : "${VPC_PUBLIC_SUBNET_STRING:=}" : "${VPC_PRIVATE_SUBNET_STRING:=}" +: "${ENTERPRISE_LICENSE_PATH:=}" +: "${TEST_S3_BUCKET:=}" # Docker registry to use to push the test images to and pull from in the cluster if [ -z "${PRIVATE_REGISTRY}" ]; then @@ -25,4 +27,4 @@ if [ -z "${PRIVATE_REGISTRY}" ]; then PRIVATE_REGISTRY="${ECR_REGISTRY}" ;; esac -fi +fi \ No newline at end of file diff --git a/test/run-tests.sh b/test/run-tests.sh index 77aeb1b02..fd53fdfcc 100755 --- a/test/run-tests.sh +++ b/test/run-tests.sh @@ -86,5 +86,15 @@ else echo "Running following test :: ${TEST_TO_RUN}" fi +# Set env variable for LM +if [[ -z "${ENTERPRISE_LICENSE_LOCATION}" ]]; then + echo "License path not set. Changing to default" + export ENTERPRISE_LICENSE_LOCATION="${ENTERPRISE_LICENSE_PATH}" +fi +if [[ -z "${TEST_BUCKET}" ]]; then + echo "Test bucket not set. Changing to default" + export TEST_BUCKET="${TEST_S3_BUCKET}" +fi + # Running only smoke test cases by default or value passed through TEST_FOCUS env variable. To run different test packages add/remove path from focus argument or TEST_FOCUS variable ginkgo -v -progress -r -stream -nodes=${NUM_NODES} --focus="${TEST_TO_RUN}" ${topdir}/test -- -commit-hash=${COMMIT_HASH} -operator-image=${PRIVATE_SPLUNK_OPERATOR_IMAGE} -splunk-image=${PRIVATE_SPLUNK_ENTERPRISE_IMAGE} \ No newline at end of file diff --git a/test/smoke/smoke_test.go b/test/smoke/smoke_test.go index 48eaf4b40..a71412026 100644 --- a/test/smoke/smoke_test.go +++ b/test/smoke/smoke_test.go @@ -1,6 +1,7 @@ package smoke import ( + "fmt" "os/exec" "strings" @@ -125,4 +126,32 @@ var _ = Describe("Smoke test", func() { testenv.VerifyRFSFMet(deployment, testenvInstance) }) }) + + Context("Standalone deployment (S1) with LM", func() { + It("smoke: can deploy a standalone instance and a License Master", func() { + // Download License File + licenseFilePath, err := testenv.DownloadFromS3Bucket() + Expect(err).To(Succeed(), "Unable to downlaod license file") + + // Create License Config Map + testenvInstance.CreateLicenseConfigMap(licenseFilePath) + + // Create standalone Deployment with License Master + standalone, err := deployment.DeployStandaloneWithLM(deployment.GetName()) + Expect(err).To(Succeed(), "Unable to deploy standalone instance with LM") + + // Wait for License Master to be in READY status + testenv.LicenseMasterReady(deployment, testenvInstance) + + // Wait for Standalone to be in READY status + testenv.StandaloneReady(deployment, deployment.GetName(), standalone, testenvInstance) + + // Verify MC Pod is Ready + testenv.MCPodReady(testenvInstance.GetName(), deployment) + + // Verify LM is configured on standalone instance + standalonePodName := fmt.Sprintf(testenv.StandalonePod, deployment.GetName(), 0) + testenv.VerifyLMConfiguredOnPod(deployment, standalonePodName) + }) + }) }) diff --git a/test/testenv/deployment.go b/test/testenv/deployment.go index af2aaa4a5..0630b655d 100644 --- a/test/testenv/deployment.go +++ b/test/testenv/deployment.go @@ -381,3 +381,25 @@ func (d *Deployment) DeployMultisiteCluster(name string, indexerReplicas int, si return nil } + +// DeployStandaloneWithLM deploys a standalone splunk enterprise instance with license master on the specified testenv +func (d *Deployment) DeployStandaloneWithLM(name string) (*enterprisev1.Standalone, error) { + var licenseMaster string + + // If license file specified, deploy License Master + if d.testenv.licenseFilePath != "" { + // Deploy the license master + _, err := d.DeployLicenseMaster(name) + if err != nil { + return nil, err + } + licenseMaster = name + } + + standalone := newStandaloneWithLM(name, d.testenv.namespace, licenseMaster) + deployed, err := d.deployCR(name, standalone) + if err != nil { + return nil, err + } + return deployed.(*enterprisev1.Standalone), err +} diff --git a/test/testenv/lmutil.go b/test/testenv/lmutil.go new file mode 100644 index 000000000..fbafc41a4 --- /dev/null +++ b/test/testenv/lmutil.go @@ -0,0 +1,77 @@ +package testenv + +import ( + "encoding/json" + "fmt" + "os" + "strings" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/s3" + "github.com/aws/aws-sdk-go/service/s3/s3manager" + + logf "sigs.k8s.io/controller-runtime/pkg/log" +) + +type licenserLocalSlaveResponse struct { + Entry []struct { + Name string `json:"name"` + ID string `json:"id"` + Content struct { + GUID []string `json:"guid"` + LastTrackerdbServiceTime int `json:"last_trackerdb_service_time"` + LicenseKeys []string `json:"license_keys"` + MasterGUID string `json:"master_guid"` + MasterURI string `json:"master_uri"` + } `json:"content"` + } `json:"entry"` +} + +// CheckLicenseMasterConfigured checks if lm is configured on given pod +func CheckLicenseMasterConfigured(deployment *Deployment, podName string) bool { + stdin := "curl -ks -u admin:$(cat /mnt/splunk-secrets/password) https://localhost:8089/services/licenser/localslave?output_mode=json" + command := []string{"/bin/sh"} + stdout, stderr, err := deployment.PodExecCommand(podName, command, stdin, false) + if err != nil { + logf.Log.Error(err, "Failed to execute command on pod", "pod", podName, "command", command) + return false + } + logf.Log.Info("Command executed on pod", "pod", podName, "command", command, "stdin", stdin, "stdout", stdout, "stderr", stderr) + restResponse := licenserLocalSlaveResponse{} + err = json.Unmarshal([]byte(stdout), &restResponse) + if err != nil { + logf.Log.Error(err, "Failed to parse health status") + return false + } + licenseMaster := restResponse.Entry[0].Content.MasterURI + logf.Log.Info("License Master configuration on POD", "POD", podName, "License Master", licenseMaster) + return strings.Contains(licenseMaster, "license-master-service:8089") +} + +// DownloadFromS3Bucket downloads license file from S3 +func DownloadFromS3Bucket() (string, error) { + dataBucket := os.Getenv("TEST_BUCKET") + location := os.Getenv("ENTERPRISE_LICENSE_LOCATION") + fmt.Printf("%s : dataBucket %s : location\n", os.Getenv("TEST_BUCKET"), os.Getenv("ENTERPRISE_LICENSE_LOCATION")) + item := "enterprise.lic" + file, err := os.Create(item) + if err != nil { + logf.Log.Error(err, "Failed to create license file") + } + defer file.Close() + + sess, _ := session.NewSession(&aws.Config{Region: aws.String("us-west-2")}) + downloader := s3manager.NewDownloader(sess) + numBytes, err := downloader.Download(file, + &s3.GetObjectInput{ + Bucket: aws.String(dataBucket), + Key: aws.String(location + "/" + "enterprise.lic"), + }) + if err != nil { + logf.Log.Error(err, "Failed to download license file") + } + + logf.Log.Info("Downloaded", "filename", file.Name(), "bytes", numBytes) + return file.Name(), err +} diff --git a/test/testenv/testenv.go b/test/testenv/testenv.go index f65173b5d..ba478c0b3 100644 --- a/test/testenv/testenv.go +++ b/test/testenv/testenv.go @@ -449,6 +449,14 @@ func (testenv *TestEnv) createOperator() error { return nil } +// CreateLicenseConfigMap sets the license file path and create config map. +// Required if license file path is not present during TestEnv initialization +func (testenv *TestEnv) CreateLicenseConfigMap(path string) error { + testenv.licenseFilePath = path + err := testenv.createLicenseConfigMap() + return err +} + func (testenv *TestEnv) createLicenseConfigMap() error { lic, err := newLicenseConfigMap(testenv.licenseCMName, testenv.namespace, testenv.licenseFilePath) if err != nil { diff --git a/test/testenv/util.go b/test/testenv/util.go index 5e3b27ca3..637a1cf88 100644 --- a/test/testenv/util.go +++ b/test/testenv/util.go @@ -344,6 +344,35 @@ func newOperator(name, ns, account, operatorImageAndTag, splunkEnterpriseImageAn return &operator } +// newStandaloneWithLM creates and initializes CR for Standalone Kind with License Master +func newStandaloneWithLM(name, ns string, licenseMasterName string) *enterprisev1.Standalone { + + new := enterprisev1.Standalone{ + TypeMeta: metav1.TypeMeta{ + Kind: "Standalone", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: ns, + Finalizers: []string{"enterprise.splunk.com/delete-pvc"}, + }, + + Spec: enterprisev1.StandaloneSpec{ + CommonSplunkSpec: enterprisev1.CommonSplunkSpec{ + Spec: splcommon.Spec{ + ImagePullPolicy: "IfNotPresent", + }, + LicenseMasterRef: corev1.ObjectReference{ + Name: licenseMasterName, + }, + Volumes: []corev1.Volume{}, + }, + }, + } + + return &new +} + // DumpGetPods prints list of pods in the namespace func DumpGetPods(ns string) { output, err := exec.Command("kubectl", "get", "pods", "-n", ns).Output() diff --git a/test/testenv/verificationutils.go b/test/testenv/verificationutils.go index 794845606..fc11b4b72 100644 --- a/test/testenv/verificationutils.go +++ b/test/testenv/verificationutils.go @@ -3,6 +3,7 @@ package testenv import ( "encoding/json" "fmt" + gomega "github.com/onsi/gomega" enterprisev1 "github.com/splunk/splunk-operator/pkg/apis/enterprise/v1beta1" @@ -176,3 +177,35 @@ func VerifyNoSHCInNamespace(deployment *Deployment, testenvInstance *TestEnv) { return shcStatus }, deployment.GetTimeout(), PollInterval).Should(gomega.Equal(false)) } + +// LicenseMasterReady verify LM is in ready status and does not flip flop +func LicenseMasterReady(deployment *Deployment, testenvInstance *TestEnv) { + licenseMaster := &enterprisev1.LicenseMaster{} + + testenvInstance.Log.Info("Verifying License Master becomes READY") + gomega.Eventually(func() splcommon.Phase { + err := deployment.GetInstance(deployment.GetName(), licenseMaster) + if err != nil { + return splcommon.PhaseError + } + testenvInstance.Log.Info("Waiting for License Master instance status to be ready", + "instance", licenseMaster.ObjectMeta.Name, "Phase", licenseMaster.Status.Phase) + DumpGetPods(testenvInstance.GetName()) + + return licenseMaster.Status.Phase + }, deployment.GetTimeout(), PollInterval).Should(gomega.Equal(splcommon.PhaseReady)) + + // In a steady state, we should stay in Ready and not flip-flop around + gomega.Consistently(func() splcommon.Phase { + _ = deployment.GetInstance(deployment.GetName(), licenseMaster) + return licenseMaster.Status.Phase + }, ConsistentDuration, ConsistentPollInterval).Should(gomega.Equal(splcommon.PhaseReady)) +} + +// VerifyLMConfiguredOnPod verify LM is configured on given POD +func VerifyLMConfiguredOnPod(deployment *Deployment, podName string) { + gomega.Eventually(func() bool { + lmConfigured := CheckLicenseMasterConfigured(deployment, podName) + return lmConfigured + }, deployment.GetTimeout(), PollInterval).Should(gomega.Equal(true)) +} From 1756859613e86ecc7f813e903a04b5e45eecf545 Mon Sep 17 00:00:00 2001 From: Victor Ebken Date: Mon, 11 Jan 2021 16:01:57 -0800 Subject: [PATCH 20/43] updates after merge conflict --- pkg/splunk/enterprise/indexercluster_test.go | 2 +- pkg/splunk/enterprise/searchheadcluster_test.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/splunk/enterprise/indexercluster_test.go b/pkg/splunk/enterprise/indexercluster_test.go index d46f7b2e4..103769b0f 100644 --- a/pkg/splunk/enterprise/indexercluster_test.go +++ b/pkg/splunk/enterprise/indexercluster_test.go @@ -1033,7 +1033,7 @@ func TestGetIndexerStatefulSet(t *testing.T) { // Block moving DefaultsURLApps to SPLUNK_DEFAULTS_URL for indexer cluster member cr.Spec.DefaultsURLApps = "/mnt/apps/apps.yml" - test(`{"kind":"StatefulSet","apiVersion":"apps/v1","metadata":{"name":"splunk-stack1-indexer","namespace":"test","creationTimestamp":null,"ownerReferences":[{"apiVersion":"","kind":"","name":"stack1","uid":"","controller":true}]},"spec":{"replicas":1,"selector":{"matchLabels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-master1-indexer"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-master1-indexer"},"annotations":{"traffic.sidecar.istio.io/excludeOutboundPorts":"8089,8191,9997,7777,9000,17000,17500,19000","traffic.sidecar.istio.io/includeInboundPorts":"8000,8088"}},"spec":{"volumes":[{"name":"mnt-splunk-secrets","secret":{"secretName":"splunk-stack1-indexer-secret-v1","defaultMode":420}}],"containers":[{"name":"splunk","image":"splunk/splunk","ports":[{"name":"splunkweb","containerPort":8000,"protocol":"TCP"},{"name":"hec","containerPort":8088,"protocol":"TCP"},{"name":"splunkd","containerPort":8089,"protocol":"TCP"},{"name":"s2s","containerPort":9997,"protocol":"TCP"},{"name":"user-defined","containerPort":32000,"protocol":"UDP"}],"env":[{"name":"SPLUNK_HOME","value":"/opt/splunk"},{"name":"SPLUNK_START_ARGS","value":"--accept-license"},{"name":"SPLUNK_DEFAULTS_URL","value":"/mnt/splunk-secrets/default.yml"},{"name":"SPLUNK_HOME_OWNERSHIP_ENFORCEMENT","value":"false"},{"name":"SPLUNK_ROLE","value":"splunk_indexer"},{"name":"SPLUNK_DECLARATIVE_ADMIN_PASSWORD","value":"true"},{"name":"SPLUNK_INDEXER_URL","value":"splunk-stack1-indexer-0.splunk-stack1-indexer-headless.test.svc.cluster.local"},{"name":"SPLUNK_CLUSTER_MASTER_URL","value":"splunk-master1-cluster-master-service"}],"resources":{"limits":{"cpu":"4","memory":"8Gi"},"requests":{"cpu":"100m","memory":"512Mi"}},"volumeMounts":[{"name":"pvc-etc","mountPath":"/opt/splunk/etc"},{"name":"pvc-var","mountPath":"/opt/splunk/var"},{"name":"mnt-splunk-secrets","mountPath":"/mnt/splunk-secrets"}],"livenessProbe":{"exec":{"command":["/sbin/checkstate.sh"]},"initialDelaySeconds":300,"timeoutSeconds":30,"periodSeconds":30},"readinessProbe":{"exec":{"command":["/bin/grep","started","/opt/container_artifact/splunk-container.state"]},"initialDelaySeconds":10,"timeoutSeconds":5,"periodSeconds":5},"imagePullPolicy":"IfNotPresent"}],"securityContext":{"runAsUser":41812,"fsGroup":41812},"affinity":{"podAntiAffinity":{"preferredDuringSchedulingIgnoredDuringExecution":[{"weight":100,"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app.kubernetes.io/instance","operator":"In","values":["splunk-stack1-indexer"]}]},"topologyKey":"kubernetes.io/hostname"}}]}},"schedulerName":"default-scheduler"}},"volumeClaimTemplates":[{"metadata":{"name":"pvc-etc","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-master1-indexer"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"10Gi"}}},"status":{}},{"metadata":{"name":"pvc-var","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-master1-indexer"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"100Gi"}}},"status":{}}],"serviceName":"splunk-stack1-indexer-headless","podManagementPolicy":"Parallel","updateStrategy":{"type":"OnDelete"}},"status":{"replicas":0}}`) + test(`{"kind":"StatefulSet","apiVersion":"apps/v1","metadata":{"name":"splunk-stack1-indexer","namespace":"test","creationTimestamp":null,"ownerReferences":[{"apiVersion":"","kind":"","name":"stack1","uid":"","controller":true}]},"spec":{"replicas":1,"selector":{"matchLabels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-master1-indexer"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-master1-indexer"},"annotations":{"traffic.sidecar.istio.io/excludeOutboundPorts":"8089,8191,9997,7777,9000,17000,17500,19000","traffic.sidecar.istio.io/includeInboundPorts":"8000,8088"}},"spec":{"volumes":[{"name":"mnt-splunk-secrets","secret":{"secretName":"splunk-stack1-indexer-secret-v1","defaultMode":420}}],"containers":[{"name":"splunk","image":"splunk/splunk","ports":[{"name":"http-splunkweb","containerPort":8000,"protocol":"TCP"},{"name":"http-hec","containerPort":8088,"protocol":"TCP"},{"name":"https-splunkd","containerPort":8089,"protocol":"TCP"},{"name":"tcp-s2s","containerPort":9997,"protocol":"TCP"},{"name":"user-defined","containerPort":32000,"protocol":"UDP"}],"env":[{"name":"SPLUNK_HOME","value":"/opt/splunk"},{"name":"SPLUNK_START_ARGS","value":"--accept-license"},{"name":"SPLUNK_DEFAULTS_URL","value":"/mnt/splunk-secrets/default.yml"},{"name":"SPLUNK_HOME_OWNERSHIP_ENFORCEMENT","value":"false"},{"name":"SPLUNK_ROLE","value":"splunk_indexer"},{"name":"SPLUNK_DECLARATIVE_ADMIN_PASSWORD","value":"true"},{"name":"SPLUNK_INDEXER_URL","value":"splunk-stack1-indexer-0.splunk-stack1-indexer-headless.test.svc.cluster.local"},{"name":"SPLUNK_CLUSTER_MASTER_URL","value":"splunk-master1-cluster-master-service"}],"resources":{"limits":{"cpu":"4","memory":"8Gi"},"requests":{"cpu":"100m","memory":"512Mi"}},"volumeMounts":[{"name":"pvc-etc","mountPath":"/opt/splunk/etc"},{"name":"pvc-var","mountPath":"/opt/splunk/var"},{"name":"mnt-splunk-secrets","mountPath":"/mnt/splunk-secrets"}],"livenessProbe":{"exec":{"command":["/sbin/checkstate.sh"]},"initialDelaySeconds":300,"timeoutSeconds":30,"periodSeconds":30},"readinessProbe":{"exec":{"command":["/bin/grep","started","/opt/container_artifact/splunk-container.state"]},"initialDelaySeconds":10,"timeoutSeconds":5,"periodSeconds":5},"imagePullPolicy":"IfNotPresent"}],"securityContext":{"runAsUser":41812,"fsGroup":41812},"affinity":{"podAntiAffinity":{"preferredDuringSchedulingIgnoredDuringExecution":[{"weight":100,"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app.kubernetes.io/instance","operator":"In","values":["splunk-stack1-indexer"]}]},"topologyKey":"kubernetes.io/hostname"}}]}},"schedulerName":"default-scheduler"}},"volumeClaimTemplates":[{"metadata":{"name":"pvc-etc","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-master1-indexer"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"10Gi"}}},"status":{}},{"metadata":{"name":"pvc-var","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-master1-indexer"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"100Gi"}}},"status":{}}],"serviceName":"splunk-stack1-indexer-headless","podManagementPolicy":"Parallel","updateStrategy":{"type":"OnDelete"}},"status":{"replicas":0}}`) cr.Spec.ClusterMasterRef.Namespace = "other" if err := validateIndexerClusterSpec(&cr); err == nil { diff --git a/pkg/splunk/enterprise/searchheadcluster_test.go b/pkg/splunk/enterprise/searchheadcluster_test.go index f3d014186..059f7ed0b 100644 --- a/pkg/splunk/enterprise/searchheadcluster_test.go +++ b/pkg/splunk/enterprise/searchheadcluster_test.go @@ -560,7 +560,7 @@ func TestGetSearchHeadStatefulSet(t *testing.T) { // DefaultsURLApps should not be passed to SPLUNK_DEFAULTS_URL for SHCMember. These apps will be pushed via the SHCDeployer cr.Spec.DefaultsURLApps = "/mnt/apps/apps.yml" - test(`{"kind":"StatefulSet","apiVersion":"apps/v1","metadata":{"name":"splunk-stack1-search-head","namespace":"test","creationTimestamp":null,"ownerReferences":[{"apiVersion":"","kind":"","name":"stack1","uid":"","controller":true}]},"spec":{"replicas":6,"selector":{"matchLabels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-search-head","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"search-head","app.kubernetes.io/part-of":"splunk-stack1-search-head"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-search-head","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"search-head","app.kubernetes.io/part-of":"splunk-stack1-search-head"},"annotations":{"traffic.sidecar.istio.io/excludeOutboundPorts":"8089,8191,9997,7777,9000,17000,17500,19000","traffic.sidecar.istio.io/includeInboundPorts":"8000"}},"spec":{"volumes":[{"name":"mnt-splunk-secrets","secret":{"secretName":"splunk-stack1-search-head-secret-v1","defaultMode":420}},{"name":"mnt-splunk-jdk","emptyDir":{}},{"name":"mnt-splunk-spark","emptyDir":{}}],"initContainers":[{"name":"init","image":"splunk/spark","command":["bash","-c","cp -r /opt/jdk /mnt \u0026\u0026 cp -r /opt/spark /mnt"],"resources":{"limits":{"cpu":"1","memory":"512Mi"},"requests":{"cpu":"250m","memory":"128Mi"}},"volumeMounts":[{"name":"mnt-splunk-jdk","mountPath":"/mnt/jdk"},{"name":"mnt-splunk-spark","mountPath":"/mnt/spark"}],"imagePullPolicy":"IfNotPresent"}],"containers":[{"name":"splunk","image":"splunk/splunk","ports":[{"name":"splunkweb","containerPort":8000,"protocol":"TCP"},{"name":"splunkd","containerPort":8089,"protocol":"TCP"},{"name":"dfsmaster","containerPort":9000,"protocol":"TCP"},{"name":"dfccontrol","containerPort":17000,"protocol":"TCP"},{"name":"datareceive","containerPort":19000,"protocol":"TCP"}],"env":[{"name":"SPLUNK_HOME","value":"/opt/splunk"},{"name":"SPLUNK_START_ARGS","value":"--accept-license"},{"name":"SPLUNK_DEFAULTS_URL","value":"/mnt/splunk-secrets/default.yml"},{"name":"SPLUNK_HOME_OWNERSHIP_ENFORCEMENT","value":"false"},{"name":"SPLUNK_ROLE","value":"splunk_search_head"},{"name":"SPLUNK_DECLARATIVE_ADMIN_PASSWORD","value":"true"},{"name":"SPLUNK_SEARCH_HEAD_URL","value":"splunk-stack1-search-head-0.splunk-stack1-search-head-headless.test.svc.cluster.local,splunk-stack1-search-head-1.splunk-stack1-search-head-headless.test.svc.cluster.local,splunk-stack1-search-head-2.splunk-stack1-search-head-headless.test.svc.cluster.local,splunk-stack1-search-head-3.splunk-stack1-search-head-headless.test.svc.cluster.local,splunk-stack1-search-head-4.splunk-stack1-search-head-headless.test.svc.cluster.local,splunk-stack1-search-head-5.splunk-stack1-search-head-headless.test.svc.cluster.local"},{"name":"SPLUNK_SEARCH_HEAD_CAPTAIN_URL","value":"splunk-stack1-search-head-0.splunk-stack1-search-head-headless.test.svc.cluster.local"},{"name":"SPLUNK_DEPLOYER_URL","value":"splunk-stack1-deployer-service"},{"name":"SPLUNK_CLUSTER_MASTER_URL","value":"splunk-stack1-cluster-master-service.test2.svc.cluster.local"},{"name":"SPLUNK_ENABLE_DFS","value":"true"},{"name":"SPARK_MASTER_HOST","value":"splunk-stack1-spark-master-service"},{"name":"SPARK_MASTER_WEBUI_PORT","value":"8009"},{"name":"SPARK_HOME","value":"/mnt/splunk-spark"},{"name":"JAVA_HOME","value":"/mnt/splunk-jdk"},{"name":"SPLUNK_DFW_NUM_SLOTS_ENABLED","value":"true"}],"resources":{"limits":{"cpu":"4","memory":"8Gi"},"requests":{"cpu":"100m","memory":"512Mi"}},"volumeMounts":[{"name":"pvc-etc","mountPath":"/opt/splunk/etc"},{"name":"pvc-var","mountPath":"/opt/splunk/var"},{"name":"mnt-splunk-secrets","mountPath":"/mnt/splunk-secrets"},{"name":"mnt-splunk-jdk","mountPath":"/mnt/splunk-jdk"},{"name":"mnt-splunk-spark","mountPath":"/mnt/splunk-spark"}],"livenessProbe":{"exec":{"command":["/sbin/checkstate.sh"]},"initialDelaySeconds":300,"timeoutSeconds":30,"periodSeconds":30},"readinessProbe":{"exec":{"command":["/bin/grep","started","/opt/container_artifact/splunk-container.state"]},"initialDelaySeconds":10,"timeoutSeconds":5,"periodSeconds":5},"imagePullPolicy":"IfNotPresent"}],"securityContext":{"runAsUser":41812,"fsGroup":41812},"affinity":{"podAntiAffinity":{"preferredDuringSchedulingIgnoredDuringExecution":[{"weight":100,"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app.kubernetes.io/instance","operator":"In","values":["splunk-stack1-search-head"]}]},"topologyKey":"kubernetes.io/hostname"}}]}},"schedulerName":"default-scheduler"}},"volumeClaimTemplates":[{"metadata":{"name":"pvc-etc","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-search-head","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"search-head","app.kubernetes.io/part-of":"splunk-stack1-search-head"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"10Gi"}}},"status":{}},{"metadata":{"name":"pvc-var","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-search-head","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"search-head","app.kubernetes.io/part-of":"splunk-stack1-search-head"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"100Gi"}}},"status":{}}],"serviceName":"splunk-stack1-search-head-headless","podManagementPolicy":"Parallel","updateStrategy":{"type":"OnDelete"}},"status":{"replicas":0}}`) + test(`{"kind":"StatefulSet","apiVersion":"apps/v1","metadata":{"name":"splunk-stack1-search-head","namespace":"test","creationTimestamp":null,"ownerReferences":[{"apiVersion":"","kind":"","name":"stack1","uid":"","controller":true}]},"spec":{"replicas":6,"selector":{"matchLabels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-search-head","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"search-head","app.kubernetes.io/part-of":"splunk-stack1-search-head"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-search-head","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"search-head","app.kubernetes.io/part-of":"splunk-stack1-search-head"},"annotations":{"traffic.sidecar.istio.io/excludeOutboundPorts":"8089,8191,9997,7777,9000,17000,17500,19000","traffic.sidecar.istio.io/includeInboundPorts":"8000"}},"spec":{"volumes":[{"name":"mnt-splunk-secrets","secret":{"secretName":"splunk-stack1-search-head-secret-v1","defaultMode":420}},{"name":"mnt-splunk-jdk","emptyDir":{}},{"name":"mnt-splunk-spark","emptyDir":{}}],"initContainers":[{"name":"init","image":"splunk/spark","command":["bash","-c","cp -r /opt/jdk /mnt \u0026\u0026 cp -r /opt/spark /mnt"],"resources":{"limits":{"cpu":"1","memory":"512Mi"},"requests":{"cpu":"250m","memory":"128Mi"}},"volumeMounts":[{"name":"mnt-splunk-jdk","mountPath":"/mnt/jdk"},{"name":"mnt-splunk-spark","mountPath":"/mnt/spark"}],"imagePullPolicy":"IfNotPresent"}],"containers":[{"name":"splunk","image":"splunk/splunk","ports":[{"name":"http-splunkweb","containerPort":8000,"protocol":"TCP"},{"name":"https-splunkd","containerPort":8089,"protocol":"TCP"},{"name":"tcp-dfsmaster","containerPort":9000,"protocol":"TCP"},{"name":"tcp-dfccontrol","containerPort":17000,"protocol":"TCP"},{"name":"tcp-datareceive","containerPort":19000,"protocol":"TCP"}],"env":[{"name":"SPLUNK_HOME","value":"/opt/splunk"},{"name":"SPLUNK_START_ARGS","value":"--accept-license"},{"name":"SPLUNK_DEFAULTS_URL","value":"/mnt/splunk-secrets/default.yml"},{"name":"SPLUNK_HOME_OWNERSHIP_ENFORCEMENT","value":"false"},{"name":"SPLUNK_ROLE","value":"splunk_search_head"},{"name":"SPLUNK_DECLARATIVE_ADMIN_PASSWORD","value":"true"},{"name":"SPLUNK_SEARCH_HEAD_URL","value":"splunk-stack1-search-head-0.splunk-stack1-search-head-headless.test.svc.cluster.local,splunk-stack1-search-head-1.splunk-stack1-search-head-headless.test.svc.cluster.local,splunk-stack1-search-head-2.splunk-stack1-search-head-headless.test.svc.cluster.local,splunk-stack1-search-head-3.splunk-stack1-search-head-headless.test.svc.cluster.local,splunk-stack1-search-head-4.splunk-stack1-search-head-headless.test.svc.cluster.local,splunk-stack1-search-head-5.splunk-stack1-search-head-headless.test.svc.cluster.local"},{"name":"SPLUNK_SEARCH_HEAD_CAPTAIN_URL","value":"splunk-stack1-search-head-0.splunk-stack1-search-head-headless.test.svc.cluster.local"},{"name":"SPLUNK_DEPLOYER_URL","value":"splunk-stack1-deployer-service"},{"name":"SPLUNK_CLUSTER_MASTER_URL","value":"splunk-stack1-cluster-master-service.test2.svc.cluster.local"},{"name":"SPLUNK_ENABLE_DFS","value":"true"},{"name":"SPARK_MASTER_HOST","value":"splunk-stack1-spark-master-service"},{"name":"SPARK_MASTER_WEBUI_PORT","value":"8009"},{"name":"SPARK_HOME","value":"/mnt/splunk-spark"},{"name":"JAVA_HOME","value":"/mnt/splunk-jdk"},{"name":"SPLUNK_DFW_NUM_SLOTS_ENABLED","value":"true"}],"resources":{"limits":{"cpu":"4","memory":"8Gi"},"requests":{"cpu":"100m","memory":"512Mi"}},"volumeMounts":[{"name":"pvc-etc","mountPath":"/opt/splunk/etc"},{"name":"pvc-var","mountPath":"/opt/splunk/var"},{"name":"mnt-splunk-secrets","mountPath":"/mnt/splunk-secrets"},{"name":"mnt-splunk-jdk","mountPath":"/mnt/splunk-jdk"},{"name":"mnt-splunk-spark","mountPath":"/mnt/splunk-spark"}],"livenessProbe":{"exec":{"command":["/sbin/checkstate.sh"]},"initialDelaySeconds":300,"timeoutSeconds":30,"periodSeconds":30},"readinessProbe":{"exec":{"command":["/bin/grep","started","/opt/container_artifact/splunk-container.state"]},"initialDelaySeconds":10,"timeoutSeconds":5,"periodSeconds":5},"imagePullPolicy":"IfNotPresent"}],"securityContext":{"runAsUser":41812,"fsGroup":41812},"affinity":{"podAntiAffinity":{"preferredDuringSchedulingIgnoredDuringExecution":[{"weight":100,"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app.kubernetes.io/instance","operator":"In","values":["splunk-stack1-search-head"]}]},"topologyKey":"kubernetes.io/hostname"}}]}},"schedulerName":"default-scheduler"}},"volumeClaimTemplates":[{"metadata":{"name":"pvc-etc","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-search-head","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"search-head","app.kubernetes.io/part-of":"splunk-stack1-search-head"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"10Gi"}}},"status":{}},{"metadata":{"name":"pvc-var","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-search-head","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"search-head","app.kubernetes.io/part-of":"splunk-stack1-search-head"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"100Gi"}}},"status":{}}],"serviceName":"splunk-stack1-search-head-headless","podManagementPolicy":"Parallel","updateStrategy":{"type":"OnDelete"}},"status":{"replicas":0}}`) // Define additional service port in CR and verified the statefulset has the new port cr.Spec.ServiceTemplate.Spec.Ports = []corev1.ServicePort{{Name: "user-defined", Port: 32000, Protocol: "UDP"}} @@ -593,7 +593,7 @@ func TestGetDeployerStatefulSet(t *testing.T) { cr.Spec.Replicas = 3 test(`{"kind":"StatefulSet","apiVersion":"apps/v1","metadata":{"name":"splunk-stack1-deployer","namespace":"test","creationTimestamp":null,"ownerReferences":[{"apiVersion":"","kind":"","name":"stack1","uid":"","controller":true}]},"spec":{"replicas":1,"selector":{"matchLabels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-deployer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"deployer","app.kubernetes.io/part-of":"splunk-stack1-search-head"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-deployer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"deployer","app.kubernetes.io/part-of":"splunk-stack1-search-head"},"annotations":{"traffic.sidecar.istio.io/excludeOutboundPorts":"8089,8191,9997,7777,9000,17000,17500,19000","traffic.sidecar.istio.io/includeInboundPorts":"8000"}},"spec":{"volumes":[{"name":"mnt-splunk-secrets","secret":{"secretName":"splunk-stack1-deployer-secret-v1","defaultMode":420}}],"containers":[{"name":"splunk","image":"splunk/splunk","ports":[{"name":"http-splunkweb","containerPort":8000,"protocol":"TCP"},{"name":"https-splunkd","containerPort":8089,"protocol":"TCP"}],"env":[{"name":"SPLUNK_HOME","value":"/opt/splunk"},{"name":"SPLUNK_START_ARGS","value":"--accept-license"},{"name":"SPLUNK_DEFAULTS_URL","value":"/mnt/splunk-secrets/default.yml"},{"name":"SPLUNK_HOME_OWNERSHIP_ENFORCEMENT","value":"false"},{"name":"SPLUNK_ROLE","value":"splunk_deployer"},{"name":"SPLUNK_DECLARATIVE_ADMIN_PASSWORD","value":"true"},{"name":"SPLUNK_SEARCH_HEAD_URL","value":"splunk-stack1-search-head-0.splunk-stack1-search-head-headless.test.svc.cluster.local,splunk-stack1-search-head-1.splunk-stack1-search-head-headless.test.svc.cluster.local,splunk-stack1-search-head-2.splunk-stack1-search-head-headless.test.svc.cluster.local"},{"name":"SPLUNK_SEARCH_HEAD_CAPTAIN_URL","value":"splunk-stack1-search-head-0.splunk-stack1-search-head-headless.test.svc.cluster.local"}],"resources":{"limits":{"cpu":"4","memory":"8Gi"},"requests":{"cpu":"100m","memory":"512Mi"}},"volumeMounts":[{"name":"pvc-etc","mountPath":"/opt/splunk/etc"},{"name":"pvc-var","mountPath":"/opt/splunk/var"},{"name":"mnt-splunk-secrets","mountPath":"/mnt/splunk-secrets"}],"livenessProbe":{"exec":{"command":["/sbin/checkstate.sh"]},"initialDelaySeconds":300,"timeoutSeconds":30,"periodSeconds":30},"readinessProbe":{"exec":{"command":["/bin/grep","started","/opt/container_artifact/splunk-container.state"]},"initialDelaySeconds":10,"timeoutSeconds":5,"periodSeconds":5},"imagePullPolicy":"IfNotPresent"}],"securityContext":{"runAsUser":41812,"fsGroup":41812},"affinity":{"podAntiAffinity":{"preferredDuringSchedulingIgnoredDuringExecution":[{"weight":100,"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app.kubernetes.io/instance","operator":"In","values":["splunk-stack1-deployer"]}]},"topologyKey":"kubernetes.io/hostname"}}]}},"schedulerName":"default-scheduler"}},"volumeClaimTemplates":[{"metadata":{"name":"pvc-etc","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-deployer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"deployer","app.kubernetes.io/part-of":"splunk-stack1-search-head"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"10Gi"}}},"status":{}},{"metadata":{"name":"pvc-var","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-deployer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"deployer","app.kubernetes.io/part-of":"splunk-stack1-search-head"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"100Gi"}}},"status":{}}],"serviceName":"splunk-stack1-deployer-headless","podManagementPolicy":"Parallel","updateStrategy":{"type":"OnDelete"}},"status":{"replicas":0}}`) - + // Allow installation of apps via DefaultsURLApps on the SHCDeployer cr.Spec.DefaultsURLApps = "/mnt/apps/apps.yml" test(`{"kind":"StatefulSet","apiVersion":"apps/v1","metadata":{"name":"splunk-stack1-deployer","namespace":"test","creationTimestamp":null,"ownerReferences":[{"apiVersion":"","kind":"","name":"stack1","uid":"","controller":true}]},"spec":{"replicas":1,"selector":{"matchLabels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-deployer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"deployer","app.kubernetes.io/part-of":"splunk-stack1-search-head"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-deployer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"deployer","app.kubernetes.io/part-of":"splunk-stack1-search-head"},"annotations":{"traffic.sidecar.istio.io/excludeOutboundPorts":"8089,8191,9997,7777,9000,17000,17500,19000","traffic.sidecar.istio.io/includeInboundPorts":"8000"}},"spec":{"volumes":[{"name":"mnt-splunk-secrets","secret":{"secretName":"splunk-stack1-deployer-secret-v1","defaultMode":420}}],"containers":[{"name":"splunk","image":"splunk/splunk","ports":[{"name":"http-splunkweb","containerPort":8000,"protocol":"TCP"},{"name":"https-splunkd","containerPort":8089,"protocol":"TCP"}],"env":[{"name":"SPLUNK_HOME","value":"/opt/splunk"},{"name":"SPLUNK_START_ARGS","value":"--accept-license"},{"name":"SPLUNK_DEFAULTS_URL","value":"/mnt/apps/apps.yml,/mnt/splunk-secrets/default.yml"},{"name":"SPLUNK_HOME_OWNERSHIP_ENFORCEMENT","value":"false"},{"name":"SPLUNK_ROLE","value":"splunk_deployer"},{"name":"SPLUNK_DECLARATIVE_ADMIN_PASSWORD","value":"true"},{"name":"SPLUNK_SEARCH_HEAD_URL","value":"splunk-stack1-search-head-0.splunk-stack1-search-head-headless.test.svc.cluster.local,splunk-stack1-search-head-1.splunk-stack1-search-head-headless.test.svc.cluster.local,splunk-stack1-search-head-2.splunk-stack1-search-head-headless.test.svc.cluster.local"},{"name":"SPLUNK_SEARCH_HEAD_CAPTAIN_URL","value":"splunk-stack1-search-head-0.splunk-stack1-search-head-headless.test.svc.cluster.local"}],"resources":{"limits":{"cpu":"4","memory":"8Gi"},"requests":{"cpu":"100m","memory":"512Mi"}},"volumeMounts":[{"name":"pvc-etc","mountPath":"/opt/splunk/etc"},{"name":"pvc-var","mountPath":"/opt/splunk/var"},{"name":"mnt-splunk-secrets","mountPath":"/mnt/splunk-secrets"}],"livenessProbe":{"exec":{"command":["/sbin/checkstate.sh"]},"initialDelaySeconds":300,"timeoutSeconds":30,"periodSeconds":30},"readinessProbe":{"exec":{"command":["/bin/grep","started","/opt/container_artifact/splunk-container.state"]},"initialDelaySeconds":10,"timeoutSeconds":5,"periodSeconds":5},"imagePullPolicy":"IfNotPresent"}],"securityContext":{"runAsUser":41812,"fsGroup":41812},"affinity":{"podAntiAffinity":{"preferredDuringSchedulingIgnoredDuringExecution":[{"weight":100,"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app.kubernetes.io/instance","operator":"In","values":["splunk-stack1-deployer"]}]},"topologyKey":"kubernetes.io/hostname"}}]}},"schedulerName":"default-scheduler"}},"volumeClaimTemplates":[{"metadata":{"name":"pvc-etc","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-deployer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"deployer","app.kubernetes.io/part-of":"splunk-stack1-search-head"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"10Gi"}}},"status":{}},{"metadata":{"name":"pvc-var","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"search-head","app.kubernetes.io/instance":"splunk-stack1-deployer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"deployer","app.kubernetes.io/part-of":"splunk-stack1-search-head"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"100Gi"}}},"status":{}}],"serviceName":"splunk-stack1-deployer-headless","podManagementPolicy":"Parallel","updateStrategy":{"type":"OnDelete"}},"status":{"replicas":0}}`) From b2b5cd3053da765b31e9e1ba72d35aab1ba6a939 Mon Sep 17 00:00:00 2001 From: akondur Date: Mon, 11 Jan 2021 16:36:44 -0800 Subject: [PATCH 21/43] Added a short name for standalone CRD --- deploy/crds/enterprise.splunk.com_standalones_crd.yaml | 2 ++ .../splunk/0.2.1/enterprise.splunk.com_standalones_crd.yaml | 2 ++ pkg/apis/enterprise/v1beta1/standalone_types.go | 2 +- 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/deploy/crds/enterprise.splunk.com_standalones_crd.yaml b/deploy/crds/enterprise.splunk.com_standalones_crd.yaml index 293f7bdfe..77beafa5e 100644 --- a/deploy/crds/enterprise.splunk.com_standalones_crd.yaml +++ b/deploy/crds/enterprise.splunk.com_standalones_crd.yaml @@ -25,6 +25,8 @@ spec: kind: Standalone listKind: StandaloneList plural: standalones + shortNames: + - stdaln singular: standalone scope: Namespaced subresources: diff --git a/deploy/olm-catalog/splunk/0.2.1/enterprise.splunk.com_standalones_crd.yaml b/deploy/olm-catalog/splunk/0.2.1/enterprise.splunk.com_standalones_crd.yaml index 044014cd5..229b0d04a 100644 --- a/deploy/olm-catalog/splunk/0.2.1/enterprise.splunk.com_standalones_crd.yaml +++ b/deploy/olm-catalog/splunk/0.2.1/enterprise.splunk.com_standalones_crd.yaml @@ -25,6 +25,8 @@ spec: kind: Standalone listKind: StandaloneList plural: standalones + shortNames: + - stdaln singular: standalone scope: Namespaced subresources: diff --git a/pkg/apis/enterprise/v1beta1/standalone_types.go b/pkg/apis/enterprise/v1beta1/standalone_types.go index 3d616a418..07c65f87c 100644 --- a/pkg/apis/enterprise/v1beta1/standalone_types.go +++ b/pkg/apis/enterprise/v1beta1/standalone_types.go @@ -73,7 +73,7 @@ type StandaloneStatus struct { // Standalone is the Schema for a Splunk Enterprise standalone instances. // +kubebuilder:subresource:status // +kubebuilder:subresource:scale:specpath=.spec.replicas,statuspath=.status.replicas,selectorpath=.status.selector -// +kubebuilder:resource:path=standalones,scope=Namespaced +// +kubebuilder:resource:path=standalones,scope=Namespaced,shortName=stdaln // +kubebuilder:printcolumn:name="Phase",type="string",JSONPath=".status.phase",description="Status of standalone instances" // +kubebuilder:printcolumn:name="Desired",type="integer",JSONPath=".status.replicas",description="Number of desired standalone instances" // +kubebuilder:printcolumn:name="Ready",type="integer",JSONPath=".status.readyReplicas",description="Current number of ready standalone instances" From 094eead96a4683407e6f4a90c8bed84d3334748e Mon Sep 17 00:00:00 2001 From: Romain BELLANGER Date: Wed, 9 Dec 2020 17:39:18 +0100 Subject: [PATCH 22/43] Monitoring-console: mount emptyDir to var and etc paths to remove dependency on Docker storage driver --- pkg/splunk/enterprise/monitoringconsole.go | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/pkg/splunk/enterprise/monitoringconsole.go b/pkg/splunk/enterprise/monitoringconsole.go index cde444abd..421fa473a 100644 --- a/pkg/splunk/enterprise/monitoringconsole.go +++ b/pkg/splunk/enterprise/monitoringconsole.go @@ -147,6 +147,10 @@ func getMonitoringConsoleStatefulSet(client splcommon.ControllerClient, cr splco labels[k] = v } + emptyVolumeSource := corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + } + //create statefulset configuration statefulSet := &appsv1.StatefulSet{ TypeMeta: metav1.TypeMeta{ @@ -191,8 +195,22 @@ func getMonitoringConsoleStatefulSet(client splcommon.ControllerClient, cr splco }, }, }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: "mnt-splunk-etc", + MountPath: "/opt/splunk/etc", + }, + { + Name: "mnt-splunk-var", + MountPath: "/opt/splunk/var", + }, + }, }, }, + Volumes: []corev1.Volume{ + {Name: "mnt-splunk-etc", VolumeSource: emptyVolumeSource}, + {Name: "mnt-splunk-var", VolumeSource: emptyVolumeSource}, + }, }, }, }, From ca175f6442e055db16ff6097a260fb8bf55f355b Mon Sep 17 00:00:00 2001 From: Romain BELLANGER Date: Wed, 13 Jan 2021 14:53:28 +0100 Subject: [PATCH 23/43] Monitoring Console: add a unit test to validate the generated StatefulSet --- .../enterprise/monitoringconsole_test.go | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/pkg/splunk/enterprise/monitoringconsole_test.go b/pkg/splunk/enterprise/monitoringconsole_test.go index beebfa398..45a894cef 100644 --- a/pkg/splunk/enterprise/monitoringconsole_test.go +++ b/pkg/splunk/enterprise/monitoringconsole_test.go @@ -15,6 +15,7 @@ package enterprise import ( + "fmt" "testing" appsv1 "k8s.io/api/apps/v1" @@ -205,3 +206,47 @@ func TestApplyMonitoringConsoleEnvConfigMap(t *testing.T) { spltest.ReconcileTester(t, "TestApplyMonitoringConsoleEnvConfigMap", "test", "test", createCalls, updateCalls, reconcile, false, ¤t) } + +func TestGetMonitoringConsoleStatefulSet(t *testing.T) { + cr := enterprisev1.SearchHeadCluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "stack1", + Namespace: "test", + }, + Spec: enterprisev1.SearchHeadClusterSpec{ + Replicas: 3, + CommonSplunkSpec: enterprisev1.CommonSplunkSpec{ + ClusterMasterRef: corev1.ObjectReference{ + Name: "stack1", + }, + }, + }, + } + + mcConfigMap := corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "splunk-test-monitoring-console", + Namespace: "test", + }, + Data: map[string]string{"a": "b"}, + } + + c := spltest.NewMockClient() + c.AddObject(&mcConfigMap) + _, err := splutil.ApplyNamespaceScopedSecretObject(c, "test") + if err != nil { + t.Errorf("Failed to create namespace scoped object") + } + + test := func(want string) { + f := func() (interface{}, error) { + if err := validateSearchHeadClusterSpec(&cr.Spec); err != nil { + t.Errorf("validateSearchHeadClusterSpec() returned error: %v", err) + } + return getMonitoringConsoleStatefulSet(c, &cr, &cr.Spec.CommonSplunkSpec, SplunkMonitoringConsole, "splunk-test-secret") + } + configTester(t, fmt.Sprintf("getMonitoringConsoleStatefulSet"), f, want) + } + + test(`{"kind":"StatefulSet","apiVersion":"apps/v1","metadata":{"name":"splunk-test-monitoring-console","namespace":"test","creationTimestamp":null},"spec":{"replicas":1,"selector":{"matchLabels":{"app.kubernetes.io/component":"monitoring-console","app.kubernetes.io/instance":"splunk-test-monitoring-console","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"monitoring-console","app.kubernetes.io/part-of":"splunk-test-monitoring-console"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app.kubernetes.io/component":"monitoring-console","app.kubernetes.io/instance":"splunk-test-monitoring-console","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"monitoring-console","app.kubernetes.io/part-of":"splunk-test-monitoring-console"},"annotations":{"monitoringConsoleConfigRev":"","traffic.sidecar.istio.io/excludeOutboundPorts":"8089,8191,9997,7777,9000,17000,17500,19000","traffic.sidecar.istio.io/includeInboundPorts":"8000,8088"}},"spec":{"volumes":[{"name":"mnt-splunk-etc","emptyDir":{}},{"name":"mnt-splunk-var","emptyDir":{}},{"name":"mnt-splunk-secrets","secret":{"secretName":"splunk-test-secret","defaultMode":420}}],"containers":[{"name":"splunk","image":"splunk/splunk","ports":[{"name":"splunkweb","containerPort":8000,"protocol":"TCP"},{"name":"hec","containerPort":8088,"protocol":"TCP"},{"name":"splunkd","containerPort":8089,"protocol":"TCP"},{"name":"s2s","containerPort":9997,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"splunk-test-monitoring-console"}}],"env":[{"name":"SPLUNK_HOME","value":"/opt/splunk"},{"name":"SPLUNK_START_ARGS","value":"--accept-license"},{"name":"SPLUNK_DEFAULTS_URL","value":"/mnt/splunk-secrets/default.yml"},{"name":"SPLUNK_HOME_OWNERSHIP_ENFORCEMENT","value":"false"},{"name":"SPLUNK_ROLE","value":"splunk_monitor"},{"name":"SPLUNK_DECLARATIVE_ADMIN_PASSWORD","value":"true"},{"name":"SPLUNK_CLUSTER_MASTER_URL","value":"splunk-stack1-cluster-master-service"}],"resources":{"limits":{"cpu":"4","memory":"8Gi"},"requests":{"cpu":"100m","memory":"512Mi"}},"volumeMounts":[{"name":"mnt-splunk-etc","mountPath":"/opt/splunk/etc"},{"name":"mnt-splunk-var","mountPath":"/opt/splunk/var"},{"name":"mnt-splunk-secrets","mountPath":"/mnt/splunk-secrets"}],"livenessProbe":{"exec":{"command":["/sbin/checkstate.sh"]},"initialDelaySeconds":300,"timeoutSeconds":30,"periodSeconds":30},"readinessProbe":{"exec":{"command":["/bin/grep","started","/opt/container_artifact/splunk-container.state"]},"initialDelaySeconds":10,"timeoutSeconds":5,"periodSeconds":5},"imagePullPolicy":"IfNotPresent"}],"securityContext":{"runAsUser":41812,"fsGroup":41812},"affinity":{"podAntiAffinity":{"preferredDuringSchedulingIgnoredDuringExecution":[{"weight":100,"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app.kubernetes.io/instance","operator":"In","values":["splunk-test-monitoring-console"]}]},"topologyKey":"kubernetes.io/hostname"}}]}},"schedulerName":"default-scheduler"}},"serviceName":"splunk-test-monitoring-console-headless","podManagementPolicy":"Parallel","updateStrategy":{"type":"OnDelete"}},"status":{"replicas":0}}`) +} From e5f3f5768821c2bb7298d270ec737e28e985ee23 Mon Sep 17 00:00:00 2001 From: akondur Date: Wed, 13 Jan 2021 10:36:09 -0800 Subject: [PATCH 24/43] Fixed unit test after merge conflict --- pkg/splunk/enterprise/standalone_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/splunk/enterprise/standalone_test.go b/pkg/splunk/enterprise/standalone_test.go index 465f42ba5..41318223d 100644 --- a/pkg/splunk/enterprise/standalone_test.go +++ b/pkg/splunk/enterprise/standalone_test.go @@ -202,7 +202,7 @@ func TestGetStandaloneStatefulSet(t *testing.T) { cr.Spec.EtcVolumeStorageConfig.EphemeralStorage = true cr.Spec.VarVolumeStorageConfig.EphemeralStorage = true - test(`{"kind":"StatefulSet","apiVersion":"apps/v1","metadata":{"name":"splunk-stack1-standalone","namespace":"test","creationTimestamp":null,"ownerReferences":[{"apiVersion":"","kind":"","name":"stack1","uid":"","controller":true}]},"spec":{"replicas":1,"selector":{"matchLabels":{"app.kubernetes.io/component":"standalone","app.kubernetes.io/instance":"splunk-stack1-standalone","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"standalone","app.kubernetes.io/part-of":"splunk-stack1-standalone"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app.kubernetes.io/component":"standalone","app.kubernetes.io/instance":"splunk-stack1-standalone","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"standalone","app.kubernetes.io/part-of":"splunk-stack1-standalone"},"annotations":{"traffic.sidecar.istio.io/excludeOutboundPorts":"8089,8191,9997,7777,9000,17000,17500,19000","traffic.sidecar.istio.io/includeInboundPorts":"8000,8088"}},"spec":{"volumes":[{"name":"mnt-splunk-etc","emptyDir":{}},{"name":"mnt-splunk-var","emptyDir":{}},{"name":"mnt-splunk-secrets","secret":{"secretName":"splunk-stack1-standalone-secret-v1","defaultMode":420}}],"containers":[{"name":"splunk","image":"splunk/splunk","ports":[{"name":"splunkweb","containerPort":8000,"protocol":"TCP"},{"name":"hec","containerPort":8088,"protocol":"TCP"},{"name":"splunkd","containerPort":8089,"protocol":"TCP"},{"name":"dfsmaster","containerPort":9000,"protocol":"TCP"},{"name":"s2s","containerPort":9997,"protocol":"TCP"},{"name":"dfccontrol","containerPort":17000,"protocol":"TCP"},{"name":"datareceive","containerPort":19000,"protocol":"TCP"}],"env":[{"name":"SPLUNK_HOME","value":"/opt/splunk"},{"name":"SPLUNK_START_ARGS","value":"--accept-license"},{"name":"SPLUNK_DEFAULTS_URL","value":"/mnt/splunk-secrets/default.yml"},{"name":"SPLUNK_HOME_OWNERSHIP_ENFORCEMENT","value":"false"},{"name":"SPLUNK_ROLE","value":"splunk_standalone"},{"name":"SPLUNK_DECLARATIVE_ADMIN_PASSWORD","value":"true"}],"resources":{"limits":{"cpu":"4","memory":"8Gi"},"requests":{"cpu":"100m","memory":"512Mi"}},"volumeMounts":[{"name":"mnt-splunk-etc","mountPath":"/opt/splunk/etc"},{"name":"mnt-splunk-var","mountPath":"/opt/splunk/var"},{"name":"mnt-splunk-secrets","mountPath":"/mnt/splunk-secrets"}],"livenessProbe":{"exec":{"command":["/sbin/checkstate.sh"]},"initialDelaySeconds":300,"timeoutSeconds":30,"periodSeconds":30},"readinessProbe":{"exec":{"command":["/bin/grep","started","/opt/container_artifact/splunk-container.state"]},"initialDelaySeconds":10,"timeoutSeconds":5,"periodSeconds":5},"imagePullPolicy":"IfNotPresent"}],"securityContext":{"runAsUser":41812,"fsGroup":41812},"affinity":{"podAntiAffinity":{"preferredDuringSchedulingIgnoredDuringExecution":[{"weight":100,"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app.kubernetes.io/instance","operator":"In","values":["splunk-stack1-standalone"]}]},"topologyKey":"kubernetes.io/hostname"}}]}},"schedulerName":"default-scheduler"}},"serviceName":"splunk-stack1-standalone-headless","podManagementPolicy":"Parallel","updateStrategy":{"type":"OnDelete"}},"status":{"replicas":0}}`) + test(`{"kind":"StatefulSet","apiVersion":"apps/v1","metadata":{"name":"splunk-stack1-standalone","namespace":"test","creationTimestamp":null,"ownerReferences":[{"apiVersion":"","kind":"","name":"stack1","uid":"","controller":true}]},"spec":{"replicas":1,"selector":{"matchLabels":{"app.kubernetes.io/component":"standalone","app.kubernetes.io/instance":"splunk-stack1-standalone","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"standalone","app.kubernetes.io/part-of":"splunk-stack1-standalone"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app.kubernetes.io/component":"standalone","app.kubernetes.io/instance":"splunk-stack1-standalone","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"standalone","app.kubernetes.io/part-of":"splunk-stack1-standalone"},"annotations":{"traffic.sidecar.istio.io/excludeOutboundPorts":"8089,8191,9997,7777,9000,17000,17500,19000","traffic.sidecar.istio.io/includeInboundPorts":"8000,8088"}},"spec":{"volumes":[{"name":"mnt-splunk-etc","emptyDir":{}},{"name":"mnt-splunk-var","emptyDir":{}},{"name":"mnt-splunk-secrets","secret":{"secretName":"splunk-stack1-standalone-secret-v1","defaultMode":420}}],"containers":[{"name":"splunk","image":"splunk/splunk","ports":[{"name":"http-splunkweb","containerPort":8000,"protocol":"TCP"},{"name":"http-hec","containerPort":8088,"protocol":"TCP"},{"name":"https-splunkd","containerPort":8089,"protocol":"TCP"},{"name":"tcp-dfsmaster","containerPort":9000,"protocol":"TCP"},{"name":"tcp-s2s","containerPort":9997,"protocol":"TCP"},{"name":"tcp-dfccontrol","containerPort":17000,"protocol":"TCP"},{"name":"tcp-datareceive","containerPort":19000,"protocol":"TCP"}],"env":[{"name":"SPLUNK_HOME","value":"/opt/splunk"},{"name":"SPLUNK_START_ARGS","value":"--accept-license"},{"name":"SPLUNK_DEFAULTS_URL","value":"/mnt/splunk-secrets/default.yml"},{"name":"SPLUNK_HOME_OWNERSHIP_ENFORCEMENT","value":"false"},{"name":"SPLUNK_ROLE","value":"splunk_standalone"},{"name":"SPLUNK_DECLARATIVE_ADMIN_PASSWORD","value":"true"}],"resources":{"limits":{"cpu":"4","memory":"8Gi"},"requests":{"cpu":"100m","memory":"512Mi"}},"volumeMounts":[{"name":"mnt-splunk-etc","mountPath":"/opt/splunk/etc"},{"name":"mnt-splunk-var","mountPath":"/opt/splunk/var"},{"name":"mnt-splunk-secrets","mountPath":"/mnt/splunk-secrets"}],"livenessProbe":{"exec":{"command":["/sbin/checkstate.sh"]},"initialDelaySeconds":300,"timeoutSeconds":30,"periodSeconds":30},"readinessProbe":{"exec":{"command":["/bin/grep","started","/opt/container_artifact/splunk-container.state"]},"initialDelaySeconds":10,"timeoutSeconds":5,"periodSeconds":5},"imagePullPolicy":"IfNotPresent"}],"securityContext":{"runAsUser":41812,"fsGroup":41812},"affinity":{"podAntiAffinity":{"preferredDuringSchedulingIgnoredDuringExecution":[{"weight":100,"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app.kubernetes.io/instance","operator":"In","values":["splunk-stack1-standalone"]}]},"topologyKey":"kubernetes.io/hostname"}}]}},"schedulerName":"default-scheduler"}},"serviceName":"splunk-stack1-standalone-headless","podManagementPolicy":"Parallel","updateStrategy":{"type":"OnDelete"}},"status":{"replicas":0}}`) cr.Spec.EtcVolumeStorageConfig.EphemeralStorage = false cr.Spec.VarVolumeStorageConfig.EphemeralStorage = false From c41e0f5c327dd08d894d8dde4ec5b7e1c0019911 Mon Sep 17 00:00:00 2001 From: akondur Date: Thu, 14 Jan 2021 11:08:23 -0800 Subject: [PATCH 25/43] Additional ut check for serviceaccount --- pkg/splunk/controller/serviceaccount_test.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pkg/splunk/controller/serviceaccount_test.go b/pkg/splunk/controller/serviceaccount_test.go index 30f8d37d4..7f837b2fd 100644 --- a/pkg/splunk/controller/serviceaccount_test.go +++ b/pkg/splunk/controller/serviceaccount_test.go @@ -66,8 +66,11 @@ func TestGetServiceAccount(t *testing.T) { } // Make sure serviceAccount exists - _, err = GetServiceAccount(client, namespacedName) + got, err := GetServiceAccount(client, namespacedName) if err != nil { + if got.GetName() != current.GetName() { + t.Errorf("Incorrect service account retrieved got %s want %s", got.GetName(), current.GetName()) + } t.Errorf("Should not return an error, when the serviceAccount exists") } } From 6f3f27eb4edc41be7c35a9a303e63bb796003306 Mon Sep 17 00:00:00 2001 From: akondur Date: Thu, 14 Jan 2021 17:35:32 -0800 Subject: [PATCH 26/43] Fix documentation for defaultsUrl --- docs/Examples.md | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/docs/Examples.md b/docs/Examples.md index 2f173a3d9..9ab90415f 100644 --- a/docs/Examples.md +++ b/docs/Examples.md @@ -426,13 +426,15 @@ spec: defaultsUrl: /mnt/defaults/default.yml ``` -`volumes` will mount the ConfigMap in all of your pods under the -`/mnt/licenses` directory. - -`defaultsUrl` may specify one or more local paths or URLs, each separated -by a comma. For example, you can use a `generic.yml` with common -settings and an `apps.yml` that provides additional parameters for app -installation. +In the above example, `volumes` will mount the `splunk-defaults` ConfigMap +with `default.yml` file under the `/mnt/defaults` directory on all pods of +the Custom Resource `Standalone`. + +`defaultsUrl` represents the full path to the `default.yml` configuration +file on the pods. In addition, `defaultsUrl` may specify one or more local +paths or URLs, each separated by a comma. For example, you can use a `generic.yml` +with common settings and an `apps.yml` that provides additional parameters for +app installation. ```yaml defaultsUrl: "http://myco.com/splunk/generic.yml,/mnt/defaults/apps.yml" From 90b391b4107fe857b8952237c95f62d1dd0a79bb Mon Sep 17 00:00:00 2001 From: akondur Date: Thu, 14 Jan 2021 18:12:56 -0800 Subject: [PATCH 27/43] Additional ut for update scenario --- pkg/splunk/controller/serviceaccount_test.go | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/pkg/splunk/controller/serviceaccount_test.go b/pkg/splunk/controller/serviceaccount_test.go index 7f837b2fd..f60834f25 100644 --- a/pkg/splunk/controller/serviceaccount_test.go +++ b/pkg/splunk/controller/serviceaccount_test.go @@ -73,4 +73,22 @@ func TestGetServiceAccount(t *testing.T) { } t.Errorf("Should not return an error, when the serviceAccount exists") } + + var dummySaName string = "dummy_sa" + + current.Name = dummySaName + // Update serviceAccount + err = ApplyServiceAccount(client, ¤t) + if err != nil { + t.Errorf("Failed to create the serviceAccount. Error: %s", err.Error()) + } + + // Make sure serviceAccount is updated + got, err = GetServiceAccount(client, namespacedName) + if err != nil { + if got.GetName() != dummySaName { + t.Errorf("Incorrect service account retrieved got %s want %s", got.GetName(), current.GetName()) + } + t.Errorf("Should not return an error, when the serviceAccount exists") + } } From 668a8c04ddbfdf8a82fc3b692d1998ac412be407 Mon Sep 17 00:00:00 2001 From: akondur Date: Fri, 15 Jan 2021 12:18:18 -0800 Subject: [PATCH 28/43] Added/modified copyright to source code --- LICENSE | 2 +- cmd/manager/main.go | 2 +- go.mod | 1 + go.sum | 2 ++ pkg/apis/addtoscheme_enterprise_v1beta1.go | 14 ++++++++++++++ pkg/apis/apis.go | 14 ++++++++++++++ pkg/apis/enterprise/group.go | 14 ++++++++++++++ .../enterprise/v1beta1/clustermaster_types.go | 2 +- pkg/apis/enterprise/v1beta1/common_types.go | 2 +- pkg/apis/enterprise/v1beta1/doc.go | 14 ++++++++++++++ .../v1beta1/indexercluster_types.go | 2 +- .../enterprise/v1beta1/licensemaster_types.go | 2 +- pkg/apis/enterprise/v1beta1/register.go | 14 ++++++++++++++ .../v1beta1/searchheadcluster_types.go | 2 +- pkg/apis/enterprise/v1beta1/spark_types.go | 2 +- .../enterprise/v1beta1/standalone_types.go | 2 +- pkg/controller/add_clustermaster.go | 14 ++++++++++++++ pkg/controller/add_indexercluster.go | 2 +- pkg/controller/add_licensemaster.go | 2 +- pkg/controller/add_searchheadcluster.go | 2 +- pkg/controller/add_spark.go | 2 +- pkg/controller/add_standalone.go | 2 +- pkg/controller/controller.go | 2 +- pkg/splunk/client/doc.go | 2 +- pkg/splunk/client/enterprise.go | 2 +- pkg/splunk/client/enterprise_test.go | 2 +- pkg/splunk/common/doc.go | 2 +- pkg/splunk/common/messages.go | 2 +- pkg/splunk/common/names.go | 2 +- pkg/splunk/common/names_test.go | 2 +- pkg/splunk/common/types.go | 2 +- pkg/splunk/common/types_test.go | 2 +- pkg/splunk/common/util.go | 2 +- pkg/splunk/common/util_test.go | 2 +- pkg/splunk/controller/configmap.go | 2 +- pkg/splunk/controller/configmap_test.go | 2 +- pkg/splunk/controller/controller.go | 2 +- pkg/splunk/controller/controller_test.go | 2 +- pkg/splunk/controller/deployment.go | 2 +- pkg/splunk/controller/deployment_test.go | 2 +- pkg/splunk/controller/doc.go | 2 +- pkg/splunk/controller/finalizers.go | 2 +- pkg/splunk/controller/finalizers_test.go | 2 +- pkg/splunk/controller/secret.go | 2 +- pkg/splunk/controller/secret_test.go | 2 +- pkg/splunk/controller/service.go | 2 +- pkg/splunk/controller/service_test.go | 2 +- pkg/splunk/controller/statefulset.go | 2 +- pkg/splunk/controller/statefulset_test.go | 2 +- pkg/splunk/controller/util.go | 2 +- pkg/splunk/controller/util_test.go | 2 +- pkg/splunk/enterprise/clustermaster.go | 2 +- pkg/splunk/enterprise/clustermaster_test.go | 2 +- pkg/splunk/enterprise/configuration.go | 2 +- pkg/splunk/enterprise/configuration_test.go | 2 +- pkg/splunk/enterprise/doc.go | 2 +- pkg/splunk/enterprise/finalizers.go | 2 +- pkg/splunk/enterprise/finalizers_test.go | 2 +- pkg/splunk/enterprise/indexercluster.go | 2 +- pkg/splunk/enterprise/indexercluster_test.go | 2 +- pkg/splunk/enterprise/licensemaster.go | 2 +- pkg/splunk/enterprise/licensemaster_test.go | 2 +- pkg/splunk/enterprise/monitoringconsole.go | 2 +- .../enterprise/monitoringconsole_test.go | 2 +- pkg/splunk/enterprise/names.go | 2 +- pkg/splunk/enterprise/names_test.go | 2 +- pkg/splunk/enterprise/searchheadcluster.go | 2 +- .../enterprise/searchheadcluster_test.go | 2 +- pkg/splunk/enterprise/standalone.go | 2 +- pkg/splunk/enterprise/standalone_test.go | 2 +- pkg/splunk/enterprise/types.go | 2 +- pkg/splunk/enterprise/util.go | 2 +- pkg/splunk/enterprise/util_test.go | 2 +- pkg/splunk/spark/configuration.go | 2 +- pkg/splunk/spark/configuration_test.go | 2 +- pkg/splunk/spark/doc.go | 2 +- pkg/splunk/spark/names.go | 2 +- pkg/splunk/spark/names_test.go | 2 +- pkg/splunk/spark/spark.go | 2 +- pkg/splunk/spark/spark_test.go | 2 +- pkg/splunk/spark/types.go | 2 +- pkg/splunk/test/client.go | 2 +- pkg/splunk/test/controller.go | 2 +- pkg/splunk/test/doc.go | 2 +- pkg/splunk/util/messages.go | 2 +- pkg/splunk/util/secrets.go | 2 +- pkg/splunk/util/secrets_test.go | 2 +- pkg/splunk/util/util.go | 14 ++++++++++++++ pkg/splunk/util/util_test.go | 14 ++++++++++++++ test/delete_cr/deletecr_suite_test.go | 14 ++++++++++++++ test/delete_cr/deletecr_test.go | 14 ++++++++++++++ test/example/example1_test.go | 13 +++++++++++++ test/example/example2_test.go | 13 +++++++++++++ test/example/example_suite_test.go | 13 +++++++++++++ .../ingest_search/ingest_search_suite_test.go | 13 +++++++++++++ test/ingest_search/ingest_search_test.go | 13 +++++++++++++ .../monitoring_console_suite_test.go | 14 ++++++++++++++ .../monitoring_console_test.go | 13 +++++++++++++ test/smoke/cluster_master_sites_response.go | 2 +- test/smoke/smoke_suite_test.go | 13 +++++++++++++ test/smoke/smoke_test.go | 13 +++++++++++++ test/testenv/cmutil.go | 15 +++++++++++++++ test/testenv/deployment.go | 14 ++++++++++++++ test/testenv/ingest_utils.go | 19 ++++++++++++++++++- test/testenv/lmutil.go | 14 ++++++++++++++ test/testenv/mcutil.go | 14 ++++++++++++++ test/testenv/search_head_cluster_utils.go | 14 ++++++++++++++ test/testenv/search_utils.go | 14 ++++++++++++++ test/testenv/testenv.go | 14 ++++++++++++++ test/testenv/util.go | 14 ++++++++++++++ test/testenv/verificationutils.go | 14 ++++++++++++++ version/version.go | 2 +- 112 files changed, 487 insertions(+), 82 deletions(-) diff --git a/LICENSE b/LICENSE index 11471cd2f..3a77ee08d 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright 2020 Splunk Inc. +Copyright 2021 Splunk Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/cmd/manager/main.go b/cmd/manager/main.go index 9cd342f76..d3adc21cf 100644 --- a/cmd/manager/main.go +++ b/cmd/manager/main.go @@ -1,4 +1,4 @@ -// Copyright (c) 2018-2020 Splunk Inc. All rights reserved. +// Copyright (c) 2018-2021 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. diff --git a/go.mod b/go.mod index d34e93ca0..098aa77a4 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/splunk/splunk-operator go 1.13 require ( + github.com/aws/aws-sdk-go v1.17.7 github.com/go-logr/logr v0.1.0 github.com/hashicorp/golang-lru v0.5.4 // indirect github.com/onsi/ginkgo v1.12.2 diff --git a/go.sum b/go.sum index acdc47633..c3938cd6f 100644 --- a/go.sum +++ b/go.sum @@ -68,6 +68,7 @@ github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:l github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/auth0/go-jwt-middleware v0.0.0-20170425171159-5493cabe49f7/go.mod h1:LWMyo4iOLWXHGdBki7NIht1kHru/0wM179h+d3g8ATM= github.com/aws/aws-sdk-go v1.16.26/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/aws/aws-sdk-go v1.17.7 h1:/4+rDPe0W95KBmNGYCG+NUvdL8ssPYBMxL+aSCg6nIA= github.com/aws/aws-sdk-go v1.17.7/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/bazelbuild/bazel-gazelle v0.0.0-20181012220611-c728ce9f663e/go.mod h1:uHBSeeATKpVazAACZBDPL/Nk/UhQDDsJWDlqYJo8/Us= github.com/bazelbuild/buildtools v0.0.0-20180226164855-80c7f0d45d7e/go.mod h1:5JP0TXzWDHXv8qvxRC4InIazwdyDseBDbzESUMKk1yU= @@ -403,6 +404,7 @@ github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANyt github.com/jackc/fake v0.0.0-20150926172116-812a484cc733/go.mod h1:WrMFNQdiFJ80sQsxDoMokWK1W5TQtxBFNpzWTD84ibQ= github.com/jackc/pgx v3.2.0+incompatible/go.mod h1:0ZGrqGqkRlliWnWB4zKnWtjbSWbGkVEFm4TeybAXq+I= github.com/jimstudt/http-authentication v0.0.0-20140401203705-3eca13d6893a/go.mod h1:wK6yTYYcgjHE1Z1QtXACPDjcFJyBskHEdagmnq3vsP8= +github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks= github.com/joefitzgerald/rainbow-reporter v0.1.0/go.mod h1:481CNgqmVHQZzdIbN52CupLJyoVwB10FQ/IQlF1pdL8= diff --git a/pkg/apis/addtoscheme_enterprise_v1beta1.go b/pkg/apis/addtoscheme_enterprise_v1beta1.go index dbb412c36..36ccea732 100644 --- a/pkg/apis/addtoscheme_enterprise_v1beta1.go +++ b/pkg/apis/addtoscheme_enterprise_v1beta1.go @@ -1,3 +1,17 @@ +// Copyright (c) 2018-2021 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 apis import ( diff --git a/pkg/apis/apis.go b/pkg/apis/apis.go index 07dc96164..4c5251197 100644 --- a/pkg/apis/apis.go +++ b/pkg/apis/apis.go @@ -1,3 +1,17 @@ +// Copyright (c) 2018-2021 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 apis import ( diff --git a/pkg/apis/enterprise/group.go b/pkg/apis/enterprise/group.go index bf918ee2c..6c3731770 100644 --- a/pkg/apis/enterprise/group.go +++ b/pkg/apis/enterprise/group.go @@ -1,3 +1,17 @@ +// Copyright (c) 2018-2021 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 enterprise contains enterprise API versions. // // This file ensures Go source parsers acknowledge the enterprise package diff --git a/pkg/apis/enterprise/v1beta1/clustermaster_types.go b/pkg/apis/enterprise/v1beta1/clustermaster_types.go index 40821b254..663618f72 100644 --- a/pkg/apis/enterprise/v1beta1/clustermaster_types.go +++ b/pkg/apis/enterprise/v1beta1/clustermaster_types.go @@ -1,4 +1,4 @@ -// Copyright (c) 2018-2020 Splunk Inc. All rights reserved. +// Copyright (c) 2018-2021 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. diff --git a/pkg/apis/enterprise/v1beta1/common_types.go b/pkg/apis/enterprise/v1beta1/common_types.go index 37ddf74f8..90fcdc37c 100644 --- a/pkg/apis/enterprise/v1beta1/common_types.go +++ b/pkg/apis/enterprise/v1beta1/common_types.go @@ -1,4 +1,4 @@ -// Copyright (c) 2018-2020 Splunk Inc. All rights reserved. +// Copyright (c) 2018-2021 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. diff --git a/pkg/apis/enterprise/v1beta1/doc.go b/pkg/apis/enterprise/v1beta1/doc.go index b7d61104f..6a549274a 100644 --- a/pkg/apis/enterprise/v1beta1/doc.go +++ b/pkg/apis/enterprise/v1beta1/doc.go @@ -1,3 +1,17 @@ +// Copyright (c) 2018-2021 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 v1beta1 contains API Schema definitions for the enterprise v1beta1 API group // +k8s:deepcopy-gen=package,register // +groupName=enterprise.splunk.com diff --git a/pkg/apis/enterprise/v1beta1/indexercluster_types.go b/pkg/apis/enterprise/v1beta1/indexercluster_types.go index 702af40f8..f36b2372c 100644 --- a/pkg/apis/enterprise/v1beta1/indexercluster_types.go +++ b/pkg/apis/enterprise/v1beta1/indexercluster_types.go @@ -1,4 +1,4 @@ -// Copyright (c) 2018-2020 Splunk Inc. All rights reserved. +// Copyright (c) 2018-2021 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. diff --git a/pkg/apis/enterprise/v1beta1/licensemaster_types.go b/pkg/apis/enterprise/v1beta1/licensemaster_types.go index bcd731619..b9f48552a 100644 --- a/pkg/apis/enterprise/v1beta1/licensemaster_types.go +++ b/pkg/apis/enterprise/v1beta1/licensemaster_types.go @@ -1,4 +1,4 @@ -// Copyright (c) 2018-2020 Splunk Inc. All rights reserved. +// Copyright (c) 2018-2021 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. diff --git a/pkg/apis/enterprise/v1beta1/register.go b/pkg/apis/enterprise/v1beta1/register.go index 35db64917..b9c51b09f 100644 --- a/pkg/apis/enterprise/v1beta1/register.go +++ b/pkg/apis/enterprise/v1beta1/register.go @@ -1,3 +1,17 @@ +// Copyright (c) 2018-2021 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. + // NOTE: Boilerplate only. Ignore this file. // Package v1beta1 contains API Schema definitions for the enterprise v1beta1 API group diff --git a/pkg/apis/enterprise/v1beta1/searchheadcluster_types.go b/pkg/apis/enterprise/v1beta1/searchheadcluster_types.go index 04dd455f9..4b23291f3 100644 --- a/pkg/apis/enterprise/v1beta1/searchheadcluster_types.go +++ b/pkg/apis/enterprise/v1beta1/searchheadcluster_types.go @@ -1,4 +1,4 @@ -// Copyright (c) 2018-2020 Splunk Inc. All rights reserved. +// Copyright (c) 2018-2021 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. diff --git a/pkg/apis/enterprise/v1beta1/spark_types.go b/pkg/apis/enterprise/v1beta1/spark_types.go index 76065d914..c90ffe0e7 100644 --- a/pkg/apis/enterprise/v1beta1/spark_types.go +++ b/pkg/apis/enterprise/v1beta1/spark_types.go @@ -1,4 +1,4 @@ -// Copyright (c) 2018-2020 Splunk Inc. All rights reserved. +// Copyright (c) 2018-2021 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. diff --git a/pkg/apis/enterprise/v1beta1/standalone_types.go b/pkg/apis/enterprise/v1beta1/standalone_types.go index 07c65f87c..13802429e 100644 --- a/pkg/apis/enterprise/v1beta1/standalone_types.go +++ b/pkg/apis/enterprise/v1beta1/standalone_types.go @@ -1,4 +1,4 @@ -// Copyright (c) 2018-2020 Splunk Inc. All rights reserved. +// Copyright (c) 2018-2021 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. diff --git a/pkg/controller/add_clustermaster.go b/pkg/controller/add_clustermaster.go index cdb0e2009..b42a3e82b 100644 --- a/pkg/controller/add_clustermaster.go +++ b/pkg/controller/add_clustermaster.go @@ -1,3 +1,17 @@ +// Copyright (c) 2018-2021 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 ( diff --git a/pkg/controller/add_indexercluster.go b/pkg/controller/add_indexercluster.go index c5205eb8e..6bf6dbb84 100644 --- a/pkg/controller/add_indexercluster.go +++ b/pkg/controller/add_indexercluster.go @@ -1,4 +1,4 @@ -// Copyright (c) 2018-2020 Splunk Inc. All rights reserved. +// Copyright (c) 2018-2021 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. diff --git a/pkg/controller/add_licensemaster.go b/pkg/controller/add_licensemaster.go index 0ab731f89..73ba2b297 100644 --- a/pkg/controller/add_licensemaster.go +++ b/pkg/controller/add_licensemaster.go @@ -1,4 +1,4 @@ -// Copyright (c) 2018-2020 Splunk Inc. All rights reserved. +// Copyright (c) 2018-2021 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. diff --git a/pkg/controller/add_searchheadcluster.go b/pkg/controller/add_searchheadcluster.go index f3df62bc5..95928204c 100644 --- a/pkg/controller/add_searchheadcluster.go +++ b/pkg/controller/add_searchheadcluster.go @@ -1,4 +1,4 @@ -// Copyright (c) 2018-2020 Splunk Inc. All rights reserved. +// Copyright (c) 2018-2021 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. diff --git a/pkg/controller/add_spark.go b/pkg/controller/add_spark.go index c88687acf..e39368851 100644 --- a/pkg/controller/add_spark.go +++ b/pkg/controller/add_spark.go @@ -1,4 +1,4 @@ -// Copyright (c) 2018-2020 Splunk Inc. All rights reserved. +// Copyright (c) 2018-2021 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. diff --git a/pkg/controller/add_standalone.go b/pkg/controller/add_standalone.go index 978c71e8e..e0e3ee0aa 100644 --- a/pkg/controller/add_standalone.go +++ b/pkg/controller/add_standalone.go @@ -1,4 +1,4 @@ -// Copyright (c) 2018-2020 Splunk Inc. All rights reserved. +// Copyright (c) 2018-2021 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. diff --git a/pkg/controller/controller.go b/pkg/controller/controller.go index e7c69cde4..807c1544d 100644 --- a/pkg/controller/controller.go +++ b/pkg/controller/controller.go @@ -1,4 +1,4 @@ -// Copyright (c) 2018-2020 Splunk Inc. All rights reserved. +// Copyright (c) 2018-2021 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. diff --git a/pkg/splunk/client/doc.go b/pkg/splunk/client/doc.go index 2ae81ce56..00f2fefca 100644 --- a/pkg/splunk/client/doc.go +++ b/pkg/splunk/client/doc.go @@ -1,4 +1,4 @@ -// Copyright (c) 2018-2020 Splunk Inc. All rights reserved. +// Copyright (c) 2018-2021 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. diff --git a/pkg/splunk/client/enterprise.go b/pkg/splunk/client/enterprise.go index a61e5e277..8c0733561 100644 --- a/pkg/splunk/client/enterprise.go +++ b/pkg/splunk/client/enterprise.go @@ -1,4 +1,4 @@ -// Copyright (c) 2018-2020 Splunk Inc. All rights reserved. +// Copyright (c) 2018-2021 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. diff --git a/pkg/splunk/client/enterprise_test.go b/pkg/splunk/client/enterprise_test.go index 42903e887..f018c3019 100644 --- a/pkg/splunk/client/enterprise_test.go +++ b/pkg/splunk/client/enterprise_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2018-2020 Splunk Inc. All rights reserved. +// Copyright (c) 2018-2021 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. diff --git a/pkg/splunk/common/doc.go b/pkg/splunk/common/doc.go index 6f9d36353..92ab0e4a7 100644 --- a/pkg/splunk/common/doc.go +++ b/pkg/splunk/common/doc.go @@ -1,4 +1,4 @@ -// Copyright (c) 2018-2020 Splunk Inc. All rights reserved. +// Copyright (c) 2018-2021 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. diff --git a/pkg/splunk/common/messages.go b/pkg/splunk/common/messages.go index 39df3be7e..0654794d3 100644 --- a/pkg/splunk/common/messages.go +++ b/pkg/splunk/common/messages.go @@ -1,4 +1,4 @@ -// Copyright (c) 2018-2020 Splunk Inc. All rights reserved. +// Copyright (c) 2018-2021 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. diff --git a/pkg/splunk/common/names.go b/pkg/splunk/common/names.go index b5c789f7c..ad7618398 100644 --- a/pkg/splunk/common/names.go +++ b/pkg/splunk/common/names.go @@ -1,4 +1,4 @@ -// Copyright (c) 2018-2020 Splunk Inc. All rights reserved. +// Copyright (c) 2018-2021 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. diff --git a/pkg/splunk/common/names_test.go b/pkg/splunk/common/names_test.go index aa16bbc6e..9191ff82c 100644 --- a/pkg/splunk/common/names_test.go +++ b/pkg/splunk/common/names_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2018-2020 Splunk Inc. All rights reserved. +// Copyright (c) 2018-2021 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. diff --git a/pkg/splunk/common/types.go b/pkg/splunk/common/types.go index e5b3da09b..ea9144f42 100644 --- a/pkg/splunk/common/types.go +++ b/pkg/splunk/common/types.go @@ -1,4 +1,4 @@ -// Copyright (c) 2018-2020 Splunk Inc. All rights reserved. +// Copyright (c) 2018-2021 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. diff --git a/pkg/splunk/common/types_test.go b/pkg/splunk/common/types_test.go index 01b91f9a9..1137c28a0 100644 --- a/pkg/splunk/common/types_test.go +++ b/pkg/splunk/common/types_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2018-2020 Splunk Inc. All rights reserved. +// Copyright (c) 2018-2021 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. diff --git a/pkg/splunk/common/util.go b/pkg/splunk/common/util.go index c80863125..228cff8df 100644 --- a/pkg/splunk/common/util.go +++ b/pkg/splunk/common/util.go @@ -1,4 +1,4 @@ -// Copyright (c) 2018-2020 Splunk Inc. All rights reserved. +// Copyright (c) 2018-2021 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. diff --git a/pkg/splunk/common/util_test.go b/pkg/splunk/common/util_test.go index 7fff0ce2c..cccae112d 100644 --- a/pkg/splunk/common/util_test.go +++ b/pkg/splunk/common/util_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2018-2020 Splunk Inc. All rights reserved. +// Copyright (c) 2018-2021 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. diff --git a/pkg/splunk/controller/configmap.go b/pkg/splunk/controller/configmap.go index e6f48911e..2953d3e08 100644 --- a/pkg/splunk/controller/configmap.go +++ b/pkg/splunk/controller/configmap.go @@ -1,4 +1,4 @@ -// Copyright (c) 2018-2020 Splunk Inc. All rights reserved. +// Copyright (c) 2018-2021 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. diff --git a/pkg/splunk/controller/configmap_test.go b/pkg/splunk/controller/configmap_test.go index 3385ca5e0..df463732d 100644 --- a/pkg/splunk/controller/configmap_test.go +++ b/pkg/splunk/controller/configmap_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2018-2020 Splunk Inc. All rights reserved. +// Copyright (c) 2018-2021 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. diff --git a/pkg/splunk/controller/controller.go b/pkg/splunk/controller/controller.go index 2496a4be9..e065d5f35 100644 --- a/pkg/splunk/controller/controller.go +++ b/pkg/splunk/controller/controller.go @@ -1,4 +1,4 @@ -// Copyright (c) 2018-2020 Splunk Inc. All rights reserved. +// Copyright (c) 2018-2021 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. diff --git a/pkg/splunk/controller/controller_test.go b/pkg/splunk/controller/controller_test.go index dcc2c70c2..1b862870e 100644 --- a/pkg/splunk/controller/controller_test.go +++ b/pkg/splunk/controller/controller_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2018-2020 Splunk Inc. All rights reserved. +// Copyright (c) 2018-2021 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. diff --git a/pkg/splunk/controller/deployment.go b/pkg/splunk/controller/deployment.go index 294b0cbf9..6ca23c57c 100644 --- a/pkg/splunk/controller/deployment.go +++ b/pkg/splunk/controller/deployment.go @@ -1,4 +1,4 @@ -// Copyright (c) 2018-2020 Splunk Inc. All rights reserved. +// Copyright (c) 2018-2021 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. diff --git a/pkg/splunk/controller/deployment_test.go b/pkg/splunk/controller/deployment_test.go index a70e9f002..d21796c60 100644 --- a/pkg/splunk/controller/deployment_test.go +++ b/pkg/splunk/controller/deployment_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2018-2020 Splunk Inc. All rights reserved. +// Copyright (c) 2018-2021 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. diff --git a/pkg/splunk/controller/doc.go b/pkg/splunk/controller/doc.go index ab5b6b0aa..924bd7f82 100644 --- a/pkg/splunk/controller/doc.go +++ b/pkg/splunk/controller/doc.go @@ -1,4 +1,4 @@ -// Copyright (c) 2018-2020 Splunk Inc. All rights reserved. +// Copyright (c) 2018-2021 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. diff --git a/pkg/splunk/controller/finalizers.go b/pkg/splunk/controller/finalizers.go index 6cb8b2b02..6cf11d68d 100644 --- a/pkg/splunk/controller/finalizers.go +++ b/pkg/splunk/controller/finalizers.go @@ -1,4 +1,4 @@ -// Copyright (c) 2018-2020 Splunk Inc. All rights reserved. +// Copyright (c) 2018-2021 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. diff --git a/pkg/splunk/controller/finalizers_test.go b/pkg/splunk/controller/finalizers_test.go index cc9deb68c..158a7274a 100644 --- a/pkg/splunk/controller/finalizers_test.go +++ b/pkg/splunk/controller/finalizers_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2018-2020 Splunk Inc. All rights reserved. +// Copyright (c) 2018-2021 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. diff --git a/pkg/splunk/controller/secret.go b/pkg/splunk/controller/secret.go index 8f79a502b..e4c455704 100644 --- a/pkg/splunk/controller/secret.go +++ b/pkg/splunk/controller/secret.go @@ -1,4 +1,4 @@ -// Copyright (c) 2018-2020 Splunk Inc. All rights reserved. +// Copyright (c) 2018-2021 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. diff --git a/pkg/splunk/controller/secret_test.go b/pkg/splunk/controller/secret_test.go index 278b5f60d..4fdafcaae 100644 --- a/pkg/splunk/controller/secret_test.go +++ b/pkg/splunk/controller/secret_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2018-2020 Splunk Inc. All rights reserved. +// Copyright (c) 2018-2021 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. diff --git a/pkg/splunk/controller/service.go b/pkg/splunk/controller/service.go index 7aa881cc7..0bdd3e7fa 100644 --- a/pkg/splunk/controller/service.go +++ b/pkg/splunk/controller/service.go @@ -1,4 +1,4 @@ -// Copyright (c) 2018-2020 Splunk Inc. All rights reserved. +// Copyright (c) 2018-2021 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. diff --git a/pkg/splunk/controller/service_test.go b/pkg/splunk/controller/service_test.go index 663bfd945..cde648926 100644 --- a/pkg/splunk/controller/service_test.go +++ b/pkg/splunk/controller/service_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2018-2020 Splunk Inc. All rights reserved. +// Copyright (c) 2018-2021 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. diff --git a/pkg/splunk/controller/statefulset.go b/pkg/splunk/controller/statefulset.go index 9bdd5df17..fd7c93ccf 100644 --- a/pkg/splunk/controller/statefulset.go +++ b/pkg/splunk/controller/statefulset.go @@ -1,4 +1,4 @@ -// Copyright (c) 2018-2020 Splunk Inc. All rights reserved. +// Copyright (c) 2018-2021 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. diff --git a/pkg/splunk/controller/statefulset_test.go b/pkg/splunk/controller/statefulset_test.go index f97a80820..494cd00eb 100644 --- a/pkg/splunk/controller/statefulset_test.go +++ b/pkg/splunk/controller/statefulset_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2018-2020 Splunk Inc. All rights reserved. +// Copyright (c) 2018-2021 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. diff --git a/pkg/splunk/controller/util.go b/pkg/splunk/controller/util.go index 03595e94f..f31ac7364 100644 --- a/pkg/splunk/controller/util.go +++ b/pkg/splunk/controller/util.go @@ -1,4 +1,4 @@ -// Copyright (c) 2018-2020 Splunk Inc. All rights reserved. +// Copyright (c) 2018-2021 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. diff --git a/pkg/splunk/controller/util_test.go b/pkg/splunk/controller/util_test.go index a736818a3..0314f979a 100644 --- a/pkg/splunk/controller/util_test.go +++ b/pkg/splunk/controller/util_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2018-2020 Splunk Inc. All rights reserved. +// Copyright (c) 2018-2021 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. diff --git a/pkg/splunk/enterprise/clustermaster.go b/pkg/splunk/enterprise/clustermaster.go index 3bc5199c0..e370dd968 100644 --- a/pkg/splunk/enterprise/clustermaster.go +++ b/pkg/splunk/enterprise/clustermaster.go @@ -1,4 +1,4 @@ -// Copyright (c) 2018-2020 Splunk Inc. All rights reserved. +// Copyright (c) 2018-2021 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. diff --git a/pkg/splunk/enterprise/clustermaster_test.go b/pkg/splunk/enterprise/clustermaster_test.go index 4d9f862b1..dd7682c41 100644 --- a/pkg/splunk/enterprise/clustermaster_test.go +++ b/pkg/splunk/enterprise/clustermaster_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2018-2020 Splunk Inc. All rights reserved. +// Copyright (c) 2018-2021 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. diff --git a/pkg/splunk/enterprise/configuration.go b/pkg/splunk/enterprise/configuration.go index d18956869..f36473b62 100644 --- a/pkg/splunk/enterprise/configuration.go +++ b/pkg/splunk/enterprise/configuration.go @@ -1,4 +1,4 @@ -// Copyright (c) 2018-2020 Splunk Inc. All rights reserved. +// Copyright (c) 2018-2021 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. diff --git a/pkg/splunk/enterprise/configuration_test.go b/pkg/splunk/enterprise/configuration_test.go index 9b7f59da9..1952055db 100644 --- a/pkg/splunk/enterprise/configuration_test.go +++ b/pkg/splunk/enterprise/configuration_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2018-2020 Splunk Inc. All rights reserved. +// Copyright (c) 2018-2021 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. diff --git a/pkg/splunk/enterprise/doc.go b/pkg/splunk/enterprise/doc.go index c56851d69..76d98e122 100644 --- a/pkg/splunk/enterprise/doc.go +++ b/pkg/splunk/enterprise/doc.go @@ -1,4 +1,4 @@ -// Copyright (c) 2018-2020 Splunk Inc. All rights reserved. +// Copyright (c) 2018-2021 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. diff --git a/pkg/splunk/enterprise/finalizers.go b/pkg/splunk/enterprise/finalizers.go index 1034bb03f..0a755e7db 100644 --- a/pkg/splunk/enterprise/finalizers.go +++ b/pkg/splunk/enterprise/finalizers.go @@ -1,4 +1,4 @@ -// Copyright (c) 2018-2020 Splunk Inc. All rights reserved. +// Copyright (c) 2018-2021 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. diff --git a/pkg/splunk/enterprise/finalizers_test.go b/pkg/splunk/enterprise/finalizers_test.go index 08aff67ab..f4471c513 100644 --- a/pkg/splunk/enterprise/finalizers_test.go +++ b/pkg/splunk/enterprise/finalizers_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2018-2020 Splunk Inc. All rights reserved. +// Copyright (c) 2018-2021 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. diff --git a/pkg/splunk/enterprise/indexercluster.go b/pkg/splunk/enterprise/indexercluster.go index a975661c0..6068415fc 100644 --- a/pkg/splunk/enterprise/indexercluster.go +++ b/pkg/splunk/enterprise/indexercluster.go @@ -1,4 +1,4 @@ -// Copyright (c) 2018-2020 Splunk Inc. All rights reserved. +// Copyright (c) 2018-2021 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. diff --git a/pkg/splunk/enterprise/indexercluster_test.go b/pkg/splunk/enterprise/indexercluster_test.go index 83c5c4302..13c69f539 100644 --- a/pkg/splunk/enterprise/indexercluster_test.go +++ b/pkg/splunk/enterprise/indexercluster_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2018-2020 Splunk Inc. All rights reserved. +// Copyright (c) 2018-2021 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. diff --git a/pkg/splunk/enterprise/licensemaster.go b/pkg/splunk/enterprise/licensemaster.go index c9ef4931b..557d42ed0 100644 --- a/pkg/splunk/enterprise/licensemaster.go +++ b/pkg/splunk/enterprise/licensemaster.go @@ -1,4 +1,4 @@ -// Copyright (c) 2018-2020 Splunk Inc. All rights reserved. +// Copyright (c) 2018-2021 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. diff --git a/pkg/splunk/enterprise/licensemaster_test.go b/pkg/splunk/enterprise/licensemaster_test.go index 573025096..298f9aca9 100644 --- a/pkg/splunk/enterprise/licensemaster_test.go +++ b/pkg/splunk/enterprise/licensemaster_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2018-2020 Splunk Inc. All rights reserved. +// Copyright (c) 2018-2021 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. diff --git a/pkg/splunk/enterprise/monitoringconsole.go b/pkg/splunk/enterprise/monitoringconsole.go index cde444abd..97949c3fd 100644 --- a/pkg/splunk/enterprise/monitoringconsole.go +++ b/pkg/splunk/enterprise/monitoringconsole.go @@ -1,4 +1,4 @@ -// Copyright (c) 2018-2020 Splunk Inc. All rights reserved. +// Copyright (c) 2018-2021 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. diff --git a/pkg/splunk/enterprise/monitoringconsole_test.go b/pkg/splunk/enterprise/monitoringconsole_test.go index beebfa398..6fb4ff9ca 100644 --- a/pkg/splunk/enterprise/monitoringconsole_test.go +++ b/pkg/splunk/enterprise/monitoringconsole_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2018-2020 Splunk Inc. All rights reserved. +// Copyright (c) 2018-2021 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. diff --git a/pkg/splunk/enterprise/names.go b/pkg/splunk/enterprise/names.go index a60d862f4..e90eefc0c 100644 --- a/pkg/splunk/enterprise/names.go +++ b/pkg/splunk/enterprise/names.go @@ -1,4 +1,4 @@ -// Copyright (c) 2018-2020 Splunk Inc. All rights reserved. +// Copyright (c) 2018-2021 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. diff --git a/pkg/splunk/enterprise/names_test.go b/pkg/splunk/enterprise/names_test.go index 613570dcb..b8231bc24 100644 --- a/pkg/splunk/enterprise/names_test.go +++ b/pkg/splunk/enterprise/names_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2018-2020 Splunk Inc. All rights reserved. +// Copyright (c) 2018-2021 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. diff --git a/pkg/splunk/enterprise/searchheadcluster.go b/pkg/splunk/enterprise/searchheadcluster.go index 9cbca1408..e151fe805 100644 --- a/pkg/splunk/enterprise/searchheadcluster.go +++ b/pkg/splunk/enterprise/searchheadcluster.go @@ -1,4 +1,4 @@ -// Copyright (c) 2018-2020 Splunk Inc. All rights reserved. +// Copyright (c) 2018-2021 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. diff --git a/pkg/splunk/enterprise/searchheadcluster_test.go b/pkg/splunk/enterprise/searchheadcluster_test.go index 059f7ed0b..e80aaca70 100644 --- a/pkg/splunk/enterprise/searchheadcluster_test.go +++ b/pkg/splunk/enterprise/searchheadcluster_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2018-2020 Splunk Inc. All rights reserved. +// Copyright (c) 2018-2021 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. diff --git a/pkg/splunk/enterprise/standalone.go b/pkg/splunk/enterprise/standalone.go index 7eb91e2a7..cb33501e7 100644 --- a/pkg/splunk/enterprise/standalone.go +++ b/pkg/splunk/enterprise/standalone.go @@ -1,4 +1,4 @@ -// Copyright (c) 2018-2020 Splunk Inc. All rights reserved. +// Copyright (c) 2018-2021 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. diff --git a/pkg/splunk/enterprise/standalone_test.go b/pkg/splunk/enterprise/standalone_test.go index 41318223d..fd7346d30 100644 --- a/pkg/splunk/enterprise/standalone_test.go +++ b/pkg/splunk/enterprise/standalone_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2018-2020 Splunk Inc. All rights reserved. +// Copyright (c) 2018-2021 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. diff --git a/pkg/splunk/enterprise/types.go b/pkg/splunk/enterprise/types.go index 56710a412..3ba167f0f 100644 --- a/pkg/splunk/enterprise/types.go +++ b/pkg/splunk/enterprise/types.go @@ -1,4 +1,4 @@ -// Copyright (c) 2018-2020 Splunk Inc. All rights reserved. +// Copyright (c) 2018-2021 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. diff --git a/pkg/splunk/enterprise/util.go b/pkg/splunk/enterprise/util.go index 0ec317d9a..7ea7219f5 100644 --- a/pkg/splunk/enterprise/util.go +++ b/pkg/splunk/enterprise/util.go @@ -1,4 +1,4 @@ -// Copyright (c) 2018-2020 Splunk Inc. All rights reserved. +// Copyright (c) 2018-2021 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. diff --git a/pkg/splunk/enterprise/util_test.go b/pkg/splunk/enterprise/util_test.go index c72ef8a33..212e615cc 100644 --- a/pkg/splunk/enterprise/util_test.go +++ b/pkg/splunk/enterprise/util_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2018-2020 Splunk Inc. All rights reserved. +// Copyright (c) 2018-2021 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. diff --git a/pkg/splunk/spark/configuration.go b/pkg/splunk/spark/configuration.go index 5441ece0b..9331d64e4 100644 --- a/pkg/splunk/spark/configuration.go +++ b/pkg/splunk/spark/configuration.go @@ -1,4 +1,4 @@ -// Copyright (c) 2018-2020 Splunk Inc. All rights reserved. +// Copyright (c) 2018-2021 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. diff --git a/pkg/splunk/spark/configuration_test.go b/pkg/splunk/spark/configuration_test.go index e43116569..19b907643 100644 --- a/pkg/splunk/spark/configuration_test.go +++ b/pkg/splunk/spark/configuration_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2018-2020 Splunk Inc. All rights reserved. +// Copyright (c) 2018-2021 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. diff --git a/pkg/splunk/spark/doc.go b/pkg/splunk/spark/doc.go index 1094a33aa..b77468dde 100644 --- a/pkg/splunk/spark/doc.go +++ b/pkg/splunk/spark/doc.go @@ -1,4 +1,4 @@ -// Copyright (c) 2018-2020 Splunk Inc. All rights reserved. +// Copyright (c) 2018-2021 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. diff --git a/pkg/splunk/spark/names.go b/pkg/splunk/spark/names.go index 2bdcb974f..5e52f5e5a 100644 --- a/pkg/splunk/spark/names.go +++ b/pkg/splunk/spark/names.go @@ -1,4 +1,4 @@ -// Copyright (c) 2018-2020 Splunk Inc. All rights reserved. +// Copyright (c) 2018-2021 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. diff --git a/pkg/splunk/spark/names_test.go b/pkg/splunk/spark/names_test.go index 2c5a24b14..c555ac8c9 100644 --- a/pkg/splunk/spark/names_test.go +++ b/pkg/splunk/spark/names_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2018-2020 Splunk Inc. All rights reserved. +// Copyright (c) 2018-2021 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. diff --git a/pkg/splunk/spark/spark.go b/pkg/splunk/spark/spark.go index a3bed0cef..27c362cd5 100644 --- a/pkg/splunk/spark/spark.go +++ b/pkg/splunk/spark/spark.go @@ -1,4 +1,4 @@ -// Copyright (c) 2018-2020 Splunk Inc. All rights reserved. +// Copyright (c) 2018-2021 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. diff --git a/pkg/splunk/spark/spark_test.go b/pkg/splunk/spark/spark_test.go index 7a9fb8711..e7747ef87 100644 --- a/pkg/splunk/spark/spark_test.go +++ b/pkg/splunk/spark/spark_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2018-2020 Splunk Inc. All rights reserved. +// Copyright (c) 2018-2021 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. diff --git a/pkg/splunk/spark/types.go b/pkg/splunk/spark/types.go index dc401c54c..31c35d994 100644 --- a/pkg/splunk/spark/types.go +++ b/pkg/splunk/spark/types.go @@ -1,4 +1,4 @@ -// Copyright (c) 2018-2020 Splunk Inc. All rights reserved. +// Copyright (c) 2018-2021 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. diff --git a/pkg/splunk/test/client.go b/pkg/splunk/test/client.go index 796650492..03dbcfc22 100644 --- a/pkg/splunk/test/client.go +++ b/pkg/splunk/test/client.go @@ -1,4 +1,4 @@ -// Copyright (c) 2018-2020 Splunk Inc. All rights reserved. +// Copyright (c) 2018-2021 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. diff --git a/pkg/splunk/test/controller.go b/pkg/splunk/test/controller.go index 099177361..a5f7b5f4e 100644 --- a/pkg/splunk/test/controller.go +++ b/pkg/splunk/test/controller.go @@ -1,4 +1,4 @@ -// Copyright (c) 2018-2020 Splunk Inc. All rights reserved. +// Copyright (c) 2018-2021 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. diff --git a/pkg/splunk/test/doc.go b/pkg/splunk/test/doc.go index 50df9da49..efec30dbb 100644 --- a/pkg/splunk/test/doc.go +++ b/pkg/splunk/test/doc.go @@ -1,4 +1,4 @@ -// Copyright (c) 2018-2020 Splunk Inc. All rights reserved. +// Copyright (c) 2018-2021 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. diff --git a/pkg/splunk/util/messages.go b/pkg/splunk/util/messages.go index 74b9dd75d..3360e55c0 100644 --- a/pkg/splunk/util/messages.go +++ b/pkg/splunk/util/messages.go @@ -1,4 +1,4 @@ -// Copyright (c) 2018-2020 Splunk Inc. All rights reserved. +// Copyright (c) 2018-2021 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. diff --git a/pkg/splunk/util/secrets.go b/pkg/splunk/util/secrets.go index de9901f35..f032beb53 100644 --- a/pkg/splunk/util/secrets.go +++ b/pkg/splunk/util/secrets.go @@ -1,4 +1,4 @@ -// Copyright (c) 2018-2020 Splunk Inc. All rights reserved. +// Copyright (c) 2018-2021 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. diff --git a/pkg/splunk/util/secrets_test.go b/pkg/splunk/util/secrets_test.go index 2a4f57ea3..497a9ef3c 100644 --- a/pkg/splunk/util/secrets_test.go +++ b/pkg/splunk/util/secrets_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2018-2020 Splunk Inc. All rights reserved. +// Copyright (c) 2018-2021 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. diff --git a/pkg/splunk/util/util.go b/pkg/splunk/util/util.go index a52f50342..183ecf93d 100644 --- a/pkg/splunk/util/util.go +++ b/pkg/splunk/util/util.go @@ -1,3 +1,17 @@ +// Copyright (c) 2018-2021 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 util import ( diff --git a/pkg/splunk/util/util_test.go b/pkg/splunk/util/util_test.go index 74d31ea86..3d32ef11e 100644 --- a/pkg/splunk/util/util_test.go +++ b/pkg/splunk/util/util_test.go @@ -1,3 +1,17 @@ +// Copyright (c) 2018-2021 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 util import ( diff --git a/test/delete_cr/deletecr_suite_test.go b/test/delete_cr/deletecr_suite_test.go index 5db1d7596..2e45f2b5b 100644 --- a/test/delete_cr/deletecr_suite_test.go +++ b/test/delete_cr/deletecr_suite_test.go @@ -1,3 +1,17 @@ +// Copyright (c) 2018-2021 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 deletecr import ( diff --git a/test/delete_cr/deletecr_test.go b/test/delete_cr/deletecr_test.go index b3fe25733..87a5cf644 100644 --- a/test/delete_cr/deletecr_test.go +++ b/test/delete_cr/deletecr_test.go @@ -1,3 +1,17 @@ +// Copyright (c) 2018-2021 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 deletecr import ( diff --git a/test/example/example1_test.go b/test/example/example1_test.go index cd8772fb0..da2447a25 100644 --- a/test/example/example1_test.go +++ b/test/example/example1_test.go @@ -1,3 +1,16 @@ +// Copyright (c) 2018-2021 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 example import ( diff --git a/test/example/example2_test.go b/test/example/example2_test.go index dbf8615cb..f6a41380a 100644 --- a/test/example/example2_test.go +++ b/test/example/example2_test.go @@ -1,3 +1,16 @@ +// Copyright (c) 2018-2021 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 example import ( diff --git a/test/example/example_suite_test.go b/test/example/example_suite_test.go index 1b52773d2..2ac45f80a 100644 --- a/test/example/example_suite_test.go +++ b/test/example/example_suite_test.go @@ -1,3 +1,16 @@ +// Copyright (c) 2018-2021 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 example import ( diff --git a/test/ingest_search/ingest_search_suite_test.go b/test/ingest_search/ingest_search_suite_test.go index f1d9f8bf9..5ac50276f 100644 --- a/test/ingest_search/ingest_search_suite_test.go +++ b/test/ingest_search/ingest_search_suite_test.go @@ -1,3 +1,16 @@ +// Copyright (c) 2018-2021 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 ingestsearchtest import ( diff --git a/test/ingest_search/ingest_search_test.go b/test/ingest_search/ingest_search_test.go index 69994f9af..c6c1f57ee 100644 --- a/test/ingest_search/ingest_search_test.go +++ b/test/ingest_search/ingest_search_test.go @@ -1,3 +1,16 @@ +// Copyright (c) 2018-2021 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 ingestsearchtest import ( diff --git a/test/monitoring_console/monitoring_console_suite_test.go b/test/monitoring_console/monitoring_console_suite_test.go index a9530004f..38235b719 100644 --- a/test/monitoring_console/monitoring_console_suite_test.go +++ b/test/monitoring_console/monitoring_console_suite_test.go @@ -1,3 +1,17 @@ +// Copyright (c) 2018-2021 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 monitoringconsoletest import ( diff --git a/test/monitoring_console/monitoring_console_test.go b/test/monitoring_console/monitoring_console_test.go index 3b41aace9..b01f5d74b 100644 --- a/test/monitoring_console/monitoring_console_test.go +++ b/test/monitoring_console/monitoring_console_test.go @@ -1,3 +1,16 @@ +// Copyright (c) 2018-2021 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 monitoringconsoletest import ( diff --git a/test/smoke/cluster_master_sites_response.go b/test/smoke/cluster_master_sites_response.go index 301f19811..d3bbed45d 100644 --- a/test/smoke/cluster_master_sites_response.go +++ b/test/smoke/cluster_master_sites_response.go @@ -1,4 +1,4 @@ -// Copyright (c) 2018-2020 Splunk Inc. All rights reserved. +// Copyright (c) 2018-2021 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. diff --git a/test/smoke/smoke_suite_test.go b/test/smoke/smoke_suite_test.go index ca5bf1a72..ec90b96e4 100644 --- a/test/smoke/smoke_suite_test.go +++ b/test/smoke/smoke_suite_test.go @@ -1,3 +1,16 @@ +// Copyright (c) 2018-2021 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 smoke import ( diff --git a/test/smoke/smoke_test.go b/test/smoke/smoke_test.go index a71412026..15cf4ff07 100644 --- a/test/smoke/smoke_test.go +++ b/test/smoke/smoke_test.go @@ -1,3 +1,16 @@ +// Copyright (c) 2018-2021 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 smoke import ( diff --git a/test/testenv/cmutil.go b/test/testenv/cmutil.go index d848160b8..ab880255d 100644 --- a/test/testenv/cmutil.go +++ b/test/testenv/cmutil.go @@ -1,8 +1,23 @@ +// Copyright (c) 2018-2021 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 testenv import ( "encoding/json" "fmt" + logf "sigs.k8s.io/controller-runtime/pkg/log" ) diff --git a/test/testenv/deployment.go b/test/testenv/deployment.go index 0630b655d..f454116ad 100644 --- a/test/testenv/deployment.go +++ b/test/testenv/deployment.go @@ -1,3 +1,17 @@ +// Copyright (c) 2018-2021 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 testenv import ( diff --git a/test/testenv/ingest_utils.go b/test/testenv/ingest_utils.go index 5d7227b08..f215a9a4c 100644 --- a/test/testenv/ingest_utils.go +++ b/test/testenv/ingest_utils.go @@ -1,3 +1,17 @@ +// Copyright (c) 2018-2021 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 testenv import ( @@ -19,9 +33,12 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client/apiutil" "sigs.k8s.io/controller-runtime/pkg/client/config" logf "sigs.k8s.io/controller-runtime/pkg/log" + // Used to move files between pods - _ "k8s.io/kubernetes/pkg/kubectl/cmd/cp" _ "unsafe" + + // Import kubectl cmd cp utils + _ "k8s.io/kubernetes/pkg/kubectl/cmd/cp" ) // CreateMockLogfile creates a mock logfile with n entries to be ingested. diff --git a/test/testenv/lmutil.go b/test/testenv/lmutil.go index fbafc41a4..d2feaecf4 100644 --- a/test/testenv/lmutil.go +++ b/test/testenv/lmutil.go @@ -1,3 +1,17 @@ +// Copyright (c) 2018-2021 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 testenv import ( diff --git a/test/testenv/mcutil.go b/test/testenv/mcutil.go index 572e0dc60..614f4b038 100644 --- a/test/testenv/mcutil.go +++ b/test/testenv/mcutil.go @@ -1,3 +1,17 @@ +// Copyright (c) 2018-2021 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 testenv import ( diff --git a/test/testenv/search_head_cluster_utils.go b/test/testenv/search_head_cluster_utils.go index 55938b483..c213557e9 100644 --- a/test/testenv/search_head_cluster_utils.go +++ b/test/testenv/search_head_cluster_utils.go @@ -1,3 +1,17 @@ +// Copyright (c) 2018-2021 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 testenv import ( diff --git a/test/testenv/search_utils.go b/test/testenv/search_utils.go index 9fa3b887a..cebad00ec 100644 --- a/test/testenv/search_utils.go +++ b/test/testenv/search_utils.go @@ -1,3 +1,17 @@ +// Copyright (c) 2018-2021 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 testenv import ( diff --git a/test/testenv/testenv.go b/test/testenv/testenv.go index ba478c0b3..c7e81a543 100644 --- a/test/testenv/testenv.go +++ b/test/testenv/testenv.go @@ -1,3 +1,17 @@ +// Copyright (c) 2018-2021 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 testenv import ( diff --git a/test/testenv/util.go b/test/testenv/util.go index 637a1cf88..985db6644 100644 --- a/test/testenv/util.go +++ b/test/testenv/util.go @@ -1,3 +1,17 @@ +// Copyright (c) 2018-2021 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 testenv import ( diff --git a/test/testenv/verificationutils.go b/test/testenv/verificationutils.go index fc11b4b72..24fddcb9b 100644 --- a/test/testenv/verificationutils.go +++ b/test/testenv/verificationutils.go @@ -1,3 +1,17 @@ +// Copyright (c) 2018-2021 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 testenv import ( diff --git a/version/version.go b/version/version.go index 36ec83b1d..cd725763c 100644 --- a/version/version.go +++ b/version/version.go @@ -1,4 +1,4 @@ -// Copyright (c) 2018-2020 Splunk Inc. All rights reserved. +// Copyright (c) 2018-2021 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. From afba86e89a59cff213f63846952031dbcd41c36c Mon Sep 17 00:00:00 2001 From: akondur Date: Fri, 15 Jan 2021 13:57:11 -0800 Subject: [PATCH 29/43] Addressing review comments --- deploy/crds/enterprise.splunk.com_clustermasters_crd.yaml | 3 ++- deploy/crds/enterprise.splunk.com_indexerclusters_crd.yaml | 3 ++- deploy/crds/enterprise.splunk.com_licensemasters_crd.yaml | 3 ++- deploy/crds/enterprise.splunk.com_searchheadclusters_crd.yaml | 3 ++- deploy/crds/enterprise.splunk.com_standalones_crd.yaml | 3 ++- .../0.2.1/enterprise.splunk.com_clustermasters_crd.yaml | 3 ++- .../0.2.1/enterprise.splunk.com_indexerclusters_crd.yaml | 3 ++- .../0.2.1/enterprise.splunk.com_licensemasters_crd.yaml | 3 ++- .../0.2.1/enterprise.splunk.com_searchheadclusters_crd.yaml | 3 ++- .../splunk/0.2.1/enterprise.splunk.com_standalones_crd.yaml | 3 ++- pkg/apis/enterprise/v1beta1/common_types.go | 4 +++- pkg/splunk/controller/serviceaccount.go | 4 ++++ 12 files changed, 27 insertions(+), 11 deletions(-) diff --git a/deploy/crds/enterprise.splunk.com_clustermasters_crd.yaml b/deploy/crds/enterprise.splunk.com_clustermasters_crd.yaml index d67a2a231..c194ad2ed 100644 --- a/deploy/crds/enterprise.splunk.com_clustermasters_crd.yaml +++ b/deploy/crds/enterprise.splunk.com_clustermasters_crd.yaml @@ -754,7 +754,8 @@ spec: type: string serviceAccount: description: ServiceAccount is the service account used by the pods - deployed by the CRD + deployed by the CRD. If not specified uses the default serviceAccount + for the namespace as per https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/#use-the-default-service-account-to-access-the-api-server type: string serviceTemplate: description: ServiceTemplate is a template used to create Kubernetes diff --git a/deploy/crds/enterprise.splunk.com_indexerclusters_crd.yaml b/deploy/crds/enterprise.splunk.com_indexerclusters_crd.yaml index 39ef02106..d2e5189f4 100644 --- a/deploy/crds/enterprise.splunk.com_indexerclusters_crd.yaml +++ b/deploy/crds/enterprise.splunk.com_indexerclusters_crd.yaml @@ -781,7 +781,8 @@ spec: type: string serviceAccount: description: ServiceAccount is the service account used by the pods - deployed by the CRD + deployed by the CRD. If not specified uses the default serviceAccount + for the namespace as per https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/#use-the-default-service-account-to-access-the-api-server type: string serviceTemplate: description: ServiceTemplate is a template used to create Kubernetes diff --git a/deploy/crds/enterprise.splunk.com_licensemasters_crd.yaml b/deploy/crds/enterprise.splunk.com_licensemasters_crd.yaml index 8715c4afc..b2fe9ab56 100644 --- a/deploy/crds/enterprise.splunk.com_licensemasters_crd.yaml +++ b/deploy/crds/enterprise.splunk.com_licensemasters_crd.yaml @@ -759,7 +759,8 @@ spec: type: string serviceAccount: description: ServiceAccount is the service account used by the pods - deployed by the CRD + deployed by the CRD. If not specified uses the default serviceAccount + for the namespace as per https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/#use-the-default-service-account-to-access-the-api-server type: string serviceTemplate: description: ServiceTemplate is a template used to create Kubernetes diff --git a/deploy/crds/enterprise.splunk.com_searchheadclusters_crd.yaml b/deploy/crds/enterprise.splunk.com_searchheadclusters_crd.yaml index 6b6ce4925..11ffcea16 100644 --- a/deploy/crds/enterprise.splunk.com_searchheadclusters_crd.yaml +++ b/deploy/crds/enterprise.splunk.com_searchheadclusters_crd.yaml @@ -781,7 +781,8 @@ spec: type: string serviceAccount: description: ServiceAccount is the service account used by the pods - deployed by the CRD + deployed by the CRD. If not specified uses the default serviceAccount + for the namespace as per https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/#use-the-default-service-account-to-access-the-api-server type: string serviceTemplate: description: ServiceTemplate is a template used to create Kubernetes diff --git a/deploy/crds/enterprise.splunk.com_standalones_crd.yaml b/deploy/crds/enterprise.splunk.com_standalones_crd.yaml index 0c2273a5c..ec058bd40 100644 --- a/deploy/crds/enterprise.splunk.com_standalones_crd.yaml +++ b/deploy/crds/enterprise.splunk.com_standalones_crd.yaml @@ -775,7 +775,8 @@ spec: type: string serviceAccount: description: ServiceAccount is the service account used by the pods - deployed by the CRD + deployed by the CRD. If not specified uses the default serviceAccount + for the namespace as per https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/#use-the-default-service-account-to-access-the-api-server type: string serviceTemplate: description: ServiceTemplate is a template used to create Kubernetes diff --git a/deploy/olm-catalog/splunk/0.2.1/enterprise.splunk.com_clustermasters_crd.yaml b/deploy/olm-catalog/splunk/0.2.1/enterprise.splunk.com_clustermasters_crd.yaml index d67a2a231..c194ad2ed 100644 --- a/deploy/olm-catalog/splunk/0.2.1/enterprise.splunk.com_clustermasters_crd.yaml +++ b/deploy/olm-catalog/splunk/0.2.1/enterprise.splunk.com_clustermasters_crd.yaml @@ -754,7 +754,8 @@ spec: type: string serviceAccount: description: ServiceAccount is the service account used by the pods - deployed by the CRD + deployed by the CRD. If not specified uses the default serviceAccount + for the namespace as per https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/#use-the-default-service-account-to-access-the-api-server type: string serviceTemplate: description: ServiceTemplate is a template used to create Kubernetes diff --git a/deploy/olm-catalog/splunk/0.2.1/enterprise.splunk.com_indexerclusters_crd.yaml b/deploy/olm-catalog/splunk/0.2.1/enterprise.splunk.com_indexerclusters_crd.yaml index 39ef02106..d2e5189f4 100644 --- a/deploy/olm-catalog/splunk/0.2.1/enterprise.splunk.com_indexerclusters_crd.yaml +++ b/deploy/olm-catalog/splunk/0.2.1/enterprise.splunk.com_indexerclusters_crd.yaml @@ -781,7 +781,8 @@ spec: type: string serviceAccount: description: ServiceAccount is the service account used by the pods - deployed by the CRD + deployed by the CRD. If not specified uses the default serviceAccount + for the namespace as per https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/#use-the-default-service-account-to-access-the-api-server type: string serviceTemplate: description: ServiceTemplate is a template used to create Kubernetes diff --git a/deploy/olm-catalog/splunk/0.2.1/enterprise.splunk.com_licensemasters_crd.yaml b/deploy/olm-catalog/splunk/0.2.1/enterprise.splunk.com_licensemasters_crd.yaml index 8715c4afc..b2fe9ab56 100644 --- a/deploy/olm-catalog/splunk/0.2.1/enterprise.splunk.com_licensemasters_crd.yaml +++ b/deploy/olm-catalog/splunk/0.2.1/enterprise.splunk.com_licensemasters_crd.yaml @@ -759,7 +759,8 @@ spec: type: string serviceAccount: description: ServiceAccount is the service account used by the pods - deployed by the CRD + deployed by the CRD. If not specified uses the default serviceAccount + for the namespace as per https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/#use-the-default-service-account-to-access-the-api-server type: string serviceTemplate: description: ServiceTemplate is a template used to create Kubernetes diff --git a/deploy/olm-catalog/splunk/0.2.1/enterprise.splunk.com_searchheadclusters_crd.yaml b/deploy/olm-catalog/splunk/0.2.1/enterprise.splunk.com_searchheadclusters_crd.yaml index 6b6ce4925..11ffcea16 100644 --- a/deploy/olm-catalog/splunk/0.2.1/enterprise.splunk.com_searchheadclusters_crd.yaml +++ b/deploy/olm-catalog/splunk/0.2.1/enterprise.splunk.com_searchheadclusters_crd.yaml @@ -781,7 +781,8 @@ spec: type: string serviceAccount: description: ServiceAccount is the service account used by the pods - deployed by the CRD + deployed by the CRD. If not specified uses the default serviceAccount + for the namespace as per https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/#use-the-default-service-account-to-access-the-api-server type: string serviceTemplate: description: ServiceTemplate is a template used to create Kubernetes diff --git a/deploy/olm-catalog/splunk/0.2.1/enterprise.splunk.com_standalones_crd.yaml b/deploy/olm-catalog/splunk/0.2.1/enterprise.splunk.com_standalones_crd.yaml index 0c2273a5c..ec058bd40 100644 --- a/deploy/olm-catalog/splunk/0.2.1/enterprise.splunk.com_standalones_crd.yaml +++ b/deploy/olm-catalog/splunk/0.2.1/enterprise.splunk.com_standalones_crd.yaml @@ -775,7 +775,8 @@ spec: type: string serviceAccount: description: ServiceAccount is the service account used by the pods - deployed by the CRD + deployed by the CRD. If not specified uses the default serviceAccount + for the namespace as per https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/#use-the-default-service-account-to-access-the-api-server type: string serviceTemplate: description: ServiceTemplate is a template used to create Kubernetes diff --git a/pkg/apis/enterprise/v1beta1/common_types.go b/pkg/apis/enterprise/v1beta1/common_types.go index 5549b5d35..b82b4d69b 100644 --- a/pkg/apis/enterprise/v1beta1/common_types.go +++ b/pkg/apis/enterprise/v1beta1/common_types.go @@ -70,7 +70,9 @@ type CommonSplunkSpec struct { // Mock to differentiate between UTs and actual reconcile Mock bool `json:"Mock"` - // ServiceAccount is the service account used by the pods deployed by the CRD + // ServiceAccount is the service account used by the pods deployed by the CRD. + // If not specified uses the default serviceAccount for the namespace as per + // https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/#use-the-default-service-account-to-access-the-api-server ServiceAccount string `json:"serviceAccount"` } diff --git a/pkg/splunk/controller/serviceaccount.go b/pkg/splunk/controller/serviceaccount.go index 4bf11b968..e3790c08d 100644 --- a/pkg/splunk/controller/serviceaccount.go +++ b/pkg/splunk/controller/serviceaccount.go @@ -27,12 +27,16 @@ import ( // ApplyServiceAccount creates or updates a Kubernetes serviceAccount func ApplyServiceAccount(client splcommon.ControllerClient, serviceAccount *corev1.ServiceAccount) error { + scopedLog := log.WithName("ApplyServiceAccount").WithValues("serviceAccount", serviceAccount.GetName(), + "namespace", serviceAccount.GetNamespace()) + namespacedName := types.NamespacedName{Namespace: serviceAccount.GetNamespace(), Name: serviceAccount.GetName()} var current corev1.ServiceAccount err := client.Get(context.TODO(), namespacedName, ¤t) if err == nil { if !reflect.DeepEqual(serviceAccount, ¤t) { + scopedLog.Info("Updating service account") current = *serviceAccount err = splutil.UpdateResource(client, ¤t) } From af168cf323422e243f995cb21ee1af439f7b26a8 Mon Sep 17 00:00:00 2001 From: Param Dhanoya Date: Mon, 18 Jan 2021 14:53:57 -0800 Subject: [PATCH 30/43] Fixed MC statefulset JSON --- pkg/splunk/enterprise/monitoringconsole_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/splunk/enterprise/monitoringconsole_test.go b/pkg/splunk/enterprise/monitoringconsole_test.go index d34584716..df1c57f3d 100644 --- a/pkg/splunk/enterprise/monitoringconsole_test.go +++ b/pkg/splunk/enterprise/monitoringconsole_test.go @@ -248,5 +248,5 @@ func TestGetMonitoringConsoleStatefulSet(t *testing.T) { configTester(t, fmt.Sprintf("getMonitoringConsoleStatefulSet"), f, want) } - test(`{"kind":"StatefulSet","apiVersion":"apps/v1","metadata":{"name":"splunk-test-monitoring-console","namespace":"test","creationTimestamp":null},"spec":{"replicas":1,"selector":{"matchLabels":{"app.kubernetes.io/component":"monitoring-console","app.kubernetes.io/instance":"splunk-test-monitoring-console","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"monitoring-console","app.kubernetes.io/part-of":"splunk-test-monitoring-console"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app.kubernetes.io/component":"monitoring-console","app.kubernetes.io/instance":"splunk-test-monitoring-console","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"monitoring-console","app.kubernetes.io/part-of":"splunk-test-monitoring-console"},"annotations":{"monitoringConsoleConfigRev":"","traffic.sidecar.istio.io/excludeOutboundPorts":"8089,8191,9997,7777,9000,17000,17500,19000","traffic.sidecar.istio.io/includeInboundPorts":"8000,8088"}},"spec":{"volumes":[{"name":"mnt-splunk-etc","emptyDir":{}},{"name":"mnt-splunk-var","emptyDir":{}},{"name":"mnt-splunk-secrets","secret":{"secretName":"splunk-test-secret","defaultMode":420}}],"containers":[{"name":"splunk","image":"splunk/splunk","ports":[{"name":"splunkweb","containerPort":8000,"protocol":"TCP"},{"name":"hec","containerPort":8088,"protocol":"TCP"},{"name":"splunkd","containerPort":8089,"protocol":"TCP"},{"name":"s2s","containerPort":9997,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"splunk-test-monitoring-console"}}],"env":[{"name":"SPLUNK_HOME","value":"/opt/splunk"},{"name":"SPLUNK_START_ARGS","value":"--accept-license"},{"name":"SPLUNK_DEFAULTS_URL","value":"/mnt/splunk-secrets/default.yml"},{"name":"SPLUNK_HOME_OWNERSHIP_ENFORCEMENT","value":"false"},{"name":"SPLUNK_ROLE","value":"splunk_monitor"},{"name":"SPLUNK_DECLARATIVE_ADMIN_PASSWORD","value":"true"},{"name":"SPLUNK_CLUSTER_MASTER_URL","value":"splunk-stack1-cluster-master-service"}],"resources":{"limits":{"cpu":"4","memory":"8Gi"},"requests":{"cpu":"100m","memory":"512Mi"}},"volumeMounts":[{"name":"mnt-splunk-etc","mountPath":"/opt/splunk/etc"},{"name":"mnt-splunk-var","mountPath":"/opt/splunk/var"},{"name":"mnt-splunk-secrets","mountPath":"/mnt/splunk-secrets"}],"livenessProbe":{"exec":{"command":["/sbin/checkstate.sh"]},"initialDelaySeconds":300,"timeoutSeconds":30,"periodSeconds":30},"readinessProbe":{"exec":{"command":["/bin/grep","started","/opt/container_artifact/splunk-container.state"]},"initialDelaySeconds":10,"timeoutSeconds":5,"periodSeconds":5},"imagePullPolicy":"IfNotPresent"}],"securityContext":{"runAsUser":41812,"fsGroup":41812},"affinity":{"podAntiAffinity":{"preferredDuringSchedulingIgnoredDuringExecution":[{"weight":100,"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app.kubernetes.io/instance","operator":"In","values":["splunk-test-monitoring-console"]}]},"topologyKey":"kubernetes.io/hostname"}}]}},"schedulerName":"default-scheduler"}},"serviceName":"splunk-test-monitoring-console-headless","podManagementPolicy":"Parallel","updateStrategy":{"type":"OnDelete"}},"status":{"replicas":0}}`) + test(`{"kind":"StatefulSet","apiVersion":"apps/v1","metadata":{"name":"splunk-test-monitoring-console","namespace":"test","creationTimestamp":null},"spec":{"replicas":1,"selector":{"matchLabels":{"app.kubernetes.io/component":"monitoring-console","app.kubernetes.io/instance":"splunk-test-monitoring-console","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"monitoring-console","app.kubernetes.io/part-of":"splunk-test-monitoring-console"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app.kubernetes.io/component":"monitoring-console","app.kubernetes.io/instance":"splunk-test-monitoring-console","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"monitoring-console","app.kubernetes.io/part-of":"splunk-test-monitoring-console"},"annotations":{"monitoringConsoleConfigRev":"","traffic.sidecar.istio.io/excludeOutboundPorts":"8089,8191,9997,7777,9000,17000,17500,19000","traffic.sidecar.istio.io/includeInboundPorts":"8000,8088"}},"spec":{"volumes":[{"name":"mnt-splunk-etc","emptyDir":{}},{"name":"mnt-splunk-var","emptyDir":{}},{"name":"mnt-splunk-secrets","secret":{"secretName":"splunk-test-secret","defaultMode":420}}],"containers":[{"name":"splunk","image":"splunk/splunk","ports":[{"name":"http-splunkweb","containerPort":8000,"protocol":"TCP"},{"name":"http-hec","containerPort":8088,"protocol":"TCP"},{"name":"https-splunkd","containerPort":8089,"protocol":"TCP"},{"name":"tcp-s2s","containerPort":9997,"protocol":"TCP"}],"envFrom":[{"configMapRef":{"name":"splunk-test-monitoring-console"}}],"env":[{"name":"SPLUNK_HOME","value":"/opt/splunk"},{"name":"SPLUNK_START_ARGS","value":"--accept-license"},{"name":"SPLUNK_DEFAULTS_URL","value":"/mnt/splunk-secrets/default.yml"},{"name":"SPLUNK_HOME_OWNERSHIP_ENFORCEMENT","value":"false"},{"name":"SPLUNK_ROLE","value":"splunk_monitor"},{"name":"SPLUNK_DECLARATIVE_ADMIN_PASSWORD","value":"true"},{"name":"SPLUNK_CLUSTER_MASTER_URL","value":"splunk-stack1-cluster-master-service"}],"resources":{"limits":{"cpu":"4","memory":"8Gi"},"requests":{"cpu":"100m","memory":"512Mi"}},"volumeMounts":[{"name":"mnt-splunk-etc","mountPath":"/opt/splunk/etc"},{"name":"mnt-splunk-var","mountPath":"/opt/splunk/var"},{"name":"mnt-splunk-secrets","mountPath":"/mnt/splunk-secrets"}],"livenessProbe":{"exec":{"command":["/sbin/checkstate.sh"]},"initialDelaySeconds":300,"timeoutSeconds":30,"periodSeconds":30},"readinessProbe":{"exec":{"command":["/bin/grep","started","/opt/container_artifact/splunk-container.state"]},"initialDelaySeconds":10,"timeoutSeconds":5,"periodSeconds":5},"imagePullPolicy":"IfNotPresent"}],"securityContext":{"runAsUser":41812,"fsGroup":41812},"affinity":{"podAntiAffinity":{"preferredDuringSchedulingIgnoredDuringExecution":[{"weight":100,"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app.kubernetes.io/instance","operator":"In","values":["splunk-test-monitoring-console"]}]},"topologyKey":"kubernetes.io/hostname"}}]}},"schedulerName":"default-scheduler"}},"serviceName":"splunk-test-monitoring-console-headless","podManagementPolicy":"Parallel","updateStrategy":{"type":"OnDelete"}},"status":{"replicas":0}}`) } From f347b9013870a3e9869635d8be091e1450b85dcd Mon Sep 17 00:00:00 2001 From: akondur Date: Wed, 20 Jan 2021 13:34:44 -0800 Subject: [PATCH 31/43] Give access to the resource serviceaccount --- deploy/role.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/deploy/role.yaml b/deploy/role.yaml index 04fc10938..51dd9440e 100644 --- a/deploy/role.yaml +++ b/deploy/role.yaml @@ -14,6 +14,7 @@ rules: - secrets - pods - pods/exec + - serviceaccounts verbs: - create - delete From 9cc89e1c083089cc4bd9dd105eb37dfe2375d086 Mon Sep 17 00:00:00 2001 From: akondur Date: Wed, 20 Jan 2021 13:40:08 -0800 Subject: [PATCH 32/43] Changed logging for GetServiceAccount --- pkg/splunk/controller/serviceaccount.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/splunk/controller/serviceaccount.go b/pkg/splunk/controller/serviceaccount.go index e3790c08d..516605b87 100644 --- a/pkg/splunk/controller/serviceaccount.go +++ b/pkg/splunk/controller/serviceaccount.go @@ -49,11 +49,11 @@ func ApplyServiceAccount(client splcommon.ControllerClient, serviceAccount *core // GetServiceAccount gets the serviceAccount resource in a given namespace func GetServiceAccount(client splcommon.ControllerClient, namespacedName types.NamespacedName) (*corev1.ServiceAccount, error) { - scopedLog := log.WithName("GetServiceAccount").WithValues("serviceAccount", namespacedName.Name, - "namespace", namespacedName.Namespace) var serviceAccount corev1.ServiceAccount err := client.Get(context.TODO(), namespacedName, &serviceAccount) if err != nil { + scopedLog := log.WithName("GetServiceAccount").WithValues("serviceAccount", namespacedName.Name, + "namespace", namespacedName.Namespace, "error", err) scopedLog.Info("ServiceAccount not found") return nil, err } From f90fe3f8f93b25d61b943ed2fbad6f7ad64e90bd Mon Sep 17 00:00:00 2001 From: Param Dhanoya Date: Fri, 22 Jan 2021 14:07:59 -0800 Subject: [PATCH 33/43] CSPL-720: Added test for service account with standlaone --- test/smoke/smoke_test.go | 36 +++++++++++++++++++++++++++++++ test/testenv/deployment.go | 10 +++++++++ test/testenv/testenv.go | 35 ++++++++++++++++++++++++++++++ test/testenv/util.go | 20 ++++++++++++++++- test/testenv/verificationutils.go | 31 ++++++++++++++++++++++++++ 5 files changed, 131 insertions(+), 1 deletion(-) diff --git a/test/smoke/smoke_test.go b/test/smoke/smoke_test.go index 15cf4ff07..941096db5 100644 --- a/test/smoke/smoke_test.go +++ b/test/smoke/smoke_test.go @@ -22,6 +22,10 @@ import ( . "github.com/onsi/gomega" "github.com/splunk/splunk-operator/test/testenv" + + enterprisev1 "github.com/splunk/splunk-operator/pkg/apis/enterprise/v1beta1" + splcommon "github.com/splunk/splunk-operator/pkg/splunk/common" + corev1 "k8s.io/api/core/v1" ) func dumpGetPods(ns string) { @@ -167,4 +171,36 @@ var _ = Describe("Smoke test", func() { testenv.VerifyLMConfiguredOnPod(deployment, standalonePodName) }) }) + + Context("Standalone deployment (S1) with Service Account", func() { + It("smoke: can deploy a standalone instance attached to a service account", func() { + // Create Service Account + serviceAccountName := "smoke-service-account" + testenvInstance.CreateServiceAccount(serviceAccountName) + + standaloneSpec := enterprisev1.StandaloneSpec{ + CommonSplunkSpec: enterprisev1.CommonSplunkSpec{ + Spec: splcommon.Spec{ + ImagePullPolicy: "IfNotPresent", + }, + Volumes: []corev1.Volume{}, + ServiceAccount: serviceAccountName, + }, + } + + // Create standalone Deployment with License Master + standalone, err := deployment.DeployStandalonewithGivenSpec(deployment.GetName(), standaloneSpec) + Expect(err).To(Succeed(), "Unable to deploy standalone instance with LM") + + // Wait for Standalone to be in READY status + testenv.StandaloneReady(deployment, deployment.GetName(), standalone, testenvInstance) + + // Verify MC Pod is Ready + testenv.MCPodReady(testenvInstance.GetName(), deployment) + + // Verify serviceAccount is configured on Pod + standalonePodName := fmt.Sprintf(testenv.StandalonePod, deployment.GetName(), 0) + testenv.VerifyServiceAccountConfiguredOnPod(deployment, testenvInstance.GetName(), standalonePodName, serviceAccountName) + }) + }) }) diff --git a/test/testenv/deployment.go b/test/testenv/deployment.go index f454116ad..38fc92342 100644 --- a/test/testenv/deployment.go +++ b/test/testenv/deployment.go @@ -417,3 +417,13 @@ func (d *Deployment) DeployStandaloneWithLM(name string) (*enterprisev1.Standalo } return deployed.(*enterprisev1.Standalone), err } + +// DeployStandalonewithGivenSpec deploys a standalone with given spec +func (d *Deployment) DeployStandalonewithGivenSpec(name string, spec enterprisev1.StandaloneSpec) (*enterprisev1.Standalone, error) { + standalone := newStandaloneWithGivenSpec(name, d.testenv.namespace, spec) + deployed, err := d.deployCR(name, standalone) + if err != nil { + return nil, err + } + return deployed.(*enterprisev1.Standalone), err +} diff --git a/test/testenv/testenv.go b/test/testenv/testenv.go index c7e81a543..19178c298 100644 --- a/test/testenv/testenv.go +++ b/test/testenv/testenv.go @@ -493,6 +493,41 @@ func (testenv *TestEnv) createLicenseConfigMap() error { return nil } +// Create a service account config +func newServiceAccount(ns string, serviceAccountName string) *corev1.ServiceAccount { + + new := corev1.ServiceAccount{ + TypeMeta: metav1.TypeMeta{ + Kind: "ServiceAccount", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: serviceAccountName, + Namespace: ns, + }, + } + + return &new +} + +// CreateServiceAccount Create a service account with given name +func (testenv *TestEnv) CreateServiceAccount(name string) error { + serviceAccountConfig := newServiceAccount(testenv.namespace, name) + if err := testenv.GetKubeClient().Create(context.TODO(), serviceAccountConfig); err != nil { + testenv.Log.Error(err, "Unable to create service account") + return err + } + + testenv.pushCleanupFunc(func() error { + err := testenv.GetKubeClient().Delete(context.TODO(), serviceAccountConfig) + if err != nil { + testenv.Log.Error(err, "Unable to delete service account") + return err + } + return nil + }) + return nil +} + // NewDeployment creates a new deployment func (testenv *TestEnv) NewDeployment(name string) (*Deployment, error) { d := Deployment{ diff --git a/test/testenv/util.go b/test/testenv/util.go index 985db6644..c2d7b537f 100644 --- a/test/testenv/util.go +++ b/test/testenv/util.go @@ -86,6 +86,24 @@ func newStandalone(name, ns string) *enterprisev1.Standalone { return &new } +// newStandalone creates and initializes CR for Standalone Kind +func newStandaloneWithGivenSpec(name, ns string, spec enterprisev1.StandaloneSpec) *enterprisev1.Standalone { + + new := enterprisev1.Standalone{ + TypeMeta: metav1.TypeMeta{ + Kind: "Standalone", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: ns, + Finalizers: []string{"enterprise.splunk.com/delete-pvc"}, + }, + + Spec: spec, + } + return &new +} + func newLicenseMaster(name, ns, licenseConfigMapName string) *enterprisev1.LicenseMaster { new := enterprisev1.LicenseMaster{ TypeMeta: metav1.TypeMeta{ @@ -225,7 +243,7 @@ func newRole(name, ns string) *rbacv1.Role { Rules: []rbacv1.PolicyRule{ { APIGroups: []string{""}, - Resources: []string{"services", "endpoints", "persistentvolumeclaims", "configmaps", "secrets", "pods"}, + Resources: []string{"services", "endpoints", "persistentvolumeclaims", "configmaps", "secrets", "pods", "serviceaccounts", "pods/exec"}, Verbs: []string{"create", "delete", "deletecollection", "get", "list", "patch", "update", "watch"}, }, { diff --git a/test/testenv/verificationutils.go b/test/testenv/verificationutils.go index 24fddcb9b..9cf12ed36 100644 --- a/test/testenv/verificationutils.go +++ b/test/testenv/verificationutils.go @@ -17,13 +17,24 @@ package testenv import ( "encoding/json" "fmt" + "os/exec" + "strings" gomega "github.com/onsi/gomega" enterprisev1 "github.com/splunk/splunk-operator/pkg/apis/enterprise/v1beta1" splcommon "github.com/splunk/splunk-operator/pkg/splunk/common" + logf "sigs.k8s.io/controller-runtime/pkg/log" ) +// PodDetailsStruct captures output of kubectl get pods podname -o json +type PodDetailsStruct struct { + Spec struct { + ServiceAccount string `json:"serviceAccount"` + ServiceAccountName string `json:"serviceAccountName"` + } +} + // StandaloneReady verify Standlone is in ReadyStatus and does not flip-flop func StandaloneReady(deployment *Deployment, deploymentName string, standalone *enterprisev1.Standalone, testenvInstance *TestEnv) { gomega.Eventually(func() splcommon.Phase { @@ -223,3 +234,23 @@ func VerifyLMConfiguredOnPod(deployment *Deployment, podName string) { return lmConfigured }, deployment.GetTimeout(), PollInterval).Should(gomega.Equal(true)) } + +// VerifyServiceAccountConfiguredOnPod check if given service account is configured on given pod +func VerifyServiceAccountConfiguredOnPod(deployment *Deployment, ns string, podName string, serviceAccount string) { + gomega.Eventually(func() bool { + output, err := exec.Command("kubectl", "get", "pods", "-n", ns, podName, "-o", "json").Output() + if err != nil { + cmd := fmt.Sprintf("kubectl get pods -n %s %s -o json", ns, podName) + logf.Log.Error(err, "Failed to execute command", "command", cmd) + return false + } + restResponse := PodDetailsStruct{} + err = json.Unmarshal([]byte(output), &restResponse) + if err != nil { + logf.Log.Error(err, "Failed to parse cluster searchheads") + return false + } + logf.Log.Info("Service Account on Pod", "FOUND", restResponse.Spec.ServiceAccount, "EXPECTED", serviceAccount) + return strings.Contains(serviceAccount, restResponse.Spec.ServiceAccount) + }, deployment.GetTimeout(), PollInterval).Should(gomega.Equal(true)) +} From 2367845bce11029f86251b9c91f07660c091fb34 Mon Sep 17 00:00:00 2001 From: mgaldino Date: Wed, 13 Jan 2021 16:47:06 -0800 Subject: [PATCH 34/43] CSPL-526 - Define S2S Ingress Method Updates the Ingress documentation with scenarios for major Ingress Controllers. --- docs/Ingress.md | 998 +++++++++++++++++++--- docs/pictures/TLS-End-to-End.png | Bin 0 -> 120560 bytes docs/pictures/TLS-Gateway-Termination.png | Bin 0 -> 137801 bytes 3 files changed, 864 insertions(+), 134 deletions(-) create mode 100644 docs/pictures/TLS-End-to-End.png create mode 100644 docs/pictures/TLS-Gateway-Termination.png diff --git a/docs/Ingress.md b/docs/Ingress.md index 397d5228d..4a23a4ed7 100644 --- a/docs/Ingress.md +++ b/docs/Ingress.md @@ -1,52 +1,864 @@ -# Configuring Ingress +| Table of contents | +| ----------------------------------------------------- | +| [Configuring Ingress](#introduction) | +| [Istio](#Istio) | +| [Ingress-Nginx](#Ingress-Nginx) | +| [Nginx-Ingress-Controller](#Nginx-Ingress-Controller) | +| [Let's Encrypt](#letsencrypt) | + + + +# Configuring Ingress Using `port-forward` is great for testing, but you will ultimately want to make it easier to access your Splunk cluster outside of Kubernetes. A common -approach is using -[Kubernetes Ingress Controllers](https://kubernetes.io/docs/concepts/services-networking/ingress-controllers/). +approach is using [Kubernetes Ingress Controllers](https://kubernetes.io/docs/concepts/services-networking/ingress-controllers/). + +There are many Ingress Controllers available, with each having their own pros and cons. There are just as many ways to configure each of them, which also depends upon your specific infrastructure and organizational policies. Splunk Operator will automatically create and manage Kubernetes Services for all of the relevant components, and we expect these will provide for easy integration with most Ingress Controllers and configurations. + +``` +$ kubectl get services -o name +service/splunk-cluster-cluster-master-service +service/splunk-cluster-deployer-service +service/splunk-cluster-indexer-headless +service/splunk-cluster-indexer-service +service/splunk-cluster-license-master-service +service/splunk-cluster-search-head-headless +service/splunk-cluster-search-head-service +service/splunk-standalone-standalone-headless +service/splunk-standalone-standalone-service +``` + +We provide some examples below for configuring a few of the most popular Ingress controllers: [Istio](https://istio.io/) , [Nginx-inc](https://docs.nginx.com/nginx-ingress-controller/overview/) and [Ingress Nginx](https://kubernetes.github.io/ingress-nginx/user-guide/nginx-configuration). We hope these will serve as a useful starting point to configuring ingress in your environment. + +Before deploying an example, you will need to review the yaml and replace “example.com” with the domain name you would like to use and replace “example” in the service names with the name of your custom resource object. You will also need to point your DNS for all the desired hostnames to the IP addresses of your ingress load balancer. + + +#### Important Notes on using Splunk on Kubernetes + +#### Load Balance Requirements + +When configuring ingress for use with Splunk Forwarders, the configured ingress load balancer must resolve to two or more IPs. This is required so the auto load balancing capability of the forwarders is preserved. + +#### Splunk default network ports + +When creating a new Splunk instance on Kubernetes, the default network ports will be used for internal communication such as internal logs, replication, and others. Any change in how these ports are configured needs to be consistent across the cluster. + +For Ingress we recommend using separate ports for encrypted and non-encrypted traffic. In this documentation we will use port 9998 for encrypted data coming from outside the cluster, while keeping the default 9997 for non-encrypted intra-cluster communication. + +#### Indexer Discovery not supported +Indexer Discovery is not supported on a Kubernetes cluster. Instead, the Ingress controllers will be responsible to connect forwarders to peer nodes in Indexer clusters. + + +## Configuring Ingress Using Istio + +Istio as an ingress controller allows the cluster to receive requests from external sources and routes them to a desired destination within the cluster. Istio utilizes an Envoy proxy that allows for precise control over how data is routed to services by looking at attributes such as, hostname, uri, and HTTP headers. Through the use of destination rules, it also allows for fine grain control over how data is routed even within services themselves. + +For instructions on how to install and configure Istio for your specific infrastructure, see the Istio [getting started guide](https://istio.io/docs/setup/getting-started/). + +Most scenarios for Istio will require the configuration of a Gateway and a Virtual Service. Familiarize yourself with these concepts here [Istio Gateway](https://istio.io/latest/docs/reference/config/networking/gateway/) and [Istio Virtual Service](https://istio.io/latest/docs/reference/config/networking/virtual-service/). + +### Configuring Ingress for Splunk Web + +You can configure Istio to provide direct access to Splunk Web. + +#### Standalone Configuration + +
+Create a Gateway to receive traffic on port 80 + +

+ +```yaml +apiVersion: networking.istio.io/v1beta1 +kind: Gateway +metadata: + name: splunk-s2s +spec: + selector: + istio: ingressgateway # use istio default ingress gateway + servers: + - port: + number: 80 + name: UI + protocol: TCP + hosts: + - "splunk.example.com" +``` + +

+
+ +
+Create a virtual service to route traffic to your service, in this example we used a standalone. + +

+ +```yaml +apiVersion: networking.istio.io/v1beta1 +kind: VirtualService +metadata: + name: splunk-s2s +spec: + hosts: + - "splunk.example.com" + gateways: + - "splunk-s2s" + tcp: + - match: + - port: 80 + route: + - destination: + port: + number: 8000 + host: splunk-standalone-standalone-service +``` +

+
+ +Get the External-IP for Istio using the command: +```shell +kubectl get svc -n istio-system +``` + +On your browser, use the External-IP to access Splunk Web, for example: +``` +http:// +``` + +#### Multiple Hosts Configuration + +If your deployment has multiple hosts such as Search Heads and Cluster Master, use the following example as your Gateway and Virtual Service. + +
+Create Gateway for multiple hosts + +

+ +```yaml +apiVersion: networking.istio.io/v1beta1 +kind: Gateway +metadata: + name: splunk-gw +spec: + selector: + istio: ingressgateway # use istio default ingress gateway + servers: + - port: + number: 80 + name: http + protocol: HTTP + hosts: + - "splunk.example.com" + - "deployer.splunk.example.com" + - "cluster-master.splunk.example.com" + - "license-master.splunk.example.com" +``` + +

+
+ +Next, you will need to create VirtualServices for each of the components that you want to expose outside of Kubernetes: + +
+Create one virtual service for each component + +

+ +```yaml +apiVersion: networking.istio.io/v1beta1 +kind: VirtualService +metadata: + name: splunk +spec: + hosts: + - "splunk.example.com" + gateways: + - "splunk-gw" + http: + - match: + - uri: + prefix: "/services/collector" + route: + - destination: + port: + number: 8088 + host: splunk-example-indexer-service + - route: + - destination: + port: + number: 8000 + host: splunk-example-search-head-service +--- +apiVersion: networking.istio.io/v1beta1 +kind: VirtualService +metadata: + name: splunk-deployer +spec: + hosts: + - "deployer.splunk.example.com" + gateways: + - "splunk-gw" + http: + - route: + - destination: + port: + number: 8000 + host: splunk-example-deployer-service +--- +apiVersion: networking.istio.io/v1beta1 +kind: VirtualService +metadata: + name: splunk-cluster-master +spec: + hosts: + - "cluster-master.splunk.example.com" + gateways: + - "splunk-gw" + http: + - route: + - destination: + port: + number: 8000 + host: splunk-example-cluster-master-service +--- +apiVersion: networking.istio.io/v1beta1 +kind: VirtualService +metadata: + name: splunk-license-master +spec: + hosts: + - "license-master.splunk.example.com" + gateways: + - "splunk-gw" + http: + - route: + - destination: + port: + number: 8000 + host: splunk-example-license-master-service +``` +

+
+ +Finally, you will need to create a DestinationRule to ensure user sessions are +sticky to specific search heads: + +
+DestinationRule for the Search Head + +

+ +```yaml +apiVersion: networking.istio.io/v1beta1 +kind: DestinationRule +metadata: + name: splunk-search-head-rule +spec: + host: splunk-example-search-head-service + trafficPolicy: + loadBalancer: + consistentHash: + httpCookie: + name: SPLUNK_ISTIO_SESSION + ttl: 3600s +``` + +

+
+ + +### Configuring Ingress for Splunk Forwarder data +The pre-requisites for enabling inbound communications from Splunk Forwarders to the cluster are configuring the Istio Gateway and Istio Virtual Service: + +Example Gateway configuration +```yaml +apiVersion: networking.istio.io/v1beta1 +kind: Gateway +metadata: + name: splunk-s2s +spec: + selector: + istio: ingressgateway # use istio default ingress gateway + servers: + - port: + number: 9997 + name: tcp-s2s + protocol: TCP + hosts: + - "splunk.example.com" +``` + +Example Virtual Service Configuration +```yaml +apiVersion: networking.istio.io/v1beta1 +kind: VirtualService +metadata: + name: splunk-s2s +spec: + hosts: + - "splunk.example.com" + gateways: + - "splunk-s2s" + tcp: + - match: + - port: 9997 + route: + - destination: + port: + number: 9997 + host: splunk-example-indexer-service +``` + +Modify your `ingress-gateway` service to listen for inbound TCP connections on port 9997. + +```shell +$ kubectl patch -n istio-system service istio-ingressgateway --patch '{"spec":{"ports":[{"name":"splunk-s2s","port":9997,"protocol":"TCP"}]}}' +``` + +Use the External-IP from Istio in the Forwarder's outputs.conf. +```shell +kubectl get svc -n istio-system +``` + + +### Configuring Ingress for Splunk Forwarder data with TLS +It is highly recommended that you always use TLS encryption for your Splunk Enterprise endpoints. The following sections will cover the two main configurations supported by Istio. + +#### Splunk Forwarder data with end-to-end TLS +In this configuration Istio passes the encrypted traffic to Splunk Enterprise without any termination. Note that you need to configure the TLS certificates on the Forwarder as well as any Splunk Enterprise indexers, cluster peers, or standalone instances. + +End-to-End Configuration + +When using TLS for Ingress, we recommend you add an additional port for secure communication. By default, port 9997 will be assigned for non-encrypted traffic and you can use any other available port for secure communications. This example shows how to add port 9998 for a standalone instance. + +```yaml +apiVersion: enterprise.splunk.com/v1beta1 +kind: Standalone +metadata: + name: standalone + labels: + app: SplunkStandAlone + type: Splunk + finalizers: + - enterprise.splunk.com/delete-pvc +spec: + serviceTemplate: + spec: + ports: + - name: splunktest + port: 9998 + protocol: TCP + targetPort: 9998 +``` + +Modify your `ingress-gateway` Service to listen for S2S TCP +connections on the new port created (9998). +```shell +$ kubectl patch -n istio-system service istio-ingressgateway --patch '{"spec":{"ports":[{"name":"splunk-tls","port":9998,"protocol":"TCP"}]}}' +``` + +Create a Gateway with TLS Passthrough +```yaml +apiVersion: networking.istio.io/v1beta1 +kind: Gateway +metadata: + name: splunk-s2s +spec: + selector: + istio: ingressgateway # use istio default ingress gateway + servers: + - port: + number: 9998 + name: tls-s2s + protocol: TLS + tls: + mode: PASSTHROUGH + hosts: + - "*" +``` + +Create a Virtual Service for TLS routing +```yaml +apiVersion: networking.istio.io/v1beta1 +kind: VirtualService +metadata: + name: splunk-s2s +spec: + hosts: + - "*" + gateways: + - "splunk-s2s" + tls: + - match: + - port: 9998 + sniHosts: + - "splunk.example.com" + route: + - destination: + host: splunk-standalone-standalone-service + port: + number: 9998 +``` + +*Note*: this TLS example requires that `outputs.conf` on your forwarders includes the setting `tlsHostname = splunk.example.com`. Istio requires the TLS header to be defined so it to know which indexers to forward the traffic to. If this parameter is not defined, your forwarder connections will fail. + +If you only have one indexer cluster that you would like to use as the destination for all S2S traffic, you can optionally replace `splunk.example.com` in the above examples with the wildcard `*`. When you use this wildcard, you do not have to set the `tlsHostname` parameter in `outputs.conf` on your forwarders. + +Configure the Forwarder's outputs.conf and the Indexer's inputs.conf using the documentation [Configure Secure Forwarding](https://docs.splunk.com/Documentation/Splunk/latest/Security/Aboutsecuringdatafromforwarders) + +#### Splunk Forwarder data with TLS Gateway Termination + +In this configuration, Istio is terminating the encryption at the Gateway and forwarding the decrypted traffic to Splunk Enterprise. Note that in this case the Forwarder's outputs.conf should be configured for TLS, while the Indexer's input.conf should be configured to accept non-encrypted traffic. + +End-to-End Configuration -There are many Ingress Controllers available, each having their own pros and -cons. There are just as many ways to configure each of them, which depend -upon your specific infrastructure and organizational policies. Splunk -Operator will automatically create and manage Kubernetes Services for all -the relevant components, and we expect these will provide for easy integration -with most (if not all) Ingress Controllers and configurations. +Create a TLS secret with the certificates needed to decrypt traffic. These are the same commands used on your Indexer to terminate TLS. + +```shell +kubectl create -n istio-system secret tls s2s-tls --key= --cert= ``` -$ kubectl get services -o name -service/splunk-cluster-cluster-master-service -service/splunk-cluster-deployer-service -service/splunk-cluster-indexer-headless -service/splunk-cluster-indexer-service -service/splunk-cluster-license-master-service -service/splunk-cluster-search-head-headless -service/splunk-cluster-search-head-service + +Create a Gateway that terminates TLS +```yaml +apiVersion: networking.istio.io/v1beta1 +kind: Gateway +metadata: + name: splunk-s2s +spec: + selector: + istio: ingressgateway # use istio default ingress gateway + servers: + - port: + number: 9998 + name: tls-s2s + protocol: TLS + tls: + mode: SIMPLE + credentialName: s2s-tls # must be the same as secret + hosts: + - "*" +``` + +Create a Virtual Service for TCP routing. +```yaml +apiVersion: networking.istio.io/v1beta1 +kind: VirtualService +metadata: + name: splunk-s2s +spec: + hosts: + - "*" + gateways: + - splunk-s2s + tcp: + - match: + - port: 9998 + route: + - destination: + port: + number: 9998 + host: splunk-standalone-standalone-service +``` +Note that the Virtual Service no longer handles TLS since it has been terminated at the gateway. + +Configure your Forwarder and Indexer or Standalone certificates using the documentation: [Securing data from forwarders](https://docs.splunk.com/Documentation/Splunk/latest/Security/Aboutsecuringdatafromforwarders). + +##### Documentation tested on Istio v1.8 and Kubernetes v1.17 + +## Note on Service Mesh and Istio + +Istio is a popular choice for its Service Mesh capabilities. However, Service Mesh for Splunk instances are only supported on Istio v1.8 and above, along with Kubernetes v1.19 and above. At the time of this documentation, neither Amazon AWS nor Google Cloud have updated their stack to these versions. + +## Configuring Ingress Using NGINX + +**NOTE**: There are at least 3 flavors of the Nginx Ingress controller. + +- Kubernetes Ingress Nginx (open source) +- Nginx Ingress Open Source (F5's open source version) +- Nginx Ingress Plus (F5's paid version) + +[Nginx Comparison Chart](https://github.com/nginxinc/kubernetes-ingress/blob/master/docs/nginx-ingress-controllers.md). + +It is important to confirm which Nginx Ingress controller you intended to implement, as they have *very* different annotations and configurations. For these examples, we are using the Kubernetes Ingress Nginx (option 1) and the Nginx Ingress Open Source (option 2). + +## Configuring Ingress Using Kubernetes Ingress Nginx + +For instructions on how to install and configure the NGINX Ingress Controller, see the +[NGINX Ingress Controller GitHub repository](https://github.com/nginxinc/kubernetes-ingress/) and the [Installation Guide](https://kubernetes.github.io/ingress-nginx/deploy/). + +This Ingress Controller uses a ConfigMap to enable Ingress access to the cluster. Currently there is no support for TCP gateway termination. Requests for this feature are available in [Request 3087](https://github.com/kubernetes/ingress-nginx/issues/3087), and [Ticket 636](https://github.com/kubernetes/ingress-nginx/issues/636) but at this time only HTTPS is supported for gateway termination. + +For Splunk Forwarder communications over TCP, the only configuration available is End-to-End TLS termination. The details for creating and managing certificates, as well as the Forwarder and Indexer's configurations are the same as the example for Istio End-to-End TLS above. + +For all configurations below, we started with the standard yaml provided in the Installation Guide for AWS as a template: +[Ingress NGINX AWS Sample Deployment](https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v0.43.0/deploy/static/provider/aws/deploy.yaml). Then we will add or update those components based on each scenario. + +### Configuring Ingress for Splunk Web + +You can configure Nginx to provide direct access to Splunk Web. In the following example we use a standalone deployment to access Splunk Web. + +
+Create Ingress configuration + +

+ +```yaml +apiVersion: networking.k8s.io/v1beta1 +kind: Ingress +metadata: + name: ingress-standalone + annotations: + # use the shared ingress-nginx + kubernetes.io/ingress.class: "nginx" + nginx.ingress.kubernetes.io/default-backend: splunk-standalone-standalone-service +spec: + rules: + - host: splunk.example.com + http: + paths: + - path: / + backend: + serviceName: splunk-standalone-standalone-service + servicePort: 8000 +``` + +

+
+ +Likewise you can configure Splunk Web in multiple host configurations + +
+Create Ingress for multiple hosts + +

+ +```yaml +apiVersion: networking.k8s.io/v1beta1 +kind: Ingress +metadata: + name: ingress-standalone + annotations: + # use the shared ingress-nginx + kubernetes.io/ingress.class: "nginx" + nginx.ingress.kubernetes.io/default-backend: splunk-standalone-standalone-service +spec: + rules: + - host: splunk.example.com + http: + paths: + - path: / + backend: + serviceName: splunk-example-search-head-service + servicePort: 8000 + - path: /services/collector + backend: + serviceName: splunk-example-indexer-service + servicePort: 8088 + - host: deployer.splunk.example.com + http: + paths: + - backend: + serviceName: splunk-example-deployer-service + servicePort: 8000 + - host: cluster-master.splunk.example.com + http: + paths: + - backend: + serviceName: splunk-example-cluster-master-service + servicePort: 8000 +``` +

+
+ +### Configuring Ingress NGINX for Splunk Forwarders with End-to-End TLS + + +Note: In this example we used port 9997 for non-encrypted communication, and 9998 for encrypted. + +Update the default Ingress NGINX configuration to add the ConfigMap and Service ports: + +
+ Create a configMap to define the port-to-service routing + +

+ +```yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: tcp-services + namespace: ingress-nginx +data: + 9997: "default/splunk-standalone-standalone-service:9997" + 9998: "default/splunk-standalone-standalone-service:9998" +``` + +

+
+ +
+Add the two ports into the Service used to configure the Load Balancer + +

+ +```yaml +apiVersion: v1 +kind: Service +metadata: + annotations: + service.beta.kubernetes.io/aws-load-balancer-backend-protocol: tcp + service.beta.kubernetes.io/aws-load-balancer-cross-zone-load-balancing-enabled: 'true' + service.beta.kubernetes.io/aws-load-balancer-type: nlb + labels: + helm.sh/chart: ingress-nginx-3.10.1 + app.kubernetes.io/name: ingress-nginx + app.kubernetes.io/instance: ingress-nginx + app.kubernetes.io/version: 0.41.2 + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/component: controller + name: ingress-nginx-controller + namespace: ingress-nginx +spec: + type: LoadBalancer + externalTrafficPolicy: Local + ports: + - name: http + port: 80 + protocol: TCP + targetPort: http + - name: https + port: 443 + protocol: TCP + targetPort: https + - name: tcp-s2s + port: 9997 + protocol: TCP + targetPort: 9997 + - name: tls-s2s + port: 9998 + protocol: TCP + targetPort: 9998 +``` + +

+
+ + +##### Documentation tested on Ingress Nginx v1.19.4 and Kubernetes v1.17 + +## Configuring Ingress Using NGINX Ingress Controller (Nginxinc) + +The Nginx Ingress Controller is an open source version of the F5 product. Please review their documentation below for more details. + +[NGINX Ingress Controller Github Repo](https://github.com/nginxinc/kubernetes-ingress) + +[NGINX Ingress Controller Docs Home](https://docs.nginx.com/nginx-ingress-controller/overview/) + +[NGINX Ingress Controller Annotations Page](https://docs.nginx.com/nginx-ingress-controller/configuration/ingress-resources/advanced-configuration-with-annotations/) + + +### Install the Nginx Helm Chart +We followed the product's Helm Chart installation guide. It requires a cluster with internet access. +[NGINX Ingress Controller Helm Installation]( https://docs.nginx.com/nginx-ingress-controller/installation/installation-with-helm/) + +#### Setup Helm + +```shell +# clone the repo and check out the current production branch +$ git clone https://github.com/nginxinc/kubernetes-ingress/ +$ cd kubernetes-ingress/deployments/helm-chart +$ git checkout v1.9.0 + +# add the helm chart +$ helm repo add nginx-stable https://helm.nginx.com/stable +$ helm repo update + +# install custom resource definitions +$ kubectl create -f crds/ +``` + +#### Install Ingress +```shell +cd deployments/helm-chart + +# Edit and make changes to values.yaml as needed +helm install epat-eks-nginx nginx-stable/nginx-ingress + +#list the helms installed +helm list + +NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION +epat-eks-nginx default 5 2020-10-29 15:03:47.6 EDT deployed nginx-ingress-0.7.0 1.9.0 + +#if needed to update any configs for ingress, update the values.yaml and run upgrade +helm upgrade epat-eks-nginx nginx-stable/nginx-ingress +``` + +#### Configure Ingress services + +##### Configure Ingress for Splunk Web and HEC + +The following ingress example yaml configures Splunk Web as well as HEC as an operator installed service. HEC is exposed via ssl and Splunk Web is non-ssl. + +
+Create Ingress + +

+ +```yaml +apiVersion: extensions/v1beta1 +kind: Ingress +metadata: + annotations: + certmanager.k8s.io/cluster-issuer: letsencrypt-prod + nginx.org/client-body-buffer-size: 100M + nginx.org/client-max-body-size: "0" + nginx.org/server-snippets: | + client_body_buffer_size 100m; + nginx.org/ssl-services: splunk-standalone-standalone-headless + name: splunk-ingress + namespace: default +spec: + ingressClassName: nginx + rules: + - host: splunk.example.com + http: + paths: + - backend: + serviceName: splunk-standalone-standalone-service + servicePort: 8000 + path: /en-US + pathType: Prefix + - backend: + serviceName: splunk-standalone-standalone-headless + servicePort: 8088 + path: /services/collector + pathType: Prefix + - backend: + serviceName: splunk-standalone-standalone-headless + servicePort: 8089 + path: /.well-known + pathType: Prefix + tls: + - hosts: + - splunk.example.com + secretName: operator-tls +status: + loadBalancer: {} +``` + +

+
+ +##### Ingress Service for Splunk Forwarders +Enable the global configuration to setup a listener and transport server + +
+Create GlobalConfiguration + +

+ +```yaml +apiVersion: k8s.nginx.org/v1alpha1 +kind: GlobalConfiguration +metadata: + name: nginx-configuration + namespace: default +spec: + listeners: + - name: s2s-tcp + port: 30403 + protocol: TCP +apiVersion: k8s.nginx.org/v1alpha1 +kind: TransportServer +metadata: + name: s2s-tcp +spec: + listener: + name: s2s-tcp + protocol: TCP + upstreams: + - name: s2s-app + service: splunk-standalone-standalone-service + port: 9997 + action: + pass: s2s-app ``` +

+
-*Please note that services are currently only created for managed clusters. No -services will be created for single instance deployments.* +Edit the service to setup a node-port for the port being setup as the listener -Below we provide some examples for configuring two of the most popular Ingress controllers: the -[NGINX Ingress Controller](https://www.nginx.com/products/nginx/kubernetes-ingress-controller) -and [Istio](https://istio.io/). We hope these will serve as a useful starting -point to configuring ingress in your particular environment. +List the service +```yaml +kubectl get svc +NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE +epat-eks-nginx-nginx-ingress LoadBalancer 172.20.195.54 aa725344587a4443b97c614c6c78419c-1675645062.us-east-2.elb.amazonaws.com 80:31452/TCP,443:30402/TCP,30403:30403/TCP 7d1h +``` -Before deploying an example, you will need to replace “example.com” with -whatever domain name you would like to use, and “example” in the service -names with the name of your custom resource object. You will also need -to point your DNS for all the desired hostnames to the IP addresses of -your ingress load balancer. +Edit the service and add the Splunk Forwarder ingress port +``` +kubectl edit service epat-eks-nginx-nginx-ingress +``` +
+Sample Service + +

-## Example: Configuring Ingress Using NGINX +```yaml +apiVersion: v1 +kind: Service +metadata: + annotations: + meta.helm.sh/release-name: epat-eks-nginx + meta.helm.sh/release-namespace: default + creationTimestamp: "2020-10-23T17:05:08Z" + finalizers: + - service.kubernetes.io/load-balancer-cleanup + labels: + app.kubernetes.io/instance: epat-eks-nginx + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: epat-eks-nginx-nginx-ingress + helm.sh/chart: nginx-ingress-0.7.0 + name: epat-eks-nginx-nginx-ingress + namespace: default + resourceVersion: "3295579" + selfLink: /api/v1/namespaces/default/services/epat-eks-nginx-nginx-ingress + uid: a7253445-87a4-443b-97c6-14c6c78419c9 +spec: + clusterIP: 172.20.195.54 + externalTrafficPolicy: Local + healthCheckNodePort: 32739 + ports: + - name: http + nodePort: 31452 + port: 80 + protocol: TCP + targetPort: 80 + - name: https + nodePort: 30402 + port: 443 + protocol: TCP + targetPort: 443 + - name: s2s + nodePort: 30403 + port: 30403 + protocol: TCP + targetPort: 30403 + selector: + app: epat-eks-nginx-nginx-ingress + sessionAffinity: None + type: LoadBalancer +``` +

+
-For instructions on how to install and configure the NGINX Ingress Controller -for your specific infrastructure, please see its -[GitHub repository](https://github.com/nginxinc/kubernetes-ingress/). +##### Documentation tested on Nginx Ingress Controller v1.9.0 and Kubernetes v1.18 -It is highly recommended that you always use TLS encryption for your Splunk -endpoints. To do this, you will need to have one or more Kubernetes TLS -Secrets for all the hostnames you want to use with Splunk deployments. +## Using Let's Encrypt to manage TLS certificates If you are using [cert-manager](https://docs.cert-manager.io/en/latest/getting-started/) with [Let’s Encrypt](https://letsencrypt.org/) to manage your TLS @@ -54,6 +866,10 @@ certificates in Kubernetes, the following example Ingress object can be used to enable secure (TLS) access to all Splunk components from outside of your Kubernetes cluster: + +
Example configuration for NGINX +

+ ```yaml apiVersion: extensions/v1beta1 kind: Ingress @@ -133,18 +949,11 @@ tls: … ``` +

+
-## Example: Configuring Ingress Using Istio - -For instructions on how to install and configure Istio for your specific -infrastructure, please see its -[getting started guide](https://istio.io/docs/setup/getting-started/). - -It is highly recommended that you always use TLS encryption for your Splunk -endpoints. To do this, you will need to have one or more Kubernetes TLS -Secrets for all the hostnames you want to use with Splunk deployments. Note -that these secrets must reside in the same namespace as your Istio Ingress -pod, most likely `istio-system`. +
Example configuration for Istio +

If you are using [cert-manager](https://docs.cert-manager.io/en/latest/getting-started/) with [Let’s Encrypt](https://letsencrypt.org/) to manage your TLS certificates @@ -170,11 +979,12 @@ spec: kind: ClusterIssuer ``` + Next, you will need to create an Istio Gateway that is associated with your certificates: ```yaml -apiVersion: networking.istio.io/v1alpha3 +apiVersion: networking.istio.io/v1beta1 kind: Gateway metadata: name: splunk-gw @@ -237,7 +1047,7 @@ these by instead using multiple `port` objects in your Gateway: Next, you will need to create VirtualServices for each of the components that you want to expose outside of Kubernetes: ```yaml -apiVersion: networking.istio.io/v1alpha3 +apiVersion: networking.istio.io/v1beta1 kind: VirtualService metadata: name: splunk @@ -261,7 +1071,7 @@ spec: number: 8000 host: splunk-example-search-head-service --- -apiVersion: networking.istio.io/v1alpha3 +apiVersion: networking.istio.io/v1beta1 kind: VirtualService metadata: name: splunk-deployer @@ -277,7 +1087,7 @@ spec: number: 8000 host: splunk-example-deployer-service --- -apiVersion: networking.istio.io/v1alpha3 +apiVersion: networking.istio.io/v1beta1 kind: VirtualService metadata: name: splunk-cluster-master @@ -293,7 +1103,7 @@ spec: number: 8000 host: splunk-example-cluster-master-service --- -apiVersion: networking.istio.io/v1alpha3 +apiVersion: networking.istio.io/v1beta1 kind: VirtualService metadata: name: splunk-license-master @@ -314,7 +1124,7 @@ Finally, you will need to create a DestinationRule to ensure user sessions are sticky to specific search heads: ```yaml -apiVersion: networking.istio.io/v1alpha3 +apiVersion: networking.istio.io/v1beta1 kind: DestinationRule metadata: name: splunk-search-head-rule @@ -328,85 +1138,5 @@ spec: ttl: 3600s ``` -## Example: Using Istio for Splunk-to-Splunk (S2S) Traffic - -Istio can be used to route Splunk-to-Splunk (S2S) traffic directly to your indexers. - -First, you need to modify your `ingress-gateway` Service to listen for S2S TCP -connections on port 9997: - -``` -$ kubectl patch -n istio-system service istio-ingressgateway --patch '{"spec":{"ports":[{"name":"splunk-s2s","port":9997,"protocol":"TCP"}]}}' -``` - -The following example can be used to create a Gateway and VirtualService for -forwarding unencrypted S2S traffic: - -```yaml -apiVersion: networking.istio.io/v1alpha3 -kind: Gateway -metadata: - name: splunk-s2s -spec: - selector: - istio: ingressgateway # use istio default ingress gateway - servers: - - port: - number: 9997 - name: tcp-s2s - protocol: TCP - hosts: - - "splunk.example.com" ---- -apiVersion: networking.istio.io/v1alpha3 -kind: VirtualService -metadata: - name: splunk-s2s -spec: - hosts: - - "splunk.example.com" - gateways: - - "splunk-s2s" - tcp: - - match: - - port: 9997 - route: - - destination: - port: - number: 9997 - host: splunk-example-indexer-service -``` - -If you'd like to encrypt the S2S connections from your forwarders, you can use -Istio to terminate TLS and forward the traffic for you. Just modify your `Gateway` -to use `TLS` instead of `TCP`: - -```yaml -apiVersion: networking.istio.io/v1alpha3 -kind: Gateway -metadata: - name: splunk-s2s -spec: - selector: - istio: ingressgateway # use istio default ingress gateway - servers: - - port: - number: 9997 - name: tcp-s2s - protocol: TLS - tls: - mode: SIMPLE - credentialName: "splunk-example-com-tls" - hosts: - - "splunk.example.com" -``` - -*Please note*: this TLS example requires that `outputs.conf` on your forwarders -includes the parameter `tlsHostname = splunk.example.com`. Istio requires this -TLS header to be defined for it to know which indexers to forward the traffic -to. If this parameter is not defined, your forwarder connections will fail. - -If you only have one indexer cluster that you would like to use for all S2S -traffic, you can optionally replace `splunk.example.com` in the above examples -with the wildcard `*`. When you use this wildcard, you do not have to set the -`tlsHostname` parameter in `outputs.conf` on your forwarders. +

+
diff --git a/docs/pictures/TLS-End-to-End.png b/docs/pictures/TLS-End-to-End.png new file mode 100644 index 0000000000000000000000000000000000000000..bd182fc964c146c319040506c8f8cfbbdd685877 GIT binary patch literal 120560 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 AM*si- literal 0 HcmV?d00001 diff --git a/docs/pictures/TLS-Gateway-Termination.png b/docs/pictures/TLS-Gateway-Termination.png new file mode 100644 index 0000000000000000000000000000000000000000..6d892c9e65d137f09f6f0afc8dc0cd76135aecb2 GIT binary patch literal 137801 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�nFd`z=_MJwndEw5>up{{!LKyb*<_n)dO58 zr2nlk=aF?lN80M^R{G||7b-%$(aG4TsB6Sy==n{|C#tgBXsWB_R6b1B=i&d<%z+$` zEI)x>zh%?c4RHa`qb&f0oF_mL2JWGEMF$eByt*_3(I9c9s|um#=a;?}o`t0_V!9_Q z5g!3Hmd|6$8~18H##umG#7kfQt=m8Xn~~aMYR56$B5|7RQr0%m=a6FM=*Pk?dMEjV zQnoHS1t4G72hN`<=XBs=!f@5_I=yoA`i4200r;Y3RSEksQ_h$`A2%s=!|nn^Ke!D%QAzV?eBfiLt7na*stsG5 z4|6F88P?~~VpzTC>G4r1D(LEN$F{1;XEBt@K!-60IK@}=H0b(s)fg_fIf~?MjEh{4 z(0U>~5~Ylg$JH~KQAu8wa@=rqcfEFXhkd4GOH9QppZ_%;saC+<>!-DQ4t_l&Y=D7N z3nOYfUqvv~eg-JpR`vDHS(}*wj*5U=nFbF$x^}jR2YqRs1y}Gz`F^BA_yB*&T=iVQ zgraW4!&p&#ZzB`*!ju}-6J#Q#%G1ZC$nKy{i+>|NKC%F~cHIFfmSvi5-=&I&-k|3r zTM@KoweBhC4Fp)6D)GRD`_TT1bLgnsIoDpfU<+3>9_fA-)M=%_`&GOqJHxx8z~1$C zaz9j=IdM9-2+}yV?{p#04%dZscjY}Z)qg4JCMgh|Vy+dWcN)}For`LHtfhAwSCmZq z9{Fg>H6lzf+KD*6XFHF_`&ttT{Ouroe|&!iLc3o*tQc+m8w1zZ4G9M>zj1u= zHxLpBo5BI5@F~Cu=%vN$@sCbP?eb7RAE~odGPCQpFht-n z=uK3gU%96j(3qmCgko{50bS`@=LCTG?%Zznv+yaN%FUDo4O{B9rN#OBxC0_MPuyW1Gy;2~ zDXXaq11bdG_2cfAf|8=}`yF`?Wf!D; z!H8Gg<(4W)9zlkJ5(Q#EX$XGtO3Q)HBx4G`ZY|^q7OiHO_}}K2)rQ?q%bLKG5 z_xV20XT9(HBS{ecjQw7b{AX*w|K$Jg{Uz%t*0!koexqkU6l@M2F1Z3UO6yd84YX7b z!mcAZ8OR8ag$}EagIjZMceL4G$GQ@%gY29><+n;quUQ*`8CvcoV0=2$6l@gCDRcu8 z74CO7d^cvJPS$&TIQ_VEq0Il#2?l3_TZ!)ENs6#Z0AD<;?FPW{_dRLHfHK`U(Ep!S z5S|)Ji}!4_PO;E?w-nqbqM_OLLd%earflNv3mPFkJ99Uw8h z%t4bi2Xst61E94P#y7)SS0n^zPEQL{@Lm8k-73Eovr0hnGHDXy?%eo1EIc(Y5B&nRl|3o<>#o;3rMYSoaOSt@`nMs6_ZY+ZsZ>hsZ2aaOEKcuMmTx-zWE)rQJ6E| zv>!8MYR!>^>*0z&7SN)jhz z{)ya0w)jJ&cNTTe`94Ex)WWnvr{7O!e3HN?l+xX+H8WDbQzkEp3c0GjrFXC|G_d;O zh0uhLK$hhYq;7Ea(-~Tl#+^iBlC6G@=1EioQbiM&8SHPn5l%=~=&H zB=3;F=<)P(0A`@YvY2-9g7)%ftYy3h*B}EG90X+Eumo zyK35z^i1zzYSZ(QV)>aSu!^hoWZd2$@#`z~^1XDDpMp5}m*RFjk}Qeb4?CY++IM%Q zxw_PZ=Y8n3mH7k^EuQ>}YEOvc%h*7GM_w7ZDDau7nzl}~FFR40Lyn=#`kKj_5rAL` zXl^~-I1>EG)te`_z=wFD?nTIyC9_`e#yf6RqPGIIKplXmB#v`8)+&joV(e@pH6;Ut zKf3REU^fI0Yu=44Z4v)IHGr(n8^zvbwIF`?{Q(&U zivih{tH?4C*VK&0NR!&(_;L%Nh#L_53 zrGZOtxXvL7ht7$+0m1Z!ikH)eN{@%w0lLV?Exc$$?W-x6LElC5XK=c`Mee4h5GuGt zH{j$^CmHJ1%(zUgUYL`^1S$|!k(_mwvIOGCMv6|yYj?y6pGZvVek{_1?b)VxhJgrW z%$L_wZWEWSJI<2%n9!mfH^sgmvU~r|F~UH_Qq;a-SLS)^<|*2PE>9WJJ)@!$m+XOx zg%eQPOqg|KqJkg#NY?%I_CwYp<+T76|LOXo=TVt9_2Evk=5IQE_Iu3&*zpk{TzXO` zgX9SqO$laz@v!0;F^71~Cwf`2j_(foN_1(l?TZF;n=ua9cw!Vf&5j(a zqMC)c(*HH9TayjSi@UPqHsw{0kq5srk;g~1c7r%zB-F)9)kTU<3NhM%ZpJ;uFn z*lB&=_EgK~XECV1ZKSLZFa*TQw{|nG1nR;w?x6|8%v5ley!7!QeQRORngVj(m27iS zi`Qwn zcuG6VL{c#k2AVSlQh)S*)*$^Yq&EDXQxa4X8ETOMg@Bq3jS3A^kgB_>`7$`wme3CR z?RaC4(TF?vcg`=0<=T`tn@qRwVx-?^h~lTn`idczvOJ$|_0Thg>2wNli=47lSq*fpxREm8j<78IXKumBn(-%pdnAF zQA3`#VPMO#ww%5ESV!hCeu}2^ONICUHmHd9^Sl6^7_;KoOB1zr{h- zZD7L(q^XStjnH(%>qNp;CAQ~>7-DA4yE8HziQcNbQ<%9JqH&){qjvYTKUL8G)znT* zI7mM0dtZyY15xt@!3&3cKmc?BT+!*#7)_3fLe1g+TMBFu-MD99@-5Rl0b-KDLVBVK zE^B*)edj~pcdBv;7!={%Z*me4pXWf5{Q z5eKGA6cavOil^wL1xN3>5_0{^85t?~tt1_HDII~skh-^O8+PUD7|cBw>84!{aARSj zoq0|}D7%aoK2tSU)hlR;>cr0ZTrgrfW8tt8Uh`qvF(t0hi)K~^#Rq8w&#NKSbUlr-{S)-q0!4dKpaog+bq zjSL~FsH2lH5?IP7k6!^|?B`8$7$nhO$f~YKkMVeKlGEhkL0>O7AW*VCE*s2&caBA+ zlV^7>n6bS=L7sP>CMak4bj6$WrE3$l#}6c&LvImKB#EdM z?n3IE8*NanBP{TTD*K{g>YU!yBQ+)BOfuu`#Woo$EuF<~^*6X!QVnz(-`&IKxH;SC z_x+F*{Kq&cxH0KLULMvb4xeow>U2%>jSWqZ0r}S&4s#jnFap$bFj(=iXXksq1@jNS9V}L6%J5AIDD} zgkdw0;0m9w&S6zed!hXi3>XgZi7x5 z+N(R*LBrld7jj=mWIuFH(ff=05DT|xsQfq8Uj00NV%t+jdVFm^yeKH8 zx)xalx5Ew8+#NNmm+nzgFg&5BJuwk)s9khVdm36k-Z$6;=GcGEY4uXojOoH>p#50q zS};>hV8!NX&+q-I>z_{U3-xc7vXJw@Q%aP!x3!!KG}TG=qM`W_R@E^yf@o?4Px0h_ z&Fb!o1iOeqgosTnTF#3Onl%9^p(zeN(}rsI-WUfu0Wj9wWUH6D0 zze1Orn;X`i9JEvpC=RjK1|uUQO~!_cO(6`t=BD5zzQ6_BVGFyq_pjolK9?wQMeaCdpg7>TKMa#6)MI#Pr7m955lL~r8?b5CL3Tja_BwIt* z8Sgr2!#GR72P^&kwnLWS7dFHnjH#q0*ze5DXDYxUY9A)z$2aVG56ueoMmZd?&M9cS z)0ZP&g`Ov_6wK<@-!4G&sIN+Rsc(Gi=aGIkFSt=AagJ4qWS~20&WG7>v)-^btv1xw ztR_Lmh5pdLdFTh?rtQm4KWHR0Jh%7C8WJZev>7CKe?yBgT~F0Ia4;mvY82BG_;X+F zN9;8O|Em@$qZGUQnXJ@-6dlnJC!p!=IGa+jQaw@t1 zU=AtyZtCf!gMdQW)!;*agR&y?SbN&od9{u2On8alT)I{AAo;a|UW;2AJVdT+`O{$YRq{!Vg2*3Ji> zqkp6c_eaeCf1JmuBa9$^&As@5e)oj^*KbcY1kX{9jrt=#>Zkj0>H@g8ckYaF{V(?A zS_ya#XSjem8I$<$4Dw(9Dys*Y)phThRsZYCc0YJx51wNJVj55TukHG2WxKE0KNmFd zgM*@D{wLo_UeS?$@Eq+@)sk%g)8+j|aR*>K4pR$>{V5>+>Dy10gR_hD^7fGUUu^XM zKY)LJf&Z@_K-ug5NY}?pX!?YKClV7`)Hq@Qxv~x*J)%p9&ui* z<|fAV>*U~)j!jo%m>S+pZY9i%ojzA2RMasP{!IHZqjjT-a)&^h)OE{*&nBt&6pB0? zsy)~`i9#}$2Ah`G1z*Yk;{*q)X*PI_EUO$pg!mOpmNBXtJO)g3FxdkY~q}HorBly&N<@tzI7%4@_S!3FwQAQ(E)$3c){XWRn zm)`YOaRRjO0ppgiBPCG={E)6Tulh-sj`IbCROcL|qa-{+@T5b5BIdRN2%*S4dC zYiH!r{o#7P1xVZPvD2JB-COh4b{)I%Z4Td=@a zQ>Jfq#2T;-Xft`Vex0DPSu-JQ5L#7%;5sK^R$QkNw6O|K-xl7`hD{7$UfBia{&89V z`RQ6Y@F6MiQ(L>*x~D=ZP^IlEX$Gi(WDUC(Xu3A^+cK#!%+qt&^W3AuX1b-?A`}+u z@?eWIkByk&t5hGveUj9Er9O+wlj?(>{)(RUQjHk7qUvXvp<~}BG;JTLjni1~2i*;+ zwV%eO{KsM4)CXIgf6(iPYf21wd+U{W_iPC#zRe@D!eM^3NBIWJ`&8P>1~d1rrDnYDv*8(~2+L+! zxRSiq<+>#gO@$e}awmR-B|+>iIk!A9;qtak>R%5shsqc|9L(g{?O+>? zNQs|CU@ZF%Zw-Y)TD^)htTtglB^=f;_i>+1k=DqN z7?Vgf<29B{#7GpY;2Q0`BH}7cbNs6H=Q9b(m30bNyO;v7&u?}7Z2CgU5Bk@f@zqeM%AHGf3y2{D@i2|8Pc-` zR^Ef*EWR3%;cxN^{70tPHkzH$(}nLt@A$L^C@<*c$f=I}ZYpK9Ku{!KfAQOq-Onib zZ|_xrvW>57Jzj23&M9P6b1H*FiL>c2U=R^WWk&|0m>FcVZa1^yAwE92XSxwk0f6@2@aTOx&Wtn!OaA zTWVl5D}QOh!-ie4&8ameh}($q3;B;+q8F}ST;~ENMJAJt)BP~7nAjh3_cH*UD|qH{ znA$Gb;7F-#^g*U)N{!9Vu{7+LfZgG#&D6E(j=aFnOTBrArIz7f!}*33_%pH5DI5*7 z(9tY=z_dUcoHF4LIG|^C04nwe=-?S(X$6ympSA8LKu0Pc(qzUPev6=GiU2lXsLv^< zkinzYKF!r0DWncd+sEn(^If5hhzv@Ut95M%?5?kGuu6~Cak-hMvbC6%N?Phu*0utP z$p@g4b(&cAH?sKWV|fA`%NK%b%ex3le+lXyk&{@d<#N|(5ein%iqY6T2vHz&QB$C@ zT3L2Yfkj-)#tI&?>Trxbt*?QmQ`Kz$M0PjhR9izv5-_X5kUewo|5&sCTt9bHV9hto zuF>tf%>z%t9aLnl>p1XV6A#%Z>JC!lb_|DRnJFbaLiNwNxeq|d&%=*O z?ZUuLodpke`p#ixj{lxKoCc@nu;BTYAJ;+~{DGWsl~FKEUPtuK-;!cKZPvqwt6)`C zGV;s2n0^1}wWZN>gLa1@fXDa zsJ<<4Gq=l;!}=?%dxrnk0@y=&m7Gc*1M_gM{@0B1$X$@N90}|=xoc)j08e{)>VwuC zQ?1!O`jvlw(~rIVzxQ&&_}nk_T{e~wH&vi($qB8EAKG^edY)_KQhf@orV8QHP~Y(1 zI!izKpttH^<7JY>cVkL_MezKgu}5u%Yt7mjBSxjNkWkBOtwYyf>Z%7$^le|E_@Kxp zL;9OVvHJ`leSSnXI3Hx&ReuyV0FOyew|2pJ@O*#*tAzdf3GMLV4gJe*AyAJO=2!m| zVI@j~@O4@)LUi|(E+m5Zn2IVD!7ma#lxMqj{ZTkR)X!%w{01Akhu#$3X81|;N1*%9 zwhEKCUtV%ea+lILa_mX)kf11+yy)v^VPUsaj?ieg4r0gLjj;>%l|bisS{6P3cnazIOpq=Lf8jcx#;d90l3We-JOhBsm&AQWlr=eoGY?NSzAbSxHVVT zUFX!6^!Pffbn(ixh~-KLU90%z^r5_rgKTNxvpMteU8-3cue4D}HCaR3cfCQZZm`Cs z1S?{=j~YWtjYQI6h_Y_s;%afMt~!O&If0kQpq-l2!oL#-!pPOUyl~03UH<3<od;m(8xL?;iQ|SF&gK!*6h1t61%m zBlc@nOwNf#PM9v$XbmKBVA)yRL-MGKgSC5&mTR*>u>V}#whJ_A*C@AZYXuzkPDZ_1u>2DUw^Acb@Hs!Q@6v93ja z{3vYq<+ia*at@=zKq}Nk-)i6iZ6!VWh9cyTWMd8V%n&d1=_>;?{kv zlBjc>69R(}6m?5^c$luljR9RgNTA|$|9_K z#)_PHAhw5=j)!X`AGYz%-+(ftINg~z#G-!2J@dumz@M(??j1Rj0^HqmN2A?u zKq|sKy0>}chg&{$O=H2T`&1!5d}PD5Q{BQhB~7Ks?1sKe-bmg#6^EDUC4SOZ(wwbQ zg)A#)w4@+JOWIpZALQo!_rpb1W}d%*1z=xD6g^<|nO|I8USTps)4()i&Q3%=*gt_D zmIC5!@v)fM4ccx%D%9hEm(U)Q^t6!#1jJw7V;Ep)<{hOGg?|?v{#3f}47Yo%@8XGa zdvga>Kew_BSECFmlxN;p&tgxHv={L*iv!A{>Oq&Kr|m!#!L9Uuz3JJ_L*9W{>jFlW z$M-~Bp`3yyxN~ty%KPKf6=XJz;LXOVY*aAXV?wx>#DOq+e7SYLeE(`l*IbV-Qn1!6 zOl~MEkP<$?Lg)EwZ$g#>R0eFX1-2l+2adw_{)z`7^dn!KcT(j*N##`wA)I}>CLuGs zh|tc~8~U2dvs+7Q#bHn>aA%=X$F^V7?9G_Tc+skCAQtuUsppIrQh4m^3aQp_pgWTx z2Ig}XDl*;A6pMDClepC;(AZVj%?O(x=D?dMEa>rtt7X)k^tUa$2Da?QVPW{L z;gLS{KcGzk8mG-ccc1Uu>XD(}AeN5TWJ6S8GPO6?y=|VU;_<^XvAslRFy5BOh_QH}Ynd>{-Cd66#of zOs$UViJuiA2B|MEzssSZh48&itEtorgcfxrdc_Mdys{|Q_G5gKsl&fXMR5=klXiBh z{Ue7+!<*iP1w7C&o6F6LJn5?e-P+hd7S$vwjzBz(`&QY}-#i1bJo=SouZG%VMy}@k znvh#j0=pV^`?<%pUBjA5UQi7)&QvVA8%NzeDKMYocs2L-)w;AC7R*y0=p_2w*i`C0 z)>v*uj!E4ihXrO!rE6^y1o;}N9j#tiyJ^*0hIIeXpfD!;u~7e%mQX)T>1D8=R6k7n zGG8r{mvPTLCH&|`MdzcxzN!hpl?`om8vQ{RPd__E9BTh+L0ST}v$~1u^2v1%(uRTs zLR%*7aMoJxUWvjEAujL2Zn)nXjooL>)kz(@9rJBU`bRo5rgno{G>}*Hz;!*UCDq`{ zZ5k??mgmaKkvE$EHPXAV`@wp9P> z9nZEbv`~mgqrW_8%bJxMcunsLi_R{6!>k$Pq4zi>1a5qnB}=&i-`_nrB#6(R7x#F? zKQX8;a|xFz?a<|R*!dYe3_odXFzqM9g8aO9AIna+SRrpsqJ|L2iWDm=0*Bfw%;egB zeXURKgP8O@`)$aNLYwZ}>DrOhK()@SahlF=SzgQ7k|;#3V?yK9O_Q}Qnh_DSLS(DQ zkO+uD?x5KN;RoU-6&=cN^5GCxgv=mBiEo;c%HTIz2A-Us2)#K|t-5zJ%m+5iTX-6eJ z?;M}_n?U^^ClyBK!rr2V((MLM%;f#u9)5>2?*>zPj&Hlnjq}jnM`*j*3P`77Af?W4 z#-cwh2B*pB*fVH*&y^Kbu^Un?UL{)LHrF!NgRZxdUZ{JQ>uj_vqp0A=ZI%WCc`k4} zwPwg!@c0EN7-ooXrWBJ}9$n!%&x4QSI$~VNz3RS0v3@&Yo|q7>xe+StGhN%gT)$GO zlq67Vb}E_18&uNv(*0HP&j_yFd?s-bM5dwxdE+0y2UIN452FIHUph2|TBat;hcl~5 zMCF7jhX*wWJbe`0^vqfc)0^f?wWJ85x@}NPn9tmv)thhUwfDiR!+Kzi0{6qUVfKQ< zX8!sdPBsHqW~IEp_8^{f1szUd=|~t7{{e`Ph~#$kQ2WTELO%aV8jUz?&iZA>O`7TaH98i9+Nq3^yrE+%4c5!|%jkEE#=9 z3)!04U$rAdF2ulA1X{(m_}ZU%nPzC6R$5>VwcTqhx6mWI@cBE5>+g!?_Xhxp2+fap zMv=1eq+~pPgtA}I_qlD+5c{A38h95i1Q_ymCj|cNR_NW9>6qCt)`sXw#Xjqqt(<6U zMYrf8=DiuvO;nr?@B(xy=et`h(RhdOuFa2XwGwW#u2OlovsT`SoL-`3YcK!2CE@&D z?IVozU}ooj*dwTWp;*bZB>x)zEN252Hq_r{Z5fndeGt+_^P;2%_(U)7iQjsCzcem} zc!x%Z7RQkrAReKbs-@GJ@Lg&YxFFM#Hvd3t)kACbLXnDcWY)lgk{f%=K_#x$^WOS? za$=+Jx2RX22;q|L39oYGuGbR^Hxi54 z#hou4-;#K=e9paD(K?wNM}ivlH1J~1T``&8P`-1`!qbujM-dK@8+B3*AULKl>;Nj<^c7HOa zQubC$tXQNLss4@Gk$1S_ZLlYBM;#bmQL8z`Nj6c*x+5sR7#|$i&31rYY@>p6nODq4?DI^2K?!H@v%k)H4^iM6 z6K|Yv;zh>@>v&5qGWj}Ndq4c3Q(pOp%vr0zkJnkoafq( z_u>i9Uacnsq;F{w6Dm|*nI{DH!#zAaNJD{i=7X$tU8%}UaXu@(MKV0KX0!6Kzw-%+ zAU@F($Q0h4YVmNDvXVt|dvt5G+~@NtpZP*a`3Sy^?6|Z$4gffu=@V{m=dOa!ex-`0 zV_z?S-$F0TqECt8(O|hZ4mw%D2z$Ul=lJsZDh5(_rGTm;^V)KuJ^M^WR`Lv@w=>#a zWc9Xx!{)unZ?x=}eM6W6v0zA)3ahBC_yD$H;SSJYW)XM1-2w-Vzydmj-97uMY?C-Z zE|-M;JuhJ)@*z-NW{NbQ?zS*!Uun@?t`K9K-op2VXSp$-8mtgg>->~$$D>*V!|b(7 zw@9NYvbnIopm)-c{;nBM5u1wP3X7QSXO~1--s0I?t=o02!kC<_&nZEufV^T6h~}{o zVw3|N?w*`lhkA2iy1H*Me^2)7ifmP!IbS*PSUr#Y*1WGyh3aT(#Zk%fV>BDSV6p+; zgs8aClNU%;e?e%LbKG!35z5 z#h%~K#4s5P02gxG#R4SO?gM9HP7vp*CRjT{pO7tr3+C^Z7OJvzba|>xZz@$8HCMf$ zC1u+cUOB(TAYnyOBU?fal&z+MrUp^-frE5DN z8P*5O9TMD{4pGn?K5>u%j;9b^0ZqZMEvC0izcTDT-3Hr7Bi|ObyRQdwz`Iv~hD{2z z&ZP}jRV(@l)+WXs6M5bDmNwdRo#bGh8r`wH0ryLkdR!eX-8r~Hc}$X!n^&6Bneln^ z_N-{yyZe)4Mp6~-RfC!zM1lOFbpWzc@J=w z^b5esDTEX`;`sRnDh(AvlnZT}AXT?{SOa@mc-(F8pkDfXd2ESs&{2+CU6>|aIyb*o zSV9S}4K@GY^h+!@3s#TaT8v-e%d`<{m0LGJC-L~g6I~|BIbQMPZe9DXv8we}L>RkY z1kz*LjqMTW=l)Gexi>6C8@t|FX*@9JF!;Wu$L*{k; zvm5IXqpU^N>b{D8*NgVSTU76T@rgQ&|1hUtzSs*Fv8hKr|12XSXd=Ug*OfWZui)ot zlL3iYO$~oz4U8EmN`F}(7o{PVbHBr!B7mLXeepwm#h)D7AC`7k5%m8YVYz%$vtVU- zJK(O=dauEfiI#)RYeaxT!C4eN#rEVEq0S29V0({A`YIb9dbvDK&i9kwPWL-^zdubX zImouNymE=*$JZYkzF2f+ftkz7{@2v_FA4@=^LaxSb|-WCp9ckT@b!f9L;vbDzlH0X%C>-p~)>nOgudrB^Z}7%LM_07BhaF_UxDYT6c_`SyOJLcK+t;F%ig zVJdAap34;+L_$bd`FfDLbNZ7I?9HCwA{qdry8l4vw{yM*kadf!)Y7~4kt2Ar+jWrq ze#WEc{QQDKl$)+f)7s1-GRrd;nZMEEJ8s+6nk8f&?swVFP!iI73&Q$iGX1#)k&Ii= zlMVscJLih=^t$m`bUF?=^@Q!4RPZB`K>9JM-~dEQnA!PPjs~!n!0(qf#H4+1VF6PL z7T_m=hR+G_A=Ew4>9Seqne0}#ny!|M-(l!vlw1+59dS5^01@kz8V{n7&r3G&Gbm}* zslzZkCQiizhE>`!UL=kHg|;07^aRNI8P3oADlGq(zazQZA{Xto8}JS7*^@&B$Ehr= z9jQewDQEDxJEs}gZ4tFF>_Lubd|s8r1jvb((Lk6nQeGNA1!QJ(XjnzvhfL@5_?;&^ z0Qkelb$08tduIO=p`Sv8AA!6H*|`Np$nV~$g?Mrx&#JI0qM60txy9Z&uZ3y9xyVdU zk5uCW3HviO&8SFK;&c>~)e6Gy^m=XY+oet4IG6d7$5WF*rq+lPuj=Z<9-#4+m69!Q z1Ub%TC)t|+?O;`p{RGJTX(j1o)fZ1>>Yd#aP{~OS6`4$P7T2OYO-2MI0u(araL2bi zI!;WvT~+S4MIRf~FXB8aQ~dRjyU%LpE9db?r%6~WMtCG{lxM+xLGNY7t$n7gw}#PW zJfaEIE#qs=txD`pL1r(dzfiTV-hK%59*S@CW4dhsts;|vfgFTPyBxM6Payco?SuFm z4J>fxKrqK{h3r}cFuR)99B z1(Wh7EvWQjuLNj2NbJy(;V_GKQ6JRi4Z5?Oug9>&JegfcynBfsvv`tKe9UjnbhPSX zR>RWF71pa3CL8Wqt3_jb)bPT7+@Q!FNGr z&-3YWU|H-%xkaugS)=cSYtgp+!skni%qAy?Wb^_!`(GB%>gz-k_YPRK75#b2J zqL_Q7?ZVUYR?0+mMC1FLwtSkemS^ze>4wu5EIxz0jRSExNCmirsu!nwVbaJ-g+fzx zg`s!y>sWry=qMk74^c|tiXNBeP7GF-RQR|qKZaAn2M>$?p`4yc-mK9viyxa6D{Dpv zftILfcK0Q87W+JgoqQCTp}M?5ttNKbOz52LOs}XMp9bP&1s7L?hiL(adKL1byDd&w zf62aD@rFefR?~MU^ZpVw+|c8+d*PjtY=s=>E~i4a0)I14Wp0jXcQIa{^V}~*b1u|W zOlxA_b$;_gPO#RR9jD=f|LQ$t&4FZxe3$#*VtxUPR++VC1+TC1Z4){3Ukv9?3fN%% z>mm+Q6*^=Xs@^lxSvn4ED!SqzE3VlY<@#)0!q!%4#_4)p46b(%@F6 zEBms}!ke98zt=Va3T{nSx(@)j-;(##gRb(y0Mn(#7)HtMr$4{Bf9}AO-*aUeh}|jo zkzSzl!TM}DYE?IVZCYeslS?(NX{%&Lmc_Pv=2*^Pl1=>PO^?SdCu1#G2TPFl%>C*u zsv>6P`ZakEnw&+R!d=3(53|O_neLo+S6LZ7F!f+EL{!9tsc-m7y}6OjTx^JDCW%#| zJWT5_JJPdJ8?3v>^>@i|CdVkV`i zk2gNOm=-$efIdMr6)(mFU4ErDiT?VYQpg9EOb}Jp_Rb8GHdDEW^y3(b7+O?kO33~w zS3Wros-&-|yMo_0H!;9H1U{!$GNCR9ny2Zu%*+hBug6E!DBO<T1j(xrVhsi~e^9l6=w~4U_RIu@FInqd< zGC<3jfF`2qvK240<(7G@ymQli6ipb&?t?QNLkqZ(9_!H}IxL?!I8#YHZhom-F8xju ziccX4JE_|n9}W6N?Gx=;gk*6Re4Wc~Q4u!fH@suB+1i!6!zPfR1%pwL-c0^nm_Dku zvZNe)@HUl8W9O1?0!E;C9`6S6(%qnLrg&=VKvUH)vN9MZU0rNeF{g3AyszNxKmmNT zt@XM{7ST!$>$f#+nm=zsAd|{4U4OkCYFCz%H~rN}S!t0|5Ll^)9@)P7H(UVW>LRyh z)~ZcG_1!^w1Awbwnj}WVXc;hiFZ6Ip>1(!~NEAF*qO0`Ey~rURmGY=`gu%ZyIE=v$ zdb&~G2O)*tOh+XT^3!95iHN?_%gl|2LgSAF*UC3;Tgmo2ePEJUZ zKBiZD4h1rh`eFsNZW`UM{1|)~GAoRU@$=0j2uRREL!q1evt&m`(ub$J&WVyQ&W zS0g$;0w+3=hlN00lOJ!>o)o_cCLo~86e!^$8;-|+a}`r$Wwik-Yv$cg91gmRxNpA7 zxL;XEX(e+4uTWC+En2gRX+i?N)mPHl!ev{?;Pr8Fu@UsNUO#fz?3~y+jy7S=+I)8= zt=y>rDnbP>zKv6R<~h$PcQ0X!sja0OiYe}#JZgaAnk>V#-74v7aUPJ%vDR*_z8{fh zauD)LCDkl4!~L-9Qk*?rtq7K8lx1H~i=;VKm4DCNTs(<`0d8{QD41IKYjytDuyFyl zu(lxkrP_3dJb;RgJ*16R62@i96UH1CtP*&$ zZUlgeXtYF)pQZ7}S8|Ty69*|N$~eCDHHry}PYRs(Fu9C++2LmMVJPo5{^aOqDZ-?h zJd~TGsl?LXhhMM&Yj5H)gHSXa&+B^Io|^cjJ!0seG4p?y!`_hru@;W@yEqeBxr6S~ z?kB;hv8X_5h%xR_sa`5VmVbD<=;VCtMZNLW>Ez==VOHnGpbRtH0USxT9npunvKQAr z%`lKK(PM=u5y))VY-c*1sq|H*gpTXFI}X|L`pepJB(p&5z8b_=%HqBAzG~1V>c*n( z=e1@7)Nb7KLDp)1Q~A|MySHNQDpQx0^xc|W31Mg%fO`{(l|20G2EPE<1?C&b>!!Q+ zJw1>dV%fCLq?*Qig={Z4}w?j!Wts9Rx-aJ9J&5rbv7g z>OeZkEaHWnL{V2WreT72$u>%QC42?K!=5Q>zUvY;CL+S08NU>)xcZ*jVZqT0Xe|9YvPoCh)66Bd4nT|j{B0y#FOa<&b8=Nq$A0Ncu?(U;MRY!JZCJk--4o#fl!J~fnBj}c}efGlUhe^ z;FXUCsT05C+p=+AW_(!QB4WN^weehPy+^$FQ+qJH*50EpwKCRB7eoieFTKJJ6?4yn zhTcSfUXtanLCiJs+`-p$7vVo(OysI%UbBiZ2}Hg)ZPd5Lt|)1|jP$k!XX-us?m%Bt z5A40E=!m3OeoL>b8>Jj8hGP4%k=eb3bG(|QO2!cnTkZ_Z>Kh~O1OXCT;k{X*gvt1 znNC#dU-H7bYR>AdxkxJm7^H)k+8>0#HbEU_S>fq}-S_o`Vvhh7{5q<#n%HUSqE;>lB&8B=E+S(Dw30IpNZ~2rmpACN zVQERAP=ZIt-N)P4s+CMtRGU>=r;O}nja^Beqho;I5=)W%4L$mT4Ey5dxGwYKrXU}7 zgElh#ew)TO+vVdeoSu#=N&xVifukS1-};v-8&nn^J@rurVD>f+HRkB+8IQPD$+2Do zJi#sivj=e;uB?g>bEHG(MgzCtX*C`ANnAKU36N~#9fKWc=hXJPuE4#!?bi& zwI|&Xa$XaEu4 zB2|u;X087L=`PlUttd5Qk9{%4H@$kZ%#LFtF0&s7%D;p{2*vZc!Rmh8Z@V^S`V&MT zb~d61s=OC&km}#~8USGJ_VS7bfOod-utp0RrgBbJv7SS#dWN+6+>VrP`-thN0`Fww zW;HHrKZ`UeIJ}r3*pxYMHHf^=;U3y}XEf=zOHK(cxgs+sX%|B*dw>$Ory62h7|2E7 za#Hhv8ou9bDZq5ReZO167VKqti8X55O~&S3>6Qy)p^Y@x8&~>f=M)9i5dH3uW`x97 z;3zgaFNxgn6T)SM&%~#)O9>k0Wc6ri;zq-JZx;ndYjo|0uf&70d?ueZSen4Ms|$L$ zu<-kH5TDHx9fpt?SK?YYO#18mi)s@{$7K#QhHaJ}3`bZ}zWqDsvy)7e@}b5_?^vokOyBYmP!0cDr;Q>BmQLyjFn_vEXCh%_YCDKw-*G4|jyY7XNC4HT zS)sCN$?*BU8^AS$n;4ZRHHOrq<7Mi#oJ_>>qkMgJ{45dp7}L#bUUb@0oLNi(3LA&3 zwc#rjH0-!$-d>d&1E&HTiLv>}Z}%J7^K78u=ijDmp%8)j`rUdh(;hMzwB{XIh*^(UdZ{*B0uT^TTTCS;X;SW5Rbh7#|+n%!ByxCvz=#xi7dHVO0x6M(8agr%9 zD9P14vU`irih88IcTkx7RzX^94!Jl~C{vY==c>%)QpTWyf(O|YK|_5^c^1zvw`y`p z>s+*DHR)rS=X_IEk@U6LXoBnQ$0|}pq#)15h*Z&(EPI4@-y2{U4A^_dAGuoiV85Cm zy;rXFD)2y%wz3{4f6e00C;(*Y{f3bEU9j=ZbN-44Aa;Cx^E1vEBxzy}RPSGunV#QK zbdqiJnSC+<+7PlW#!*OsI@4|4{(!K+OWU5RMM}v?abNMP@P5=loJ?npU)O6BXw|u8 zogt+)+a;O$bbBeVU7|{vb<3jbDk$D^K8H%54`!r@$0f!$lzd(6IPI2 zg=O-hX3wk6bbq9L92jA$mjYEiwQ^%ct)}}zC_Kys&89w0kmo?vWCEaG#};+=C-}j-O}01M|IacTq)y0ZPPzcK5EzU{*(gJlG~-s3h@-*16zNCeP=*K z0Dp$x0%$Vc;P}j#_!vu+p>Y-m=XO@Rc#4_L*6L#<0~3APQ$H``Y1gikD11nl>^Vw< zMJwLxh9G)P7-UZ!K39wb9Z{q{uUJM3qKGdB!EMJ*Zl~i40=L)OdHPqk4~?=(YmDGP z$>_t#{}CJrY~l%k6}vb6sI6 zJ|nLt&)8H$1>t1z2iW~pfXZUNs)E?f7T-2eyf`jtA)?$KsvInV%N|%*mMqvFGGNfv ztQtqGifKOHUTqYFAz~A$cUB^G><0~P^d?QFi&k$3VBfr9cKM))M-H-O)UpZ1Ef4{4 zMeF8fivfJ>c<*0}gP{L=Wh~Zf&^Q|;fmb(R8LoxW!-pXlR`|P?>xA{r>M7cds+!&f zEfKMthRUOoqGRpjy}urdbQy3gNCl}sWTquKkK6#cM!P+v&r}AANwCQ3Nk1{queoAx zNYoRt&b++iCnIS%zB-@jHDHvip!%kG^=6CaW1aq&Vk`UUB?<9;XlzVqA3T`YnXFny zw#=maSg|X{8dqbv=q9rb+o1LCW8@ZYC#l~;f&eq}5xL4DnGLxRJt?~v^!xWRV74~_ zkQXEK;pngT3>3)}!D3Ghm;UJZE7Ctn8EPlE+Tpcu0+;2+5iSQDJcjAFYWCTF-U_BMUo@l{W7=)+v{>kx+1-X-Hc?Tdr-r=3V=N4hEkA&4rC9nzZeDCB}x zK=fUg%)HH!>uKD*l&`x1N~6x@G7rx>>P$XYIDq-yn-=J3ms_$aH;%wwWspP>RW+Ix zRWX$cWj4RL@Y*u^&QF-wqab|NKn|U%da0HXF1w|E8CT z%uRgK!xa${(6863FD|=$N=WSwJi1>)P=7^g2%iV+73CHHwm7dpw{Mkzuh!IDjaeCE zPhb(|7nK(ayDZ8%4QNKfqh%fjRX8=aDeUJY2n1J7317+U2A~tKFAI&R@8K|j-Fw8 z{_)^qJ}!$ezoI>Efc*;v$(lF47T_^ZPnSNc$0Dy%qce#IJ-F@3rs;6ECDm=Se zhyBRqJ>zwinqH!zk$>5}Gm?0Hn9(xe-RU?(%-}&jxnq*S0qNBN*byJ0S~L9toCeLF zuXC1z9kun-;ikuC9)rdKOvdGIZj?f&i=cH~$|Ks48>}q1wonDXy{RM7pvv{)vi z2eRIF@RhNweqtp=m>wYn2U^WC(xX*l#IKS4;9yPJwdjsbX$$OZ93|8S-NrG>1jKRt%U?>^YI zP!JfsKO7;HdVv8RfyJPQ^3b2!t2%>X zZvjSv^Wwv7&*dF?9udLn&v}l%i!=}+E>+Ej6zEp9PyIq(oIZU%O6f)o>(~!e>%X!& z{}J%Uu-t3A+YU51gh2)92ovT-KVh;`I<7OvN0~-2$p8?j0n$-R115+4LT*)-2F+h!r z&7p*6ANqqTi|hn=({lfr=jd+mKiWI5u%?zT&?_opK}AKRih2;~2N6OCMX*o=A)$oc zTY%6Zgd&Ja^(Z}nDAGcWp-4v&P(XSQ5Q_8?kSg^~^w{~%y^r_d?*m^Xo4xnUtXXYl z)-U1g-5Rh>n}LvC+A!I#TZ+G=uutz0qAM;NQr>0Ve>8R4{6&rLlEBCL3#rVuSGC}@ zJ;z=f6&<+eg>?3EvIx%_WH(9EX+6pIw&r5L-!tk$P%E;lsd>g_Hc_2(yK?*DLQhS& z#+kAz!4J5SLocgDLNIDg=oX;_$$?0Cx;W3$JN6Pcwv;i?5vA+{o}WI0QlpgY;bEO+y7KC(D1bnkAH!!LAOl zVJ?QVhLYQ1gCX@Vm&7z!N!@|N>6Z=}6G39#IoZMl+b-QEyaPTXm5hyXKL2k49ahf0 zZ)n8hdxd14+>d>+#jKU)Qhds?lUp_r93IBYcT5G&pq4%_i!)#3zSNo?p)#XUnO#d6 zS`f^bim5)Fx-!t5#qMTL7%dvD@1K11&xK;jlsL+Qo7%DBk02chY6UVv?}8eqwF~6F zmTdZ#Z2eb+63ECkkt1dQc2gJ)Zt#K&l6ZHGN#po|E4 zEi=bn``<*b-vK{S*C2tZP3p<6TVQv+JbQGS8e&yL+g**6-HnZmFcq^B$?7h=eUB}P zQ;;@90Xw;UG+ENO=7pcIj^qnWbr?4`ltD3I#i6*RZBIM*qh^*$Vx^O_zWdw_Cfq;hK1ozW9Wk$r_b?M} ze|%}ETAvaxdABC)t*Z{?Wr06H0PtkO-BvGd+rf+TsK{@8Tu1vhoiXScxF=A^wLPHVD$;JDd8 zhW_*g97byv;8g6=rL)NNMPs&@$?7h1-3px2ns(41ng1R7crT-PJ|gCM`t`+@@mWn% zo(ji2eqtxaiIKFep9JEJ^rgaer-X!e2N>%#D)|2@4=4hMPt?Tu@o8X@PaQ06+*uL& z&?a8^Fj`5_A6#($+P{nEUq$CzM?=A4CoJiWsS)nL}kl1@C!Y0PDXB$L>+Z?!H$3!f6vPH_BRT_wD7l(2jm=6#%y56rL zWppk5C-eNh)#2p5IhdUNUxVn>ldI>NDJdzOSa`-lise90H>_Jb%Q?_d2S7MTwLpzdz1A8HPyiFx{*M za<1BUW8n1A*=HhO`}G$Cuv-eK-c9MDf6FF5zoHK|E5~is(ADh=eK?PcalECz>8Pfc zXA_gLoP5W|>k5u#L;qg?h9Ta5`Ao;G%snNi^HUIilDaZ{u8Uhl1*{}y22wgUAK+(h1MNlNoQtuYa9vO}i+bu%O|5d_-18;Von1Ghjl&2fH{0Z@S$N z9bZ(?MWXC=SkNs&o)+tvD&3y8bgOH+X8#jjvLGhjIhwageVk_RxOc$F5pC3OV^wQHv zW{Z4wiM+QDqz`1JnETWdi_FqYv>vy_yanYiWR4tL*&*gRmP+2pf9ur06}>ur=OoWpW$v(5=gQ{YSOm{v(P(({SHfTUj;VHX9wkPwS&p>_=NSq_&-W5J9ASx zK_(ftYmn=!(;F8daQ~rLP`5%T;(o}ZvD`sJffJX7AXXwTzq;bzrrE!}a*TuQ znqIo|o#pkbVEho4qrlg3ceNX5_}#z$t$TmV)Bng8{q|`t2b?UvzyE&LkJoaQ(O+Ml z>A@M@@5x7`XJlmDiqk4~&OE{*o@&!q#33P}BOk$yY(SQ)7kM-P_E_*=>67FWv4aVp zZhZSr`mcjsb2jrsRke+cQvI8t;CLEpWwxdq(srL*fbwNFK$-Wr6>jFBOGjg@jMr@- zE2{&qed9=8+v`L@^Cs3Y0k1#%!^fO_@MW21^z--cLJ!qP3tV66b9rhFHGpC=|Bn6J z;A&w-Hg@4Eu95PgfLVj%>=?XT7}R*~?tJ_3L|h~)8&sR?6e0R2=7&nSFWTLeW0dYe zOtz&NP%C8)0{+9PQ(T?iEAa$pO~ciEE8q{xqH zh8^m0@p^MjEGaEht^^d|=h_!qKd$_F*DiLko7b+WIb&gBrKxeIm)Y5CqI=qu+}zut z>d=FG5Ms^})qrU8ej;(xW?i3 zN>b%l&g<-&(C$k!nF-6v&n{Cf7Nx^TEyt!j_0+QJ$Uv@jWa)^d=;fD}SzRyMbXXr| zaafvAr}n(t+sknU@yJvLuMdIqPw3k2k%UWFR6^kQ%zb6x zh2xAH-FbA8+?qpvan85yFf#NYYQs2QK8rFPk2XCoNaal3$Tg*e;=O#xL3Co_PXHY0t(EwbUrc zPVTNi)gQsl)${sXyJvcW^X(fmB@gGCFrEZtlDyh9Ed*O)CTA|xFS~4Qaz43lpxNJZ z*`$0O#!ys~e&{f|Qj%cqG^P%ev0anD*K%1t+J#5nMdv0Vp4zxi+8&TplyWR&RMxxu ztXU^KS0somE+jY-3tj1lDq6wg!ckKHo=EsbPj~^_xEVeq{a}*& ztt$o8D=Y%0^^bG{n{8$#87sO6UJi}2WQPtQ7ss2GmJF+M0Mt9{gZa9Vj7GuLX4v%} z%j*Po$P35yspZy{?rGfQ-5ULjUex>!8gwZs3G1?6cnkF~&%s zoRjn$@`shIEJ-GM&JHB2PRU#Yy{ZoqERzGxnU)#6a~WNsJ!QqvfU-u@Y@{M5OuT4) z4-T7olXbr#iY&T&)AAcNe|~i16?vGT$kCy=zhyRo3U>d!0C0*27@$D+~6xW`$BSBJz@{S+^pWnmjfKR&<)rur>S;cB(XKPtURoO0lWF`;TG`oeZFm7*H`8Ah_I1uD0k( z0XEl|8l!fRk1@1!Z8n`Z$Dt7!sy?j>os#Xb!io{8ccBT$6)5di_ZSZD?q0N@3zr=S z#r{oF4DwbA_*~K_S;}DTne*eijX#F&yKYma1N~bxj^z0l|;P!l~?3Ol=87-2$)o;JG4^t}%+(f1C$n&`bGb*+lAg)ZbU~k2m%8a^658ss>u9xk6ba zumF-W>5AE+45i&uq`WtDb}>yzKX211ow2X1$IMaY=87k6W8SmG%nj?#thixV^IODO8)Wa(jDq!6;kx1)jmPs1S#j^_;BoDF;7 zTuwbdvhx?X$E1F8d zhD;4)IYEzbyWmZ$|3l{m1VPeQj5ae)u7fgINI7>(8|2mUgl^N%m*L#=yHih0f zaa}`)b~{+%?HR^)sEKpT8-$~~G-AIVS|SgOXJOznw_|0 zL8h6!{Fo;HV4*1dvls~?r6X-NzO}dq)ur56hV8L_H=b2#PO_DeAe5n@B@Kv1fA8h8 zSyK|CAYgq)vVe#2ub#O^$rS4S2=DP?kf_bKeOl+4Kh}LvWktMz83n5R5v^KUS~!;= zV1|^<{Xfl+?;7Zvhexo!YL3&~IgF^$NzogJ1UZS&% z@(5EZ*|)4|bdu6WFSXCsY!mxS_oG$Xy>>~}Yh2^hrC2y?WzIilwDg)v zfQ5q09VR{zOJ9|9LjF-Ek^Fn?>lIFBWW1w)pK53L=~R*cHLGw!@Ie|*0ct6!b%%01 z5xu-Fezag@erl>-e0sJUUp!W_T&G78F+&_U#wzW-Yd6JSs&60qeJJkMxD3d*nvj1v z?nZHlj(0j&wG_3Ygqq1ypClC6rp&~8j2+qlpuN5fU*YMTN};Jup=DFxLn;E;nVD9J zEAl@4+G$PhM)3uOB0#Kb0d?%*84b(l?ScmQuT~FjV{}X1lgQ*I85Hf0M}K<<64UP2 z!(wAP6CC6hTh#XnWZt>2ID{kf6kCk-(3vATNHlwaLL?gF29)*PIH zYW*h$33taLfFo4(dtBQ=tC4336(H14qKWAFt{20Ecw@blDurS*Pk6l zUt|VgkBPpytzby**|_xF6|4kShieh*^2yy+d@P z1@9sZXQnYRG_kxIIjrmGD%XIK@x)LinviN+ss6k#GvF6N$#L4dHxyper%KjR2_N=i z-=y19sR@20vnQFaR(o!)B>}wa3z-F}&57F_cauTAeJ5e{tkVD|7IWF+PGJ$U#IhZG z*=e%vvROl{oKAXNHs}(L2v(lRPf-9&*yT8Qk1iQV)nyDcvLIC1^6dkf35Tca+PdM0 zM4en)Xf1wvXbX1|5r+MHHh?SZ27stK9&ClNy~$^^dG(A#fhRPvJx_wiHE0FmFj}8H zd&7QN{Ts-UG-wMid2E-bxOU+iytKMt^0m9S!Fxd6AC+(IMbGi&B;5 z!)D2dOMO5U-6{KMInP3~TBTR`*1Cd2HQBiC(Wyo8-xYAwztod^|lag>cQ-Qv&v?bSusmwTwf9ZDrO=zp;RZ&svvK+ zYk0mq%~a)b#^`j+AU;yy_);|#ccpRZ2`2?CkT8%!PdhH z&WObz{Y!i!{MYABpVbVD%5!VE$!>xd09hL%_pV^gSpF=-hqxLDYh4nPkZi@;T;)+n z4D=on*K4}>70t2gJ2(|X5yEMBP?`qFsFXMY{E`{;Giq({s8`pxSN}5jWxWeNNg7pq zCw;!(tT8@riHD+dHNwQN#ibW{9R~5*+PLhwu_{M28Uu9O2GcljaNzIsqmld1c;uvg z;3?>REV-0++r;T}ageWTXVq~73pY7S&$3>jHFd|S_H^}vNGaSe7VL9S|Zg-DD#fPsjw^g5{4dqcmO-5+yrk`}C0PR&@5f76sqF>;L8KYG8G z4s{{twYo^|V6cWDb}EvYr+*tm+%RqBenX{q&hNdvNK@dNhn!*mg?ySeS?NOOnI1W( zLLIkyx4F|Z=b{3<)3igRwpLHddygiyU&`VyzI3&+B-U%|^Tf+K-nye)X5!WFsZN|% z2FcQz&CZKmgw$6!% z411h1k8P~`=Q>MtM^^DqO^$d;X%LEAL zZQ((`Bjt%l=&*lu#Iddn;Iw?GoiHWK$D9pqS6CC4^P1RXYGMn()Cn{^-T25p0l=EJ zO*R06$8i-8V>5kBE6=UMARi7ALg^EL9my46y9^Q1PG@l7RtFa7zO$>Gu8LqCZ^VNS zI$8_Q3r42{)2 z9~*gW)7Sbe<@7xf?SeQaOYXp;w2=&a!>vF%KAYYPrkQdHn!NDyuH21=L6S};(Aff` z-|hP@ANPF#6z7g&AnV)Qw2EXu81(fWk4nC6U8B9mxEG_<5mYnhO5jC~7NWq+$Jm^} z56l)#WhEEQvZw{S0U)?VcIoou;;BeocZ+47>JpXDsECp7ha)3~!$q8USY5u{x`@N* zi-#xJt8EhBx^SO&t&?weoup!c^T1RmS1O_>!s~SB7e-sx^HXCyl zdpIJF@J7G{(1D2Q6Mu|}wFGdPJMb%DKLyn>Kmb)dkZE|JiK;VztqkeAz15KQAp+X% z;fT@#eJqaKd{J;a-sTQznVR=((whB zUt-*3{NNYU=Rlrq0c-+Z%lM~))LV?iRk`KhqST;h58Sjd?l6n^E7z_GPd}tlb*MGNdy!|Mk$=UXP*(-{>*CD+Csr!otK zLLkOwMm0>052CI46M6VPzf7Uw?(^E-baPr}u3U{bC{(+LO^qSPeIH;i^g@tx(aBW2>9^`*g4p1JY^E`%WIia;-<~gv zn^9jvIqVOe?;pz6Fzjw04wWj?2%ou+D$78qErMpfSq1dqCyLKDI8tICmo4XIJDt9+ z_O?L5=z`haPq<*>RE|L^QI zWt%g$DT9^wAy^~lVGzpn1FN+TqMM7Yz#iliBFi^Ms*%VP&WgZ= zqf#4oJx-lEfr|J|tam|)wuM^>awi@HJZE z05|VG0_R1&TyqtnL_DaK&(l4*q6#XR|KfU*jhz0%0V)>A(X#n1Dk1hVlDXKMo#GA15yrRA0+*QcL+m<>eWPL|IU)bSP@6BUD^_1)aSgu;^~ z0An8G24%Qxt}F!4P?_&gYKPF&p{Hh}0#|ruYh1X7*TdZkKh<*Klb{R34W>@8eXdBI z_KX^;%fp!HD*FMS{mvR?`Ih1?eNO74A0NpCMB21?Ldzj8a~gs&LS^$JHodQazZ8Mk zfYss=D6KhR;PhVxByhQMBla>?1~gv;**=r`_PnqpjptoKH5ZQm5t;h%@Upu04!vdh zGWH}&A8ZrY!^FH>eXkBQiK#^vL&e+(JUZ(O1Q4P}fk31@DT+svTU{F|D+>InK&VAe zu0{8q%&OW@^t92$n?ro{rZ>TqO`HJ8D4zI+W!v)2<;#HbE{AT9N`KQ)y^mH57d8sG zWiJIlFk>8`FI3AeIsecR5af;5q1$TvT*;{ynp762HkIy6SvYR%MfO=ThE%X`6#P^) zi$%(*_9;U8+6*i@&xZ(~3uY3zM262=Otd70KeKC9ED}zgTp2yWGQ6n+kmQOI4~ezy z>UwbiVi|1BL{lv{e)FJ z1b5idEXfk4ZZ?x{x-Gj7>?Q|h6Ba+y(OrtYTZHAeH3u@>0^IfH!DjvwFITDG+52L5 zkw!7LO7mnWXBY;;TwwnhfIM3!T4FQbN@xS(*+C3^(v7fh3NJt_+b{conIjSDz^orG5Md`M8- z2i|-3uwcYS5Q(8$uYPqB`_}$3&T$eXMnn-Y_itq~$c}#rLB`@ySA)!T!|4 zeng=h%THH{RkGb!ejg>P5%1PjU`uoaB)}AqxhNdr!M=GF)qUC@z|I;>m!LYm;qu%* z1WWqfEQ&YSvG6JET3(&XV*Tsb-85;T9*1y=vwWkBFz3uG7FY4N#g654=%HmWf-x7%^gZqqLvNPPzwJ zkzxl;RSXCuQUEwjylRC3EE=ax6GD-VuU5#Q^nLeeDjcDP5peG7g@#oDxgMWCEdp6L zu8W^7CY5#H!O#SddBPkK#0*ZTQHZp)jJ!yN_)Ko=OXXbrI?Q0GXLqq9b;8~0^-^W=Amc5N}=e%u*lC$Aaq&lUn%Rv8Kfn1y2gUM1_Q`% zq$SDx;l2W|V)8f`UA7q!^5{x?OUvlb^yFEL)<-i(Fvkfa4fEBk6Ac36umkh{j5aqw z=E^dVOKqn&$w>m9FkGu^^Q#l%qrm#+&HQjp$|{SV$5Y$BA}7U5^TA~nt;vGzd7`NY zS+Wa9V;n*Ku)Lyc5@hG*_lX480NK8KK7H|Djm)Z-4>srT0&?O=XdMZB=Gt`8K3JdC z8@~e)4{mv5HpZ}WO#o1t5g+oIpzH8|8vC{tjS37*LJPBspgBg__zWP46Jy4Kq&$8`!_5C+H seE6Rr{ZEkoCrJO(NdG@+q&Y7>mY_>2FXj_=fqyq;m9Ax7)xZ0H0F3jR2mk;8 literal 0 HcmV?d00001 From f7bc91ad7dffd4992a964e2052c4b884b110ec33 Mon Sep 17 00:00:00 2001 From: Param Dhanoya Date: Mon, 25 Jan 2021 18:00:29 -0800 Subject: [PATCH 35/43] CSPL-540: Added utilities and test for s3 indexes --- .circleci/config.yml | 2 +- test/env.sh | 4 ++ test/run-tests.sh | 15 +++- test/smartstore/smartstore_suite_test.go | 47 +++++++++++++ test/smartstore/smartstore_test.go | 67 ++++++++++++++++++ test/testenv/cmutil.go | 83 +++++++++++++++++----- test/testenv/deployment.go | 31 +++++++++ test/testenv/ingest_utils.go | 30 ++++++++ test/testenv/lmutil.go | 34 --------- test/testenv/remote_index_utils.go | 88 ++++++++++++++++++++++++ test/testenv/s3utils.go | 75 ++++++++++++++++++++ test/testenv/testenv.go | 35 +++++++++- test/testenv/util.go | 35 ++++++++++ test/testenv/verificationutils.go | 18 +++++ 14 files changed, 511 insertions(+), 53 deletions(-) create mode 100644 test/smartstore/smartstore_suite_test.go create mode 100644 test/smartstore/smartstore_test.go create mode 100644 test/testenv/remote_index_utils.go create mode 100644 test/testenv/s3utils.go diff --git a/.circleci/config.yml b/.circleci/config.yml index d03f247a3..3e24f8dec 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -282,7 +282,7 @@ jobs: mkdir -p /tmp/test-results find ./test -name "*junit.xml" -exec cp {} /tmp/test-results \; environment: - TEST_FOCUS: "smoke|ingest_search|monitoring_console|deletecr" + TEST_FOCUS: "smoke|ingest_search|monitoring_console|deletecr|smartstore" - store_test_results: name: Save test results path: /tmp/test-results diff --git a/test/env.sh b/test/env.sh index b025153d7..513758f3e 100644 --- a/test/env.sh +++ b/test/env.sh @@ -10,8 +10,12 @@ : "${ECR_REGISTRY:=}" : "${VPC_PUBLIC_SUBNET_STRING:=}" : "${VPC_PRIVATE_SUBNET_STRING:=}" +# Below env variables required to run license master test cases : "${ENTERPRISE_LICENSE_PATH:=}" : "${TEST_S3_BUCKET:=}" +# Below env variables requried to run remote indexes test cases +: "${INDEXES_S3_BUCKET:=}" +: "${AWS_S3_REGION:=}" # Docker registry to use to push the test images to and pull from in the cluster if [ -z "${PRIVATE_REGISTRY}" ]; then diff --git a/test/run-tests.sh b/test/run-tests.sh index fd53fdfcc..8f8074bc6 100755 --- a/test/run-tests.sh +++ b/test/run-tests.sh @@ -91,10 +91,23 @@ if [[ -z "${ENTERPRISE_LICENSE_LOCATION}" ]]; then echo "License path not set. Changing to default" export ENTERPRISE_LICENSE_LOCATION="${ENTERPRISE_LICENSE_PATH}" fi + +# Set env s3 env variables if [[ -z "${TEST_BUCKET}" ]]; then - echo "Test bucket not set. Changing to default" + echo "Data bucket not set. Changing to default" export TEST_BUCKET="${TEST_S3_BUCKET}" fi +if [[ -z "${TEST_INDEXES_S3_BUCKET}" ]]; then + echo "Test bucket not set. Changing to default" + export TEST_INDEXES_S3_BUCKET="${INDEXES_S3_BUCKET}" +fi + +if [[ -z "${S3_REGION}" ]]; then + echo "S3 Region not set. Changing to default" + export S3_REGION="${AWS_S3_REGION}" +fi + + # Running only smoke test cases by default or value passed through TEST_FOCUS env variable. To run different test packages add/remove path from focus argument or TEST_FOCUS variable ginkgo -v -progress -r -stream -nodes=${NUM_NODES} --focus="${TEST_TO_RUN}" ${topdir}/test -- -commit-hash=${COMMIT_HASH} -operator-image=${PRIVATE_SPLUNK_OPERATOR_IMAGE} -splunk-image=${PRIVATE_SPLUNK_ENTERPRISE_IMAGE} \ No newline at end of file diff --git a/test/smartstore/smartstore_suite_test.go b/test/smartstore/smartstore_suite_test.go new file mode 100644 index 000000000..fdbcc2a6d --- /dev/null +++ b/test/smartstore/smartstore_suite_test.go @@ -0,0 +1,47 @@ +package smartstore + +import ( + "testing" + "time" + + . "github.com/onsi/ginkgo" + "github.com/onsi/ginkgo/reporters" + . "github.com/onsi/gomega" + + "github.com/splunk/splunk-operator/test/testenv" +) + +const ( + // PollInterval specifies the polling interval + PollInterval = 5 * time.Second + + // ConsistentPollInterval is the interval to use to consistently check a state is stable + ConsistentPollInterval = 200 * time.Millisecond + ConsistentDuration = 2000 * time.Millisecond +) + +var ( + testenvInstance *testenv.TestEnv + testSuiteName = "smartore-" + testenv.RandomDNSName(2) +) + +// TestBasic is the main entry point +func TestBasic(t *testing.T) { + + RegisterFailHandler(Fail) + + junitReporter := reporters.NewJUnitReporter(testSuiteName + "_junit.xml") + RunSpecsWithDefaultAndCustomReporters(t, "Running "+testSuiteName, []Reporter{junitReporter}) +} + +var _ = BeforeSuite(func() { + var err error + testenvInstance, err = testenv.NewDefaultTestEnv(testSuiteName) + Expect(err).ToNot(HaveOccurred()) +}) + +var _ = AfterSuite(func() { + if testenvInstance != nil { + Expect(testenvInstance.Teardown()).ToNot(HaveOccurred()) + } +}) diff --git a/test/smartstore/smartstore_test.go b/test/smartstore/smartstore_test.go new file mode 100644 index 000000000..e37e28e7b --- /dev/null +++ b/test/smartstore/smartstore_test.go @@ -0,0 +1,67 @@ +package smartstore + +import ( + "fmt" + "os/exec" + "strings" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "github.com/splunk/splunk-operator/test/testenv" +) + +func dumpGetPods(ns string) { + output, _ := exec.Command("kubectl", "get", "pod", "-n", ns).Output() + for _, line := range strings.Split(string(output), "\n") { + testenvInstance.Log.Info(line) + } +} + +var _ = Describe("Smoke test", func() { + + var deployment *testenv.Deployment + + BeforeEach(func() { + var err error + deployment, err = testenvInstance.NewDeployment(testenv.RandomDNSName(3)) + Expect(err).To(Succeed(), "Unable to create deployment") + }) + + AfterEach(func() { + // When a test spec failed, skip the teardown so we can troubleshoot. + if CurrentGinkgoTestDescription().Failed { + testenvInstance.SkipTeardown = true + } + if deployment != nil { + deployment.Teardown() + } + }) + + Context("Confiugre indexes on standlaone deployment using CR Spec", func() { + It("smartstore: Can configure indexes through app", func() { + volumeName := "test-volume-" + testenv.RandomDNSName(3) + indexName := "test-index-" + testenv.RandomDNSName(3) + testenvInstance.Log.Info("index secret name ", "secret name ", testenvInstance.GetIndexSecretName()) + standalone, err := deployment.DeployStandaloneWithIndexes(deployment.GetName(), testenvInstance.GetIndexSecretName(), deployment.GetName(), volumeName, indexName) + Expect(err).To(Succeed(), "Unable to deploy standalone instance ") + + // Verify standalone goes to ready state + testenv.StandaloneReady(deployment, deployment.GetName(), standalone, testenvInstance) + + // Check index on pod + podName := fmt.Sprintf(testenv.StandalonePod, deployment.GetName(), 0) + testenv.VerifyIndexFoundOnPod(deployment, podName, indexName) + + // Ingest data to the index + logFile := "/opt/splunk/var/log/splunk/splunkd.log" + testenv.IngestFileViaMonitor(logFile, indexName, podName, deployment) + + // Roll Hot Buckets on the test index by restarting splunk + testenv.RollHotToWarm(deployment, podName, indexName) + + // Check for index on S3 + testenv.VerifyIndexExistsOnS3(deployment, podName, indexName) + }) + }) +}) diff --git a/test/testenv/cmutil.go b/test/testenv/cmutil.go index ab880255d..119025d02 100644 --- a/test/testenv/cmutil.go +++ b/test/testenv/cmutil.go @@ -19,8 +19,31 @@ import ( "fmt" logf "sigs.k8s.io/controller-runtime/pkg/log" + "strings" ) +// ClusterMasterSitesResponse is a representation of the sites managed by a Splunk cluster-master +// Endpoint: /services/cluster/master/sites +type ClusterMasterSitesResponse struct { + Entries []ClusterMasterSitesEntry `json:"entry"` +} + +// ClusterMasterSitesEntry represents a site of an indexer cluster with its metadata +type ClusterMasterSitesEntry struct { + Name string `json:"name"` + Content ClusterMasterSitesContent `json:"content"` +} + +// ClusterMasterSitesContent represents detailed information about a site +type ClusterMasterSitesContent struct { + Peers map[string]ClusterMasterSitesPeer `json:"peers"` +} + +// ClusterMasterSitesPeer reprensents an indexer peer member of a site +type ClusterMasterSitesPeer struct { + ServerName string `json:"server_name"` +} + // ClusterMasterHealthResponse is a representation of the health response by a Splunk cluster-master // Endpoint: /services/cluster/master/health type ClusterMasterHealthResponse struct { @@ -122,24 +145,52 @@ func CheckSearchHeadRemoved(deployment *Deployment) bool { return searchHeadRemoved } -// ClusterMasterSitesResponse is a representation of the sites managed by a Splunk cluster-master -// Endpoint: /services/cluster/master/sites -type ClusterMasterSitesResponse struct { - Entries []ClusterMasterSitesEntry `json:"entry"` -} - -// ClusterMasterSitesEntry represents a site of an indexer cluster with its metadata -type ClusterMasterSitesEntry struct { - Name string `json:"name"` - Content ClusterMasterSitesContent `json:"content"` +// RollHotBuckets roll hot buckets in cluster +func RollHotBuckets(deployment *Deployment) bool { + podName := fmt.Sprintf("splunk-%s-cluster-master-0", deployment.GetName()) + stdin := "/opt/splunk/bin/splunk rolling-restart cluster-peers -auth admin:$(cat /mnt/splunk-secrets/password)" + command := []string{"/bin/sh"} + stdout, stderr, err := deployment.PodExecCommand(podName, command, stdin, false) + if err != nil { + logf.Log.Error(err, "Failed to execute command on pod", "pod", podName, "command", command) + return false + } + logf.Log.Info("Command executed on pod", "pod", podName, "command", command, "stdin", stdin, "stdout", stdout, "stderr", stderr) + if strings.Contains(stdout, "Rolling restart of all cluster peers has been initiated.") { + return true + } + return false } -// ClusterMasterSitesContent represents detailed information about a site -type ClusterMasterSitesContent struct { - Peers map[string]ClusterMasterSitesPeer `json:"peers"` +// RollingRestartEndpointResponse is represtentation of /services/cluster/master/info endpiont +type RollingRestartEndpointResponse struct { + Entry []struct { + Content struct { + RollingRestartFlag bool `json:"rolling_restart_flag"` + } `json:"content"` + } `json:"entry"` } -// ClusterMasterSitesPeer reprensents an indexer peer member of a site -type ClusterMasterSitesPeer struct { - ServerName string `json:"server_name"` +// CheckRollingRestartStatus checks if rolling restart is happening in cluster +func CheckRollingRestartStatus(deployment *Deployment) bool { + podName := fmt.Sprintf("splunk-%s-cluster-master-0", deployment.GetName()) + stdin := "curl -ks -u admin:$(cat /mnt/splunk-secrets/password) https://localhost:8089/services/cluster/master/info?output_mode=json" + command := []string{"/bin/sh"} + stdout, stderr, err := deployment.PodExecCommand(podName, command, stdin, false) + if err != nil { + logf.Log.Error(err, "Failed to execute command on pod", "pod", podName, "command", command) + return false + } + logf.Log.Info("Command executed on pod", "pod", podName, "command", command, "stdin", stdin, "stdout", stdout, "stderr", stderr) + restResponse := RollingRestartEndpointResponse{} + err = json.Unmarshal([]byte(stdout), &restResponse) + if err != nil { + logf.Log.Error(err, "Failed to parse cluster searchheads") + return false + } + rollingRestart := true + for _, entry := range restResponse.Entry { + rollingRestart = entry.Content.RollingRestartFlag + } + return rollingRestart } diff --git a/test/testenv/deployment.go b/test/testenv/deployment.go index 38fc92342..005774391 100644 --- a/test/testenv/deployment.go +++ b/test/testenv/deployment.go @@ -34,6 +34,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client/config" enterprisev1 "github.com/splunk/splunk-operator/pkg/apis/enterprise/v1beta1" + splcommon "github.com/splunk/splunk-operator/pkg/splunk/common" ) // Deployment simply represents the deployment (standalone, clustered,...etc) we create on the testenv @@ -427,3 +428,33 @@ func (d *Deployment) DeployStandalonewithGivenSpec(name string, spec enterprisev } return deployed.(*enterprisev1.Standalone), err } + +// DeployStandaloneWithIndexes deploys a standalone splunk enterprise instance on the specified testenv +func (d *Deployment) DeployStandaloneWithIndexes(name string, indexesSecret string, volumeName, string, indexName string) (*enterprisev1.Standalone, error) { + + s3Endpoint := "https://s3-" + s3Region + ".amazonaws.com" + volumeSpec := GenerateIndexVolumeSpec(volumeName, s3Endpoint, testIndexesS3Bucket, indexesSecret) + indexSpec := GenerateIndexSpec(indexName, volumeName) + spec := enterprisev1.StandaloneSpec{ + CommonSplunkSpec: enterprisev1.CommonSplunkSpec{ + Spec: splcommon.Spec{ + ImagePullPolicy: "IfNotPresent", + }, + Volumes: []corev1.Volume{}, + }, + SmartStore: enterprisev1.SmartStoreSpec{ + VolList: []enterprisev1.VolumeSpec{ + volumeSpec, + }, + IndexList: []enterprisev1.IndexSpec{ + indexSpec, + }, + }, + } + standalone := newStandaloneWithSpec(name, d.testenv.namespace, spec) + deployed, err := d.deployCR(name, standalone) + if err != nil { + return nil, err + } + return deployed.(*enterprisev1.Standalone), err +} diff --git a/test/testenv/ingest_utils.go b/test/testenv/ingest_utils.go index f215a9a4c..f2a6a99a2 100644 --- a/test/testenv/ingest_utils.go +++ b/test/testenv/ingest_utils.go @@ -222,3 +222,33 @@ func CopyFileToPod(podName string, srcPath string, destPath string, deployment * //go:linkname cpMakeTar k8s.io/kubernetes/pkg/kubectl/cmd/cp.makeTar func cpMakeTar(srcPath, destPath string, writer io.Writer) error + +// IngestFileViaMonitor ingests a file into an instance using the oneshot CLI +func IngestFileViaMonitor(logFile string, indexName string, podName string, deployment *Deployment) error { + + // Monitor log into specified index + var addMonitorCmd strings.Builder + splunkBin := "/opt/splunk/bin/splunk" + username := "admin" + password := "$(cat /mnt/splunk-secrets/password)" + splunkCmd := "add monitor" + + fmt.Fprintf(&addMonitorCmd, "%s %s %s -index %s -auth %s:%s", splunkBin, splunkCmd, logFile, indexName, username, password) + command := []string{"/bin/bash"} + stdin := addMonitorCmd.String() + addMonitorResp, stderr, err := deployment.PodExecCommand(podName, command, stdin, false) + if err != nil { + logf.Log.Error(err, "Failed to execute command on pod", "pod", podName, "stdin", stdin, "addMonitorResp", addMonitorResp, "stderr", stderr) + return err + } + + // Validate the expected CLI response + var expectedResp strings.Builder + fmt.Fprintf(&expectedResp, "Added monitor of '%s'", logFile) + if strings.Compare(addMonitorResp, expectedResp.String()) == 0 { + logf.Log.Error(err, "Failed response to add monitor to splunk", "pod", podName, "addMonitorResp", addMonitorResp) + return err + } + logf.Log.Info("File Ingested via add monitor Successfully", "logFile", logFile, "addMonitorResp", addMonitorResp) + return nil +} diff --git a/test/testenv/lmutil.go b/test/testenv/lmutil.go index d2feaecf4..d2eea6f21 100644 --- a/test/testenv/lmutil.go +++ b/test/testenv/lmutil.go @@ -16,15 +16,8 @@ package testenv import ( "encoding/json" - "fmt" - "os" "strings" - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/session" - "github.com/aws/aws-sdk-go/service/s3" - "github.com/aws/aws-sdk-go/service/s3/s3manager" - logf "sigs.k8s.io/controller-runtime/pkg/log" ) @@ -62,30 +55,3 @@ func CheckLicenseMasterConfigured(deployment *Deployment, podName string) bool { logf.Log.Info("License Master configuration on POD", "POD", podName, "License Master", licenseMaster) return strings.Contains(licenseMaster, "license-master-service:8089") } - -// DownloadFromS3Bucket downloads license file from S3 -func DownloadFromS3Bucket() (string, error) { - dataBucket := os.Getenv("TEST_BUCKET") - location := os.Getenv("ENTERPRISE_LICENSE_LOCATION") - fmt.Printf("%s : dataBucket %s : location\n", os.Getenv("TEST_BUCKET"), os.Getenv("ENTERPRISE_LICENSE_LOCATION")) - item := "enterprise.lic" - file, err := os.Create(item) - if err != nil { - logf.Log.Error(err, "Failed to create license file") - } - defer file.Close() - - sess, _ := session.NewSession(&aws.Config{Region: aws.String("us-west-2")}) - downloader := s3manager.NewDownloader(sess) - numBytes, err := downloader.Download(file, - &s3.GetObjectInput{ - Bucket: aws.String(dataBucket), - Key: aws.String(location + "/" + "enterprise.lic"), - }) - if err != nil { - logf.Log.Error(err, "Failed to download license file") - } - - logf.Log.Info("Downloaded", "filename", file.Name(), "bytes", numBytes) - return file.Name(), err -} diff --git a/test/testenv/remote_index_utils.go b/test/testenv/remote_index_utils.go new file mode 100644 index 000000000..11f0344e6 --- /dev/null +++ b/test/testenv/remote_index_utils.go @@ -0,0 +1,88 @@ +package testenv + +import ( + "encoding/json" + + enterprisev1 "github.com/splunk/splunk-operator/pkg/apis/enterprise/v1beta1" + logf "sigs.k8s.io/controller-runtime/pkg/log" +) + +// dataIndexesResponse struct for /data/indexes response +type dataIndexesResponse struct { + Entry []struct { + Name string `json:"name"` + } `json:"entry"` +} + +// GetIndexOnPod get list of indexes on given pod +func GetIndexOnPod(deployment *Deployment, podName string, indexName string) bool { + stdin := "curl -ks -u admin:$(cat /mnt/splunk-secrets/password) https://localhost:8089/services/data/indexes?output_mode=json" + command := []string{"/bin/sh"} + stdout, stderr, err := deployment.PodExecCommand(podName, command, stdin, false) + if err != nil { + logf.Log.Error(err, "Failed to execute command on pod", "pod", podName, "command", command) + return false + } + logf.Log.Info("Command executed on pod", "pod", podName, "command", command, "stdin", stdin, "stdout", stdout, "stderr", stderr) + restResponse := dataIndexesResponse{} + err = json.Unmarshal([]byte(stdout), &restResponse) + if err != nil { + logf.Log.Error(err, "Failed to parse data/indexes response") + return false + } + indexFound := false + for _, entry := range restResponse.Entry { + if entry.Name == indexName { + indexFound = true + break + } + } + return indexFound +} + +// RestartSplunk Restart splunk inside the container +func RestartSplunk(deployment *Deployment, podName string) bool { + stdin := "/opt/splunk/bin/splunk restart -auth admin:$(cat /mnt/splunk-secrets/password)" + command := []string{"/bin/sh"} + stdout, stderr, err := deployment.PodExecCommand(podName, command, stdin, false) + if err != nil { + logf.Log.Error(err, "Failed to execute command on pod", "pod", podName, "command", command) + return false + } + logf.Log.Info("Command executed on pod", "pod", podName, "command", command, "stdin", stdin, "stdout", stdout, "stderr", stderr) + return true +} + +// RollHotToWarm rolls hot buckets to warm for a given index and pod +func RollHotToWarm(deployment *Deployment, podName string, indexName string) bool { + stdin := "/opt/splunk/bin/splunk _internal call /data/indexes/" + indexName + "/roll-hot-buckets admin:$(cat /mnt/splunk-secrets/password)" + command := []string{"/bin/sh"} + stdout, stderr, err := deployment.PodExecCommand(podName, command, stdin, false) + if err != nil { + logf.Log.Error(err, "Failed to execute command on pod", "pod", podName, "command", command) + return false + } + logf.Log.Info("Command executed on pod", "pod", podName, "command", command, "stdin", stdin, "stdout", stdout, "stderr", stderr) + return true +} + +// GenerateIndexVolumeSpec return VolumeSpec struct with given values +func GenerateIndexVolumeSpec(volumeName string, endpoint string, Path string, secretRef string) enterprisev1.VolumeSpec { + return enterprisev1.VolumeSpec{ + Name: volumeName, + Endpoint: endpoint, + Path: testIndexesS3Bucket, + SecretRef: secretRef, + } +} + +// GenerateIndexSpec return VolumeSpec struct with given values +func GenerateIndexSpec(indexName string, volName string) enterprisev1.IndexSpec { + return enterprisev1.IndexSpec{ + Name: indexName, + RemotePath: indexName, + IndexAndGlobalCommonSpec: enterprisev1.IndexAndGlobalCommonSpec{ + VolName: volName, + }, + } +} diff --git a/test/testenv/s3utils.go b/test/testenv/s3utils.go new file mode 100644 index 000000000..acf885bea --- /dev/null +++ b/test/testenv/s3utils.go @@ -0,0 +1,75 @@ +package testenv + +import ( + "os" + "strings" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/s3" + "github.com/aws/aws-sdk-go/service/s3/s3manager" + + logf "sigs.k8s.io/controller-runtime/pkg/log" +) + +// Set S3 Variables +var ( + s3Region = os.Getenv("S3_REGION") + testS3Bucket = os.Getenv("TEST_BUCKET") + testIndexesS3Bucket = os.Getenv("TEST_INDEXES_S3_BUCKET") + enterpriseLicenseLocation = os.Getenv("ENTERPRISE_LICENSE_LOCATION") +) + +// CheckPrefixExistsOnS3 lists object in a bucket +func CheckPrefixExistsOnS3(prefix string) bool { + dataBucket := testIndexesS3Bucket + sess, err := session.NewSession(&aws.Config{Region: aws.String(s3Region)}) + if err != nil { + logf.Log.Error(err, "Failed to create s3 session") + } + svc := s3.New(session.Must(sess, err)) + resp, err := svc.ListObjects(&s3.ListObjectsInput{ + Bucket: aws.String(dataBucket), + Prefix: aws.String(prefix), + }) + + if err != nil { + logf.Log.Error(err, "Failed to list objects on s3 bucket") + return false + } + + for _, key := range resp.Contents { + logf.Log.Info("CHECKING KEY ", "KEY", *key.Key) + if strings.Contains(*key.Key, prefix) { + logf.Log.Info("Prefix found on bucket", "Prefix", prefix, "KEY", *key.Key) + return true + } + } + + return false +} + +// DownloadFromS3Bucket downloads license file from S3 +func DownloadFromS3Bucket() (string, error) { + dataBucket := testS3Bucket + location := enterpriseLicenseLocation + item := "enterprise.lic" + file, err := os.Create(item) + if err != nil { + logf.Log.Error(err, "Failed to create license file") + } + defer file.Close() + sess, _ := session.NewSession(&aws.Config{Region: aws.String(s3Region)}) + downloader := s3manager.NewDownloader(sess) + numBytes, err := downloader.Download(file, + &s3.GetObjectInput{ + Bucket: aws.String(dataBucket), + Key: aws.String(location + "/" + "enterprise.lic"), + }) + if err != nil { + logf.Log.Error(err, "Failed to download license file") + } + + logf.Log.Info("Downloaded", "filename", file.Name(), "bytes", numBytes) + return file.Name(), err +} diff --git a/test/testenv/testenv.go b/test/testenv/testenv.go index 19178c298..d366cf586 100644 --- a/test/testenv/testenv.go +++ b/test/testenv/testenv.go @@ -18,6 +18,7 @@ import ( "context" "flag" "fmt" + "os" "time" "github.com/go-logr/logr" @@ -102,6 +103,7 @@ type TestEnv struct { SkipTeardown bool licenseFilePath string licenseCMName string + s3IndexSecret string kubeClient client.Client Log logr.Logger cleanupFuncs []cleanupFunc @@ -159,6 +161,7 @@ func NewTestEnv(name, commitHash, operatorImage, splunkImage, sparkImage, licens SkipTeardown: specifiedSkipTeardown, licenseCMName: envName, licenseFilePath: licenseFilePath, + s3IndexSecret: "splunk-s3-index-" + envName, } testenv.Log = logf.Log.WithValues("testenv", testenv.name) @@ -242,6 +245,9 @@ func (testenv *TestEnv) setup() error { return err } + // Create s3 secret object for index test + testenv.createIndexSecret() + if testenv.licenseFilePath != "" { err = testenv.createLicenseConfigMap() if err != nil { @@ -495,7 +501,6 @@ func (testenv *TestEnv) createLicenseConfigMap() error { // Create a service account config func newServiceAccount(ns string, serviceAccountName string) *corev1.ServiceAccount { - new := corev1.ServiceAccount{ TypeMeta: metav1.TypeMeta{ Kind: "ServiceAccount", @@ -528,6 +533,34 @@ func (testenv *TestEnv) CreateServiceAccount(name string) error { return nil } +// CreateIndexSecret create secret object +func (testenv *TestEnv) createIndexSecret() error { + secretName := testenv.s3IndexSecret + ns := testenv.namespace + data := map[string][]byte{"s3_access_key": []byte(os.Getenv("AWS_ACCESS_KEY_ID")), + "s3_secret_key": []byte(os.Getenv("AWS_SECRET_ACCESS_KEY"))} + secret := newSecretSpec(ns, secretName, data) + if err := testenv.GetKubeClient().Create(context.TODO(), secret); err != nil { + testenv.Log.Error(err, "Unable to create s3 index secret object") + return err + } + + testenv.pushCleanupFunc(func() error { + err := testenv.GetKubeClient().Delete(context.TODO(), secret) + if err != nil { + testenv.Log.Error(err, "Unable to delete s3 index secret object") + return err + } + return nil + }) + return nil +} + +// GetIndexSecretName return index secret object name +func (testenv *TestEnv) GetIndexSecretName() string { + return testenv.s3IndexSecret +} + // NewDeployment creates a new deployment func (testenv *TestEnv) NewDeployment(name string) (*Deployment, error) { d := Deployment{ diff --git a/test/testenv/util.go b/test/testenv/util.go index c2d7b537f..0671c4bd5 100644 --- a/test/testenv/util.go +++ b/test/testenv/util.go @@ -405,6 +405,41 @@ func newStandaloneWithLM(name, ns string, licenseMasterName string) *enterprisev return &new } +// newSecretSpec create spec for smartstore secret object +func newSecretSpec(ns string, secretName string, data map[string][]byte) *corev1.Secret { + secret := &corev1.Secret{ + TypeMeta: metav1.TypeMeta{ + Kind: "Secret", + APIVersion: "apps/v1beta1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: secretName, + Namespace: ns, + }, + Data: data, + Type: "Opaque", + } + return secret +} + +// newStandaloneWithSpec creates and initializes CR for Standalone Kind with given spec +func newStandaloneWithSpec(name, ns string, spec enterprisev1.StandaloneSpec) *enterprisev1.Standalone { + + new := enterprisev1.Standalone{ + TypeMeta: metav1.TypeMeta{ + Kind: "Standalone", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: ns, + Finalizers: []string{"enterprise.splunk.com/delete-pvc"}, + }, + + Spec: spec, + } + return &new +} + // DumpGetPods prints list of pods in the namespace func DumpGetPods(ns string) { output, err := exec.Command("kubectl", "get", "pods", "-n", ns).Output() diff --git a/test/testenv/verificationutils.go b/test/testenv/verificationutils.go index 9cf12ed36..ced6318d3 100644 --- a/test/testenv/verificationutils.go +++ b/test/testenv/verificationutils.go @@ -254,3 +254,21 @@ func VerifyServiceAccountConfiguredOnPod(deployment *Deployment, ns string, podN return strings.Contains(serviceAccount, restResponse.Spec.ServiceAccount) }, deployment.GetTimeout(), PollInterval).Should(gomega.Equal(true)) } + +// VerifyIndexFoundOnPod verify index found on a given POD +func VerifyIndexFoundOnPod(deployment *Deployment, podName string, indexName string) { + gomega.Eventually(func() bool { + indexFound := GetIndexOnPod(deployment, podName, indexName) + logf.Log.Info("Checking status of index on pod", "PODNAME", podName, "INDEX NAME", indexName, "STATUS", indexFound) + return indexFound + }, deployment.GetTimeout(), PollInterval).Should(gomega.Equal(true)) +} + +// VerifyIndexExistsOnS3 Verify Index Exists on S3 +func VerifyIndexExistsOnS3(deployment *Deployment, podName string, indexName string) { + gomega.Eventually(func() bool { + indexFound := CheckPrefixExistsOnS3(indexName) + logf.Log.Info("Checking Index on S3", "INDEX NAME", indexName, "STATUS", indexFound) + return indexFound + }, deployment.GetTimeout(), PollInterval).Should(gomega.Equal(true)) +} From a067d0e164296a36ccc1fb2d36d41806715a62ed Mon Sep 17 00:00:00 2001 From: akondur Date: Tue, 2 Feb 2021 15:23:41 -0800 Subject: [PATCH 36/43] Fixes to avoid pod recycle while scaling for all CRs except SHC --- ...rprise.splunk.com_indexerclusters_crd.yaml | 4 - ...ise.splunk.com_searchheadclusters_crd.yaml | 4 - ...rprise.splunk.com_indexerclusters_crd.yaml | 4 - ...ise.splunk.com_searchheadclusters_crd.yaml | 4 - .../splunk.v0.2.1.clusterserviceversion.yaml | 1 + .../v1beta1/indexercluster_types.go | 3 - .../v1beta1/searchheadcluster_types.go | 3 - pkg/splunk/common/names.go | 12 ++ pkg/splunk/common/util.go | 33 ++-- pkg/splunk/controller/statefulset.go | 153 ++---------------- pkg/splunk/controller/statefulset_test.go | 101 +----------- pkg/splunk/controller/util.go | 26 +++ pkg/splunk/controller/util_test.go | 50 ++++++ pkg/splunk/enterprise/indexercluster.go | 10 +- pkg/splunk/enterprise/indexercluster_test.go | 12 +- pkg/splunk/enterprise/searchheadcluster.go | 2 +- .../enterprise/searchheadcluster_test.go | 3 - pkg/splunk/test/controller.go | 10 +- 18 files changed, 148 insertions(+), 287 deletions(-) diff --git a/deploy/crds/enterprise.splunk.com_indexerclusters_crd.yaml b/deploy/crds/enterprise.splunk.com_indexerclusters_crd.yaml index d2e5189f4..9bb2bce3b 100644 --- a/deploy/crds/enterprise.splunk.com_indexerclusters_crd.yaml +++ b/deploy/crds/enterprise.splunk.com_indexerclusters_crd.yaml @@ -2349,10 +2349,6 @@ spec: description: Indicates whether the master is ready to begin servicing, based on whether it is initialized. type: boolean - skip_recheck_update: - description: Indicates if we need to recheck the revision update on - pods - type: boolean type: object type: object version: v1beta1 diff --git a/deploy/crds/enterprise.splunk.com_searchheadclusters_crd.yaml b/deploy/crds/enterprise.splunk.com_searchheadclusters_crd.yaml index 11ffcea16..4210949c2 100644 --- a/deploy/crds/enterprise.splunk.com_searchheadclusters_crd.yaml +++ b/deploy/crds/enterprise.splunk.com_searchheadclusters_crd.yaml @@ -2399,10 +2399,6 @@ spec: items: type: boolean type: array - skip_recheck_update: - description: Indicates if we need to recheck the revision update on - pods - type: boolean type: object type: object version: v1beta1 diff --git a/deploy/olm-catalog/splunk/0.2.1/enterprise.splunk.com_indexerclusters_crd.yaml b/deploy/olm-catalog/splunk/0.2.1/enterprise.splunk.com_indexerclusters_crd.yaml index d2e5189f4..9bb2bce3b 100644 --- a/deploy/olm-catalog/splunk/0.2.1/enterprise.splunk.com_indexerclusters_crd.yaml +++ b/deploy/olm-catalog/splunk/0.2.1/enterprise.splunk.com_indexerclusters_crd.yaml @@ -2349,10 +2349,6 @@ spec: description: Indicates whether the master is ready to begin servicing, based on whether it is initialized. type: boolean - skip_recheck_update: - description: Indicates if we need to recheck the revision update on - pods - type: boolean type: object type: object version: v1beta1 diff --git a/deploy/olm-catalog/splunk/0.2.1/enterprise.splunk.com_searchheadclusters_crd.yaml b/deploy/olm-catalog/splunk/0.2.1/enterprise.splunk.com_searchheadclusters_crd.yaml index 11ffcea16..4210949c2 100644 --- a/deploy/olm-catalog/splunk/0.2.1/enterprise.splunk.com_searchheadclusters_crd.yaml +++ b/deploy/olm-catalog/splunk/0.2.1/enterprise.splunk.com_searchheadclusters_crd.yaml @@ -2399,10 +2399,6 @@ spec: items: type: boolean type: array - skip_recheck_update: - description: Indicates if we need to recheck the revision update on - pods - type: boolean type: object type: object version: v1beta1 diff --git a/deploy/olm-catalog/splunk/0.2.1/splunk.v0.2.1.clusterserviceversion.yaml b/deploy/olm-catalog/splunk/0.2.1/splunk.v0.2.1.clusterserviceversion.yaml index 1888cd640..e197d8eaa 100644 --- a/deploy/olm-catalog/splunk/0.2.1/splunk.v0.2.1.clusterserviceversion.yaml +++ b/deploy/olm-catalog/splunk/0.2.1/splunk.v0.2.1.clusterserviceversion.yaml @@ -205,6 +205,7 @@ spec: - secrets - pods - pods/exec + - serviceaccounts verbs: - create - delete diff --git a/pkg/apis/enterprise/v1beta1/indexercluster_types.go b/pkg/apis/enterprise/v1beta1/indexercluster_types.go index f36b2372c..ca7cb6fe4 100644 --- a/pkg/apis/enterprise/v1beta1/indexercluster_types.go +++ b/pkg/apis/enterprise/v1beta1/indexercluster_types.go @@ -95,9 +95,6 @@ type IndexerClusterStatus struct { // Indicates if the cluster is in maintenance mode. MaintenanceMode bool `json:"maintenance_mode"` - // Indicates if we need to recheck the revision update on pods - SkipRecheckUpdate bool `json:"skip_recheck_update"` - // status of each indexer cluster peer Peers []IndexerClusterMemberStatus `json:"peers"` } diff --git a/pkg/apis/enterprise/v1beta1/searchheadcluster_types.go b/pkg/apis/enterprise/v1beta1/searchheadcluster_types.go index 4b23291f3..b9f15cb75 100644 --- a/pkg/apis/enterprise/v1beta1/searchheadcluster_types.go +++ b/pkg/apis/enterprise/v1beta1/searchheadcluster_types.go @@ -109,9 +109,6 @@ type SearchHeadClusterStatus struct { // Indicates resource version of namespace scoped secret NamespaceSecretResourceVersion string `json:"namespace_scoped_secret_resource_version"` - // Indicates if we need to recheck the revision update on pods - SkipRecheckUpdate bool `json:"skip_recheck_update"` - // status of each search head cluster member Members []SearchHeadClusterMemberStatus `json:"members"` } diff --git a/pkg/splunk/common/names.go b/pkg/splunk/common/names.go index ad7618398..bb0c23952 100644 --- a/pkg/splunk/common/names.go +++ b/pkg/splunk/common/names.go @@ -58,6 +58,18 @@ const ( // DefaultVarVolumeStorageCapacity represents default storage capacity for var volume DefaultVarVolumeStorageCapacity = "100Gi" + + // SortFieldContainerPort represents field name ContainerPort for sorting + SortFieldContainerPort = "ContainerPort" + + // SortFieldPort represents field name Port for sorting + SortFieldPort = "Port" + + // SortFieldName represents field name Name for sorting + SortFieldName = "Name" + + // SortFieldKey represents field name Key for sorting + SortFieldKey = "Key" ) // GetVersionedSecretName returns a versioned secret name diff --git a/pkg/splunk/common/util.go b/pkg/splunk/common/util.go index 228cff8df..8b33cc0c3 100644 --- a/pkg/splunk/common/util.go +++ b/pkg/splunk/common/util.go @@ -130,37 +130,37 @@ func SortServicePorts(ports []corev1.ServicePort) []corev1.ServicePort { // It returns true if there are material differences between them, or false otherwise. // TODO: could use refactoring; lots of boilerplate copy-pasta here func CompareContainerPorts(a []corev1.ContainerPort, b []corev1.ContainerPort) bool { - return sortAndCompareSlices(a, b, "ContainerPort") + return sortAndCompareSlices(a, b, SortFieldContainerPort) } // CompareServicePorts is a generic comparer of two Kubernetes ServicePorts. // It returns true if there are material differences between them, or false otherwise. // TODO: could use refactoring; lots of boilerplate copy-pasta here func CompareServicePorts(a []corev1.ServicePort, b []corev1.ServicePort) bool { - return sortAndCompareSlices(a, b, "Port") + return sortAndCompareSlices(a, b, SortFieldPort) } // CompareEnvs is a generic comparer of two Kubernetes Env variables. // It returns true if there are material differences between them, or false otherwise. func CompareEnvs(a []corev1.EnvVar, b []corev1.EnvVar) bool { - return sortAndCompareSlices(a, b, "Name") + return sortAndCompareSlices(a, b, SortFieldName) } // CompareTolerations compares the 2 list of tolerations func CompareTolerations(a []corev1.Toleration, b []corev1.Toleration) bool { - return sortAndCompareSlices(a, b, "Key") + return sortAndCompareSlices(a, b, SortFieldKey) } // CompareVolumes is a generic comparer of two Kubernetes Volumes. // It returns true if there are material differences between them, or false otherwise. func CompareVolumes(a []corev1.Volume, b []corev1.Volume) bool { - return sortAndCompareSlices(a, b, "Name") + return sortAndCompareSlices(a, b, SortFieldName) } // CompareVolumeMounts is a generic comparer of two Kubernetes VolumeMounts. // It returns true if there are material differences between them, or false otherwise. func CompareVolumeMounts(a []corev1.VolumeMount, b []corev1.VolumeMount) bool { - return sortAndCompareSlices(a, b, "Name") + return sortAndCompareSlices(a, b, SortFieldName) } // CompareByMarshall compares two Kubernetes objects by marshalling them to JSON. @@ -424,6 +424,21 @@ func sortAndCompareSlices(a interface{}, b interface{}, keyName string) bool { return true } + // Sort slices + SortSlice(a, keyName) + SortSlice(b, keyName) + + return !reflect.DeepEqual(a, b) +} + +// SortSlice sorts a slice of any kind by keyName +func SortSlice(a interface{}, keyName string) { + aType := reflect.TypeOf(a) + + if aType.Kind() != reflect.Slice { + panic(fmt.Sprintf("SortSlice can only be used on slices: Kind(a)=%v", aType.Kind())) + } + sortFunc := func(s interface{}, i, j int) bool { sValue := reflect.ValueOf(s) @@ -447,10 +462,4 @@ func sortAndCompareSlices(a interface{}, b interface{}, keyName string) bool { sort.Slice(a, func(i, j int) bool { return sortFunc(a, i, j) }) - - sort.Slice(b, func(i, j int) bool { - return sortFunc(b, i, j) - }) - - return !reflect.DeepEqual(a, b) } diff --git a/pkg/splunk/controller/statefulset.go b/pkg/splunk/controller/statefulset.go index fd7c93ccf..b70814168 100644 --- a/pkg/splunk/controller/statefulset.go +++ b/pkg/splunk/controller/statefulset.go @@ -34,9 +34,8 @@ 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) - skipRecheckUpdate := false if err == nil && phase == splcommon.PhaseReady { - phase, err = UpdateStatefulSetPods(client, statefulSet, mgr, desiredReplicas, &skipRecheckUpdate) + phase, err = UpdateStatefulSetPods(client, statefulSet, mgr, desiredReplicas) } return phase, err } @@ -63,6 +62,16 @@ func ApplyStatefulSet(c splcommon.ControllerClient, revised *appsv1.StatefulSet) err := c.Get(context.TODO(), namespacedName, ¤t) if err != nil { + // In every reconcile, the statefulSet spec created by the operator is compared + // against the one stored in etcd. While comparing the two specs, for the fields + // represented by slices(ports, volume mounts etc..) the order of the elements is + // important i.e any change in order followed by an update of statefulSet will cause + // a change in the UpdatedRevision field in the StatefulSpec. This inturn triggers + // a pod recycle unnecessarily. To avoid the same, sort the slices during the + // statefulSet creation. + // Note: During the update scenario below, MergePodUpdates takes care of sorting. + SortStatefulSetSlices(&revised.Spec.Template.Spec, revised.GetObjectMeta().GetName()) + // no StatefulSet exists -> just create a new one err = splutil.CreateResource(c, revised) return splcommon.PhasePending, err @@ -86,105 +95,8 @@ func ApplyStatefulSet(c splcommon.ControllerClient, revised *appsv1.StatefulSet) return splcommon.PhaseReady, nil } -// UpdatePodRevisionHash updates the controller-revision-hash label on pods. -func UpdatePodRevisionHash(c splcommon.ControllerClient, statefulSet *appsv1.StatefulSet, readyReplicas int32) error { - scopedLog := log.WithName("updatePodRevisionHash").WithValues( - "name", statefulSet.GetObjectMeta().GetName(), - "namespace", statefulSet.GetObjectMeta().GetNamespace()) - - namespacedName := types.NamespacedName{Namespace: statefulSet.GetNamespace(), Name: statefulSet.GetName()} - var current appsv1.StatefulSet - - err := c.Get(context.TODO(), namespacedName, ¤t) - if err != nil { - scopedLog.Error(err, "Unable to Get statefulset", "statefulset", statefulSet.GetName()) - return err - } - - for n := readyReplicas - 1; n >= 0; n-- { - // get Pod - podName := fmt.Sprintf("%s-%d", current.GetName(), n) - namespacedName := types.NamespacedName{Namespace: current.GetNamespace(), Name: podName} - var pod corev1.Pod - err := c.Get(context.TODO(), namespacedName, &pod) - if err != nil { - scopedLog.Error(err, "Unable to find Pod", "podName", podName) - return err - } - if pod.Status.Phase != corev1.PodRunning || len(pod.Status.ContainerStatuses) == 0 || pod.Status.ContainerStatuses[0].Ready != true { - scopedLog.Error(err, "Waiting for Pod to become ready", "podName", podName) - return err - } - - labels := pod.GetLabels() - revisionHash := labels["controller-revision-hash"] - // update pod controller revision hash - if current.Status.UpdateRevision != "" && current.Status.UpdateRevision != revisionHash { - // update controller-revision-hash label for pod with statefulset update revision - labels["controller-revision-hash"] = current.Status.UpdateRevision - pod.SetLabels(labels) - err = splutil.UpdateResource(c, &pod) - if err != nil { - scopedLog.Error(err, "Unable to update controller-revision-hash label for pod", "podName", podName) - return err - } - scopedLog.Info("Updated controller-revision-hash label for pod", "podName", podName, "revision", current.Status.UpdateRevision) - } - } - - scopedLog.Info("Updated controller-revision-hash label for all pods") - return nil -} - -// isRevisionUpdateSuccessful checks if current revision is different from updated revision -func isRevisionUpdateSuccessful(c splcommon.ControllerClient, statefulSet *appsv1.StatefulSet) bool { - scopedLog := log.WithName("isRevisionUpdateSuccessful").WithValues( - "name", statefulSet.GetObjectMeta().GetName(), - "namespace", statefulSet.GetObjectMeta().GetNamespace()) - - namespacedName := types.NamespacedName{Namespace: statefulSet.GetNamespace(), Name: statefulSet.GetName()} - var current appsv1.StatefulSet - - err := c.Get(context.TODO(), namespacedName, ¤t) - if err != nil { - scopedLog.Error(err, "Unable to Get statefulset", "statefulset", statefulSet.GetName()) - return false - } - - // Check if current revision is different from update revision - if current.Status.CurrentRevision == current.Status.UpdateRevision { - scopedLog.Error(err, "Statefulset UpdateRevision not updated yet") - return false - } - - return true -} - -// checkAndUpdatePodRevision updates the pod revision hash labels on pods if statefulset update was successful -func checkAndUpdatePodRevision(c splcommon.ControllerClient, statefulSet *appsv1.StatefulSet, readyReplicas int32, skipRecheckUpdate *bool) error { - scopedLog := log.WithName("checkAndUpdatePodRevision").WithValues( - "name", statefulSet.GetObjectMeta().GetName(), - "namespace", statefulSet.GetObjectMeta().GetNamespace()) - var err error - if !isRevisionUpdateSuccessful(c, statefulSet) { - scopedLog.Error(err, "Statefulset not updated yet") - *skipRecheckUpdate = false - return err - } - // update the controller-revision-hash label on pods to - // to avoid unnecessary recycle of pods - err = UpdatePodRevisionHash(c, statefulSet, readyReplicas) - if err != nil { - scopedLog.Error(err, "Unable to update pod-revision-hash for the pods") - *skipRecheckUpdate = false - return err - } - *skipRecheckUpdate = true - return nil -} - // UpdateStatefulSetPods manages scaling and config updates for StatefulSets -func UpdateStatefulSetPods(c splcommon.ControllerClient, statefulSet *appsv1.StatefulSet, mgr splcommon.StatefulSetPodManager, desiredReplicas int32, skipRecheckUpdate *bool) (splcommon.Phase, error) { +func UpdateStatefulSetPods(c splcommon.ControllerClient, statefulSet *appsv1.StatefulSet, mgr splcommon.StatefulSetPodManager, desiredReplicas int32) (splcommon.Phase, error) { scopedLog := log.WithName("UpdateStatefulSetPods").WithValues( "name", statefulSet.GetObjectMeta().GetName(), "namespace", statefulSet.GetObjectMeta().GetNamespace()) @@ -195,25 +107,11 @@ func UpdateStatefulSetPods(c splcommon.ControllerClient, statefulSet *appsv1.Sta if readyReplicas < replicas { scopedLog.Info("Waiting for pods to become ready") if readyReplicas > 0 { - if !*skipRecheckUpdate { - err := checkAndUpdatePodRevision(c, statefulSet, readyReplicas, skipRecheckUpdate) - if !*skipRecheckUpdate || err != nil { - scopedLog.Error(err, "Unable to update pod-revision-hash for the pods") - return splcommon.PhaseError, err - } - } return splcommon.PhaseScalingUp, nil } return splcommon.PhasePending, nil } else if readyReplicas > replicas { scopedLog.Info("Waiting for scale down to complete") - if !*skipRecheckUpdate { - err := checkAndUpdatePodRevision(c, statefulSet, readyReplicas-1, skipRecheckUpdate) - if !*skipRecheckUpdate || err != nil { - scopedLog.Error(err, "Unable to update pod-revision-hash for the pods") - return splcommon.PhaseError, err - } - } return splcommon.PhaseScalingDown, nil } @@ -224,22 +122,7 @@ func UpdateStatefulSetPods(c splcommon.ControllerClient, statefulSet *appsv1.Sta // scale up StatefulSet to match desiredReplicas scopedLog.Info("Scaling replicas up", "replicas", desiredReplicas) *statefulSet.Spec.Replicas = desiredReplicas - err := splutil.UpdateResource(c, statefulSet) - if err != nil { - scopedLog.Error(err, "Unable to update statefulset") - return splcommon.PhaseError, err - } - - // Check if the revision was updated successfully for Statefulset. - // It so can happen that it may take few seconds for the update to be - // reflected in the resource. In that case, just return from here and - // check the status back in the next reconcile loop. - err = checkAndUpdatePodRevision(c, statefulSet, readyReplicas, skipRecheckUpdate) - if !*skipRecheckUpdate || err != nil { - scopedLog.Error(err, "Unable to update pod-revision-hash for the pods") - return splcommon.PhaseError, err - } - return splcommon.PhaseScalingUp, err + return splcommon.PhaseScalingUp, splutil.UpdateResource(c, statefulSet) } // check for scaling down @@ -266,16 +149,6 @@ func UpdateStatefulSetPods(c splcommon.ControllerClient, statefulSet *appsv1.Sta return splcommon.PhaseError, err } - // Check if the revision was updated successfully for Statefulset. - // It so can happen that it may take few seconds for the update to be - // reflected in the resource. In that case, just return from here and - // check the status back in the next reconcile loop. - err = checkAndUpdatePodRevision(c, statefulSet, readyReplicas-1, skipRecheckUpdate) - if !*skipRecheckUpdate || err != nil { - scopedLog.Error(err, "Unable to update pod-revision-hash for the pods") - return splcommon.PhaseError, err - } - // delete PVCs used by the pod so that a future scale up will have clean state for _, vol := range statefulSet.Spec.VolumeClaimTemplates { namespacedName := types.NamespacedName{ diff --git a/pkg/splunk/controller/statefulset_test.go b/pkg/splunk/controller/statefulset_test.go index 494cd00eb..0936e1f00 100644 --- a/pkg/splunk/controller/statefulset_test.go +++ b/pkg/splunk/controller/statefulset_test.go @@ -64,8 +64,7 @@ func updateStatefulSetPodsTester(t *testing.T, mgr splcommon.StatefulSetPodManag // initialize client c := spltest.NewMockClient() c.AddObjects(initObjects) - skipRecheckUpdate := false - phase, err := UpdateStatefulSetPods(c, statefulSet, mgr, desiredReplicas, &skipRecheckUpdate) + phase, err := UpdateStatefulSetPods(c, statefulSet, mgr, desiredReplicas) return phase, err } @@ -113,35 +112,19 @@ func TestUpdateStatefulSetPods(t *testing.T) { t.Errorf("UpdateStatefulSetPods should not have returned error=%s with phase=%s", err, phase) } - // Check the scenario where UpdatePodRevisionHash should return error when Pod is not added to client. - phase, err = updateStatefulSetPodsTester(t, &mgr, statefulSet, 2 /*desiredReplicas*/, statefulSet) - if err == nil && phase != splcommon.PhaseError { - t.Errorf("UpdateStatefulSetPods should have returned error or phase should have been PhaseError, but we got phase=%s", phase) - } - - replicas = 3 - statefulSet.Status.ReadyReplicas = 3 - statefulSet.Spec.Replicas = &replicas - // Check the scenario where UpdatePodRevisionHash should return error when Pod is not added to client. - phase, err = updateStatefulSetPodsTester(t, &mgr, statefulSet, 1 /*desiredReplicas*/, statefulSet) - if err == nil && phase != splcommon.PhaseError { - t.Errorf("UpdateStatefulSetPods should have returned error or phase should have been PhaseError, but we got phase=%s", phase) - } - // readyReplicas < replicas replicas = 3 statefulSet.Status.ReadyReplicas = 2 statefulSet.Spec.Replicas = &replicas - // Check the scenario where UpdatePodRevisionHash should return error when readyReplicas < replicas and Pod is not found phase, err = updateStatefulSetPodsTester(t, &mgr, statefulSet, 1 /*desiredReplicas*/, statefulSet, pod) - if err == nil && phase != splcommon.PhaseError { - t.Errorf("UpdateStatefulSetPods should have returned error or phase should have been PhaseError, but we got phase=%s", phase) + if err != nil && phase != splcommon.PhaseUpdating { + t.Errorf("UpdateStatefulSetPods should not have returned error=%s with phase=%s", err, phase) } // CurrentRevision = UpdateRevision statefulSet.Status.CurrentRevision = "v1" phase, err = updateStatefulSetPodsTester(t, &mgr, statefulSet, 1 /*desiredReplicas*/, statefulSet, pod) - if err == nil && phase != splcommon.PhaseError { + if err == nil && phase != splcommon.PhaseScalingUp { t.Errorf("UpdateStatefulSetPods should have returned error or phase should have been PhaseError, but we got phase=%s", phase) } @@ -150,89 +133,17 @@ func TestUpdateStatefulSetPods(t *testing.T) { statefulSet.Status.ReadyReplicas = 3 statefulSet.Spec.Replicas = &replicas statefulSet.Status.CurrentRevision = "" - // Check the scenario where UpdatePodRevisionHash should return error when readyReplicas > replicas and Pod is not found phase, err = updateStatefulSetPodsTester(t, &mgr, statefulSet, 1 /*desiredReplicas*/, statefulSet, pod) - if err == nil && phase != splcommon.PhaseError { + if err == nil && phase != splcommon.PhaseScalingDown { t.Errorf("UpdateStatefulSetPods should have returned error or phase should have been PhaseError, but we got phase=%s", phase) } // CurrentRevision = UpdateRevision statefulSet.Status.CurrentRevision = "v1" phase, err = updateStatefulSetPodsTester(t, &mgr, statefulSet, 1 /*desiredReplicas*/, statefulSet, pod) - if err == nil && phase != splcommon.PhaseError { - t.Errorf("UpdateStatefulSetPods should have returned error or phase should have been PhaseError, but we got phase=%s", phase) - } - - // Check the scenario where UpdatePodRevisionHash should return error when statefulset is not added to client. - phase, err = updateStatefulSetPodsTester(t, &mgr, statefulSet, 2 /*desiredReplicas*/, pod) - if err == nil && phase != splcommon.PhaseError { + if err == nil && phase != splcommon.PhaseScalingDown { t.Errorf("UpdateStatefulSetPods should have returned error or phase should have been PhaseError, but we got phase=%s", phase) } - -} - -func updatePodRevisionHashTester(t *testing.T, statefulSet *appsv1.StatefulSet, replicas int32, initObjects ...runtime.Object) error { - c := spltest.NewMockClient() - c.AddObjects(initObjects) - return UpdatePodRevisionHash(c, statefulSet, replicas) -} - -func TestUpdatePodRevisionHash(t *testing.T) { - var replicas int32 = 1 - statefulSet := &appsv1.StatefulSet{ - ObjectMeta: metav1.ObjectMeta{ - Name: "splunk-stack1", - Namespace: "test", - }, - Spec: appsv1.StatefulSetSpec{ - Replicas: &replicas, - VolumeClaimTemplates: []corev1.PersistentVolumeClaim{ - {ObjectMeta: metav1.ObjectMeta{Name: "pvc-etc", Namespace: "test"}}, - {ObjectMeta: metav1.ObjectMeta{Name: "pvc-var", Namespace: "test"}}, - }, - }, - Status: appsv1.StatefulSetStatus{ - Replicas: replicas, - ReadyReplicas: replicas, - UpdatedReplicas: replicas, - UpdateRevision: "v1", - }, - } - pod := &corev1.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Name: "splunk-stack1-0", - Namespace: "test", - Labels: map[string]string{ - "controller-revision-hash": "v0", - }, - }, - Status: corev1.PodStatus{ - Phase: corev1.PodRunning, - ContainerStatuses: []corev1.ContainerStatus{ - {Ready: true}, - }, - }, - } - - err := updatePodRevisionHashTester(t, statefulSet, replicas, statefulSet, pod) - if err != nil { - t.Errorf("UpdatePodRevisionHash should not have returned error=%s", err) - } - - // Test the negative case where phase != corev1.PodRunning - pod.Status.Phase = corev1.PodPending - err = updatePodRevisionHashTester(t, statefulSet, replicas, statefulSet, pod) - if err != nil { - t.Errorf("UpdatePodRevisionHash should not have returned error=%s", err) - } - - // Test invalid pod name splunk-stack1-1 - pod.Status.Phase = corev1.PodRunning - replicas = 2 - err = updatePodRevisionHashTester(t, statefulSet, replicas, statefulSet, pod) - if err == nil { - t.Errorf("UpdatePodRevisionHash should have returned error") - } } func TestSetStatefulSetOwnerRef(t *testing.T) { diff --git a/pkg/splunk/controller/util.go b/pkg/splunk/controller/util.go index f31ac7364..e5c20cef4 100644 --- a/pkg/splunk/controller/util.go +++ b/pkg/splunk/controller/util.go @@ -189,6 +189,32 @@ func MergePodSpecUpdates(current *corev1.PodSpec, revised *corev1.PodSpec, name return result } +// SortStatefulSetSlices sorts required slices in a statefulSet +func SortStatefulSetSlices(current *corev1.PodSpec, name string) error { + scopedLog := log.WithName("SortStatefulSetSlices").WithValues("name", name) + + // Sort tolerations + splcommon.SortSlice(current.Tolerations, splcommon.SortFieldKey) + + // Sort volumes + splcommon.SortSlice(current.Volumes, splcommon.SortFieldName) + + // Sort slices inside container specs + for idx := range current.Containers { + // Sort container ports + splcommon.SortSlice(current.Containers[idx].Ports, splcommon.SortFieldContainerPort) + + // Sort VolumeMounts + splcommon.SortSlice(current.Containers[idx].VolumeMounts, splcommon.SortFieldName) + + // Sort env variables + splcommon.SortSlice(current.Containers[idx].Env, splcommon.SortFieldName) + } + scopedLog.Info("Successfully sorted slices in statefulSet") + + return nil +} + // MergeServiceSpecUpdates merges the current and revised spec of the service object func MergeServiceSpecUpdates(current *corev1.ServiceSpec, revised *corev1.ServiceSpec, name string) bool { scopedLog := log.WithName("MergeServiceSpecUpdates").WithValues("name", name) diff --git a/pkg/splunk/controller/util_test.go b/pkg/splunk/controller/util_test.go index 0314f979a..013dc3d5f 100644 --- a/pkg/splunk/controller/util_test.go +++ b/pkg/splunk/controller/util_test.go @@ -199,3 +199,53 @@ func TestMergeServiceSpecUpdates(t *testing.T) { matcher = func() bool { return current.ExternalTrafficPolicy == revised.ExternalTrafficPolicy } svcUpdateTester("Service ExternalTrafficPolicy changed") } + +func TestSortStatefulSetSlices(t *testing.T) { + var unsorted, sorted corev1.PodSpec + matcher := func() bool { return false } + + sortTester := func(sortSlice string) { + SortStatefulSetSlices(&unsorted, sortSlice) + if !matcher() { + t.Errorf("SortStatefulSetSlices() didn't sort %s", sortSlice) + } + } + + // Check volume sorting + unsorted.Volumes = []corev1.Volume{{Name: "bVolume"}, {Name: "aVolume"}} + sorted.Volumes = []corev1.Volume{{Name: "aVolume"}, {Name: "bVolume"}} + matcher = func() bool { return reflect.DeepEqual(sorted.Volumes, unsorted.Volumes) } + sortTester("Volumes") + + // Check tolerations + unsorted.Tolerations = []corev1.Toleration{{Key: "bKey"}, {Key: "aKey"}} + sorted.Tolerations = []corev1.Toleration{{Key: "aKey"}, {Key: "bKey"}} + matcher = func() bool { return reflect.DeepEqual(sorted.Tolerations, unsorted.Tolerations) } + sortTester("Tolerations") + + // Create a container + sorted.Containers = make([]corev1.Container, 1) + unsorted.Containers = make([]corev1.Container, 1) + + // Check container port sorting + unsorted.Containers[0].Ports = []corev1.ContainerPort{{ContainerPort: 8080}, {ContainerPort: 8000}} + sorted.Containers[0].Ports = []corev1.ContainerPort{{ContainerPort: 8000}, {ContainerPort: 8080}} + matcher = func() bool { return reflect.DeepEqual(sorted.Containers[0].Ports, unsorted.Containers[0].Ports) } + sortTester("Container Ports") + + // Check volume mount sorting + unsorted.Containers[0].VolumeMounts = []corev1.VolumeMount{{Name: "bVolumeMount"}, {Name: "aVolumeMount"}} + sorted.Containers[0].VolumeMounts = []corev1.VolumeMount{{Name: "aVolumeMount"}, {Name: "bVolumeMount"}} + matcher = func() bool { + return reflect.DeepEqual(sorted.Containers[0].VolumeMounts, unsorted.Containers[0].VolumeMounts) + } + sortTester("Volume mounts") + + // Check env var sorting + unsorted.Containers[0].Env = []corev1.EnvVar{{Name: "SPLUNK_ROLE", Value: "SPLUNK_INDEXER"}, {Name: "DECLARATIVE_ADMIN_PASSWORD", Value: "true"}} + sorted.Containers[0].Env = []corev1.EnvVar{{Name: "DECLARATIVE_ADMIN_PASSWORD", Value: "true"}, {Name: "SPLUNK_ROLE", Value: "SPLUNK_INDEXER"}} + matcher = func() bool { + return reflect.DeepEqual(sorted.Containers[0].Env, unsorted.Containers[0].Env) + } + sortTester("Env variables") +} diff --git a/pkg/splunk/enterprise/indexercluster.go b/pkg/splunk/enterprise/indexercluster.go index 6068415fc..61539ebc3 100644 --- a/pkg/splunk/enterprise/indexercluster.go +++ b/pkg/splunk/enterprise/indexercluster.go @@ -371,7 +371,7 @@ func (mgr *indexerClusterPodManager) Update(c splcommon.ControllerClient, statef } // manage scaling and updates - return splctrl.UpdateStatefulSetPods(c, statefulSet, mgr, desiredReplicas, &mgr.cr.Status.SkipRecheckUpdate) + return splctrl.UpdateStatefulSetPods(c, statefulSet, mgr, desiredReplicas) } // PrepareScaleDown for indexerClusterPodManager prepares indexer pod to be removed via scale down event; it returns true when ready @@ -575,7 +575,13 @@ func (mgr *indexerClusterPodManager) updateStatus(statefulSet *appsv1.StatefulSe // getIndexerStatefulSet returns a Kubernetes StatefulSet object for Splunk Enterprise indexers. func getIndexerStatefulSet(client splcommon.ControllerClient, cr *enterprisev1.IndexerCluster) (*appsv1.StatefulSet, error) { - return getSplunkStatefulSet(client, cr, &cr.Spec.CommonSplunkSpec, SplunkIndexer, cr.Spec.Replicas, getIndexerExtraEnv(cr, cr.Spec.Replicas)) + // Note: SPLUNK_INDEXER_URL is not used by the indexer pod containers, + // hence avoided the call to getIndexerExtraEnv. + // If other indexer CR specific env variables are required: + // 1. Introduce the new env variables in the function getIndexerExtraEnv + // 2. Avoid SPLUNK_INDEXER_URL in getIndexerExtraEnv for idxc CR + // 3. Re-introduce the call to getIndexerExtraEnv here. + return getSplunkStatefulSet(client, cr, &cr.Spec.CommonSplunkSpec, SplunkIndexer, cr.Spec.Replicas, make([]corev1.EnvVar, 0)) } // validateIndexerClusterSpec checks validity and makes default updates to a IndexerClusterSpec, and returns error if something is wrong. diff --git a/pkg/splunk/enterprise/indexercluster_test.go b/pkg/splunk/enterprise/indexercluster_test.go index c4d632669..7132b14cf 100644 --- a/pkg/splunk/enterprise/indexercluster_test.go +++ b/pkg/splunk/enterprise/indexercluster_test.go @@ -604,7 +604,7 @@ func TestIndexerClusterPodManager(t *testing.T) { {MetaName: "*v1.PersistentVolumeClaim-test-pvc-etc-splunk-stack1-1"}, {MetaName: "*v1.PersistentVolumeClaim-test-pvc-var-splunk-stack1-1"}, } - wantCalls = map[string][]spltest.MockFuncCall{"Get": {funcCalls[0], funcCalls[1], funcCalls[3], funcCalls[3], funcCalls[0], funcCalls[0], funcCalls[4]}, "Create": {funcCalls[1]}, "Delete": pvcCalls, "Update": {funcCalls[0], funcCalls[4]}} + wantCalls = map[string][]spltest.MockFuncCall{"Get": {funcCalls[0], funcCalls[1], funcCalls[3], funcCalls[3]}, "Create": {funcCalls[1]}, "Delete": pvcCalls, "Update": {funcCalls[0]}} wantCalls["Get"] = append(wantCalls["Get"], pvcCalls...) pvcList := []*corev1.PersistentVolumeClaim{ {ObjectMeta: metav1.ObjectMeta{Name: "pvc-etc-splunk-stack1-1", Namespace: "test"}}, @@ -1022,18 +1022,18 @@ func TestGetIndexerStatefulSet(t *testing.T) { } cr.Spec.Replicas = 0 - test(`{"kind":"StatefulSet","apiVersion":"apps/v1","metadata":{"name":"splunk-stack1-indexer","namespace":"test","creationTimestamp":null,"ownerReferences":[{"apiVersion":"","kind":"","name":"stack1","uid":"","controller":true}]},"spec":{"replicas":1,"selector":{"matchLabels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-master1-indexer"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-master1-indexer"},"annotations":{"traffic.sidecar.istio.io/excludeOutboundPorts":"8089,8191,9997,7777,9000,17000,17500,19000","traffic.sidecar.istio.io/includeInboundPorts":"8000,8088"}},"spec":{"volumes":[{"name":"mnt-splunk-secrets","secret":{"secretName":"splunk-stack1-indexer-secret-v1","defaultMode":420}}],"containers":[{"name":"splunk","image":"splunk/splunk","ports":[{"name":"http-splunkweb","containerPort":8000,"protocol":"TCP"},{"name":"http-hec","containerPort":8088,"protocol":"TCP"},{"name":"https-splunkd","containerPort":8089,"protocol":"TCP"},{"name":"tcp-s2s","containerPort":9997,"protocol":"TCP"}],"env":[{"name":"SPLUNK_HOME","value":"/opt/splunk"},{"name":"SPLUNK_START_ARGS","value":"--accept-license"},{"name":"SPLUNK_DEFAULTS_URL","value":"/mnt/splunk-secrets/default.yml"},{"name":"SPLUNK_HOME_OWNERSHIP_ENFORCEMENT","value":"false"},{"name":"SPLUNK_ROLE","value":"splunk_indexer"},{"name":"SPLUNK_DECLARATIVE_ADMIN_PASSWORD","value":"true"},{"name":"SPLUNK_INDEXER_URL","value":"splunk-stack1-indexer-0.splunk-stack1-indexer-headless.test.svc.cluster.local"},{"name":"SPLUNK_CLUSTER_MASTER_URL","value":"splunk-master1-cluster-master-service"}],"resources":{"limits":{"cpu":"4","memory":"8Gi"},"requests":{"cpu":"100m","memory":"512Mi"}},"volumeMounts":[{"name":"pvc-etc","mountPath":"/opt/splunk/etc"},{"name":"pvc-var","mountPath":"/opt/splunk/var"},{"name":"mnt-splunk-secrets","mountPath":"/mnt/splunk-secrets"}],"livenessProbe":{"exec":{"command":["/sbin/checkstate.sh"]},"initialDelaySeconds":300,"timeoutSeconds":30,"periodSeconds":30},"readinessProbe":{"exec":{"command":["/bin/grep","started","/opt/container_artifact/splunk-container.state"]},"initialDelaySeconds":10,"timeoutSeconds":5,"periodSeconds":5},"imagePullPolicy":"IfNotPresent"}],"securityContext":{"runAsUser":41812,"fsGroup":41812},"affinity":{"podAntiAffinity":{"preferredDuringSchedulingIgnoredDuringExecution":[{"weight":100,"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app.kubernetes.io/instance","operator":"In","values":["splunk-stack1-indexer"]}]},"topologyKey":"kubernetes.io/hostname"}}]}},"schedulerName":"default-scheduler"}},"volumeClaimTemplates":[{"metadata":{"name":"pvc-etc","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-master1-indexer"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"10Gi"}}},"status":{}},{"metadata":{"name":"pvc-var","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-master1-indexer"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"100Gi"}}},"status":{}}],"serviceName":"splunk-stack1-indexer-headless","podManagementPolicy":"Parallel","updateStrategy":{"type":"OnDelete"}},"status":{"replicas":0}}`) + test(`{"kind":"StatefulSet","apiVersion":"apps/v1","metadata":{"name":"splunk-stack1-indexer","namespace":"test","creationTimestamp":null,"ownerReferences":[{"apiVersion":"","kind":"","name":"stack1","uid":"","controller":true}]},"spec":{"replicas":1,"selector":{"matchLabels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-master1-indexer"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-master1-indexer"},"annotations":{"traffic.sidecar.istio.io/excludeOutboundPorts":"8089,8191,9997,7777,9000,17000,17500,19000","traffic.sidecar.istio.io/includeInboundPorts":"8000,8088"}},"spec":{"volumes":[{"name":"mnt-splunk-secrets","secret":{"secretName":"splunk-stack1-indexer-secret-v1","defaultMode":420}}],"containers":[{"name":"splunk","image":"splunk/splunk","ports":[{"name":"http-splunkweb","containerPort":8000,"protocol":"TCP"},{"name":"http-hec","containerPort":8088,"protocol":"TCP"},{"name":"https-splunkd","containerPort":8089,"protocol":"TCP"},{"name":"tcp-s2s","containerPort":9997,"protocol":"TCP"}],"env":[{"name":"SPLUNK_HOME","value":"/opt/splunk"},{"name":"SPLUNK_START_ARGS","value":"--accept-license"},{"name":"SPLUNK_DEFAULTS_URL","value":"/mnt/splunk-secrets/default.yml"},{"name":"SPLUNK_HOME_OWNERSHIP_ENFORCEMENT","value":"false"},{"name":"SPLUNK_ROLE","value":"splunk_indexer"},{"name":"SPLUNK_DECLARATIVE_ADMIN_PASSWORD","value":"true"},{"name":"SPLUNK_CLUSTER_MASTER_URL","value":"splunk-master1-cluster-master-service"}],"resources":{"limits":{"cpu":"4","memory":"8Gi"},"requests":{"cpu":"100m","memory":"512Mi"}},"volumeMounts":[{"name":"pvc-etc","mountPath":"/opt/splunk/etc"},{"name":"pvc-var","mountPath":"/opt/splunk/var"},{"name":"mnt-splunk-secrets","mountPath":"/mnt/splunk-secrets"}],"livenessProbe":{"exec":{"command":["/sbin/checkstate.sh"]},"initialDelaySeconds":300,"timeoutSeconds":30,"periodSeconds":30},"readinessProbe":{"exec":{"command":["/bin/grep","started","/opt/container_artifact/splunk-container.state"]},"initialDelaySeconds":10,"timeoutSeconds":5,"periodSeconds":5},"imagePullPolicy":"IfNotPresent"}],"securityContext":{"runAsUser":41812,"fsGroup":41812},"affinity":{"podAntiAffinity":{"preferredDuringSchedulingIgnoredDuringExecution":[{"weight":100,"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app.kubernetes.io/instance","operator":"In","values":["splunk-stack1-indexer"]}]},"topologyKey":"kubernetes.io/hostname"}}]}},"schedulerName":"default-scheduler"}},"volumeClaimTemplates":[{"metadata":{"name":"pvc-etc","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-master1-indexer"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"10Gi"}}},"status":{}},{"metadata":{"name":"pvc-var","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-master1-indexer"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"100Gi"}}},"status":{}}],"serviceName":"splunk-stack1-indexer-headless","podManagementPolicy":"Parallel","updateStrategy":{"type":"OnDelete"}},"status":{"replicas":0}}`) cr.Spec.Replicas = 1 - test(`{"kind":"StatefulSet","apiVersion":"apps/v1","metadata":{"name":"splunk-stack1-indexer","namespace":"test","creationTimestamp":null,"ownerReferences":[{"apiVersion":"","kind":"","name":"stack1","uid":"","controller":true}]},"spec":{"replicas":1,"selector":{"matchLabels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-master1-indexer"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-master1-indexer"},"annotations":{"traffic.sidecar.istio.io/excludeOutboundPorts":"8089,8191,9997,7777,9000,17000,17500,19000","traffic.sidecar.istio.io/includeInboundPorts":"8000,8088"}},"spec":{"volumes":[{"name":"mnt-splunk-secrets","secret":{"secretName":"splunk-stack1-indexer-secret-v1","defaultMode":420}}],"containers":[{"name":"splunk","image":"splunk/splunk","ports":[{"name":"http-splunkweb","containerPort":8000,"protocol":"TCP"},{"name":"http-hec","containerPort":8088,"protocol":"TCP"},{"name":"https-splunkd","containerPort":8089,"protocol":"TCP"},{"name":"tcp-s2s","containerPort":9997,"protocol":"TCP"}],"env":[{"name":"SPLUNK_HOME","value":"/opt/splunk"},{"name":"SPLUNK_START_ARGS","value":"--accept-license"},{"name":"SPLUNK_DEFAULTS_URL","value":"/mnt/splunk-secrets/default.yml"},{"name":"SPLUNK_HOME_OWNERSHIP_ENFORCEMENT","value":"false"},{"name":"SPLUNK_ROLE","value":"splunk_indexer"},{"name":"SPLUNK_DECLARATIVE_ADMIN_PASSWORD","value":"true"},{"name":"SPLUNK_INDEXER_URL","value":"splunk-stack1-indexer-0.splunk-stack1-indexer-headless.test.svc.cluster.local"},{"name":"SPLUNK_CLUSTER_MASTER_URL","value":"splunk-master1-cluster-master-service"}],"resources":{"limits":{"cpu":"4","memory":"8Gi"},"requests":{"cpu":"100m","memory":"512Mi"}},"volumeMounts":[{"name":"pvc-etc","mountPath":"/opt/splunk/etc"},{"name":"pvc-var","mountPath":"/opt/splunk/var"},{"name":"mnt-splunk-secrets","mountPath":"/mnt/splunk-secrets"}],"livenessProbe":{"exec":{"command":["/sbin/checkstate.sh"]},"initialDelaySeconds":300,"timeoutSeconds":30,"periodSeconds":30},"readinessProbe":{"exec":{"command":["/bin/grep","started","/opt/container_artifact/splunk-container.state"]},"initialDelaySeconds":10,"timeoutSeconds":5,"periodSeconds":5},"imagePullPolicy":"IfNotPresent"}],"securityContext":{"runAsUser":41812,"fsGroup":41812},"affinity":{"podAntiAffinity":{"preferredDuringSchedulingIgnoredDuringExecution":[{"weight":100,"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app.kubernetes.io/instance","operator":"In","values":["splunk-stack1-indexer"]}]},"topologyKey":"kubernetes.io/hostname"}}]}},"schedulerName":"default-scheduler"}},"volumeClaimTemplates":[{"metadata":{"name":"pvc-etc","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-master1-indexer"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"10Gi"}}},"status":{}},{"metadata":{"name":"pvc-var","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-master1-indexer"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"100Gi"}}},"status":{}}],"serviceName":"splunk-stack1-indexer-headless","podManagementPolicy":"Parallel","updateStrategy":{"type":"OnDelete"}},"status":{"replicas":0}}`) + test(`{"kind":"StatefulSet","apiVersion":"apps/v1","metadata":{"name":"splunk-stack1-indexer","namespace":"test","creationTimestamp":null,"ownerReferences":[{"apiVersion":"","kind":"","name":"stack1","uid":"","controller":true}]},"spec":{"replicas":1,"selector":{"matchLabels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-master1-indexer"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-master1-indexer"},"annotations":{"traffic.sidecar.istio.io/excludeOutboundPorts":"8089,8191,9997,7777,9000,17000,17500,19000","traffic.sidecar.istio.io/includeInboundPorts":"8000,8088"}},"spec":{"volumes":[{"name":"mnt-splunk-secrets","secret":{"secretName":"splunk-stack1-indexer-secret-v1","defaultMode":420}}],"containers":[{"name":"splunk","image":"splunk/splunk","ports":[{"name":"http-splunkweb","containerPort":8000,"protocol":"TCP"},{"name":"http-hec","containerPort":8088,"protocol":"TCP"},{"name":"https-splunkd","containerPort":8089,"protocol":"TCP"},{"name":"tcp-s2s","containerPort":9997,"protocol":"TCP"}],"env":[{"name":"SPLUNK_HOME","value":"/opt/splunk"},{"name":"SPLUNK_START_ARGS","value":"--accept-license"},{"name":"SPLUNK_DEFAULTS_URL","value":"/mnt/splunk-secrets/default.yml"},{"name":"SPLUNK_HOME_OWNERSHIP_ENFORCEMENT","value":"false"},{"name":"SPLUNK_ROLE","value":"splunk_indexer"},{"name":"SPLUNK_DECLARATIVE_ADMIN_PASSWORD","value":"true"},{"name":"SPLUNK_CLUSTER_MASTER_URL","value":"splunk-master1-cluster-master-service"}],"resources":{"limits":{"cpu":"4","memory":"8Gi"},"requests":{"cpu":"100m","memory":"512Mi"}},"volumeMounts":[{"name":"pvc-etc","mountPath":"/opt/splunk/etc"},{"name":"pvc-var","mountPath":"/opt/splunk/var"},{"name":"mnt-splunk-secrets","mountPath":"/mnt/splunk-secrets"}],"livenessProbe":{"exec":{"command":["/sbin/checkstate.sh"]},"initialDelaySeconds":300,"timeoutSeconds":30,"periodSeconds":30},"readinessProbe":{"exec":{"command":["/bin/grep","started","/opt/container_artifact/splunk-container.state"]},"initialDelaySeconds":10,"timeoutSeconds":5,"periodSeconds":5},"imagePullPolicy":"IfNotPresent"}],"securityContext":{"runAsUser":41812,"fsGroup":41812},"affinity":{"podAntiAffinity":{"preferredDuringSchedulingIgnoredDuringExecution":[{"weight":100,"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app.kubernetes.io/instance","operator":"In","values":["splunk-stack1-indexer"]}]},"topologyKey":"kubernetes.io/hostname"}}]}},"schedulerName":"default-scheduler"}},"volumeClaimTemplates":[{"metadata":{"name":"pvc-etc","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-master1-indexer"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"10Gi"}}},"status":{}},{"metadata":{"name":"pvc-var","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-master1-indexer"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"100Gi"}}},"status":{}}],"serviceName":"splunk-stack1-indexer-headless","podManagementPolicy":"Parallel","updateStrategy":{"type":"OnDelete"}},"status":{"replicas":0}}`) // Define additional service port in CR and verified the statefulset has the new port cr.Spec.ServiceTemplate.Spec.Ports = []corev1.ServicePort{{Name: "user-defined", Port: 32000, Protocol: "UDP"}} - test(`{"kind":"StatefulSet","apiVersion":"apps/v1","metadata":{"name":"splunk-stack1-indexer","namespace":"test","creationTimestamp":null,"ownerReferences":[{"apiVersion":"","kind":"","name":"stack1","uid":"","controller":true}]},"spec":{"replicas":1,"selector":{"matchLabels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-master1-indexer"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-master1-indexer"},"annotations":{"traffic.sidecar.istio.io/excludeOutboundPorts":"8089,8191,9997,7777,9000,17000,17500,19000","traffic.sidecar.istio.io/includeInboundPorts":"8000,8088"}},"spec":{"volumes":[{"name":"mnt-splunk-secrets","secret":{"secretName":"splunk-stack1-indexer-secret-v1","defaultMode":420}}],"containers":[{"name":"splunk","image":"splunk/splunk","ports":[{"name":"http-splunkweb","containerPort":8000,"protocol":"TCP"},{"name":"http-hec","containerPort":8088,"protocol":"TCP"},{"name":"https-splunkd","containerPort":8089,"protocol":"TCP"},{"name":"tcp-s2s","containerPort":9997,"protocol":"TCP"},{"name":"user-defined","containerPort":32000,"protocol":"UDP"}],"env":[{"name":"SPLUNK_HOME","value":"/opt/splunk"},{"name":"SPLUNK_START_ARGS","value":"--accept-license"},{"name":"SPLUNK_DEFAULTS_URL","value":"/mnt/splunk-secrets/default.yml"},{"name":"SPLUNK_HOME_OWNERSHIP_ENFORCEMENT","value":"false"},{"name":"SPLUNK_ROLE","value":"splunk_indexer"},{"name":"SPLUNK_DECLARATIVE_ADMIN_PASSWORD","value":"true"},{"name":"SPLUNK_INDEXER_URL","value":"splunk-stack1-indexer-0.splunk-stack1-indexer-headless.test.svc.cluster.local"},{"name":"SPLUNK_CLUSTER_MASTER_URL","value":"splunk-master1-cluster-master-service"}],"resources":{"limits":{"cpu":"4","memory":"8Gi"},"requests":{"cpu":"100m","memory":"512Mi"}},"volumeMounts":[{"name":"pvc-etc","mountPath":"/opt/splunk/etc"},{"name":"pvc-var","mountPath":"/opt/splunk/var"},{"name":"mnt-splunk-secrets","mountPath":"/mnt/splunk-secrets"}],"livenessProbe":{"exec":{"command":["/sbin/checkstate.sh"]},"initialDelaySeconds":300,"timeoutSeconds":30,"periodSeconds":30},"readinessProbe":{"exec":{"command":["/bin/grep","started","/opt/container_artifact/splunk-container.state"]},"initialDelaySeconds":10,"timeoutSeconds":5,"periodSeconds":5},"imagePullPolicy":"IfNotPresent"}],"securityContext":{"runAsUser":41812,"fsGroup":41812},"affinity":{"podAntiAffinity":{"preferredDuringSchedulingIgnoredDuringExecution":[{"weight":100,"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app.kubernetes.io/instance","operator":"In","values":["splunk-stack1-indexer"]}]},"topologyKey":"kubernetes.io/hostname"}}]}},"schedulerName":"default-scheduler"}},"volumeClaimTemplates":[{"metadata":{"name":"pvc-etc","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-master1-indexer"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"10Gi"}}},"status":{}},{"metadata":{"name":"pvc-var","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-master1-indexer"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"100Gi"}}},"status":{}}],"serviceName":"splunk-stack1-indexer-headless","podManagementPolicy":"Parallel","updateStrategy":{"type":"OnDelete"}},"status":{"replicas":0}}`) + test(`{"kind":"StatefulSet","apiVersion":"apps/v1","metadata":{"name":"splunk-stack1-indexer","namespace":"test","creationTimestamp":null,"ownerReferences":[{"apiVersion":"","kind":"","name":"stack1","uid":"","controller":true}]},"spec":{"replicas":1,"selector":{"matchLabels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-master1-indexer"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-master1-indexer"},"annotations":{"traffic.sidecar.istio.io/excludeOutboundPorts":"8089,8191,9997,7777,9000,17000,17500,19000","traffic.sidecar.istio.io/includeInboundPorts":"8000,8088"}},"spec":{"volumes":[{"name":"mnt-splunk-secrets","secret":{"secretName":"splunk-stack1-indexer-secret-v1","defaultMode":420}}],"containers":[{"name":"splunk","image":"splunk/splunk","ports":[{"name":"http-splunkweb","containerPort":8000,"protocol":"TCP"},{"name":"http-hec","containerPort":8088,"protocol":"TCP"},{"name":"https-splunkd","containerPort":8089,"protocol":"TCP"},{"name":"tcp-s2s","containerPort":9997,"protocol":"TCP"},{"name":"user-defined","containerPort":32000,"protocol":"UDP"}],"env":[{"name":"SPLUNK_HOME","value":"/opt/splunk"},{"name":"SPLUNK_START_ARGS","value":"--accept-license"},{"name":"SPLUNK_DEFAULTS_URL","value":"/mnt/splunk-secrets/default.yml"},{"name":"SPLUNK_HOME_OWNERSHIP_ENFORCEMENT","value":"false"},{"name":"SPLUNK_ROLE","value":"splunk_indexer"},{"name":"SPLUNK_DECLARATIVE_ADMIN_PASSWORD","value":"true"},{"name":"SPLUNK_CLUSTER_MASTER_URL","value":"splunk-master1-cluster-master-service"}],"resources":{"limits":{"cpu":"4","memory":"8Gi"},"requests":{"cpu":"100m","memory":"512Mi"}},"volumeMounts":[{"name":"pvc-etc","mountPath":"/opt/splunk/etc"},{"name":"pvc-var","mountPath":"/opt/splunk/var"},{"name":"mnt-splunk-secrets","mountPath":"/mnt/splunk-secrets"}],"livenessProbe":{"exec":{"command":["/sbin/checkstate.sh"]},"initialDelaySeconds":300,"timeoutSeconds":30,"periodSeconds":30},"readinessProbe":{"exec":{"command":["/bin/grep","started","/opt/container_artifact/splunk-container.state"]},"initialDelaySeconds":10,"timeoutSeconds":5,"periodSeconds":5},"imagePullPolicy":"IfNotPresent"}],"securityContext":{"runAsUser":41812,"fsGroup":41812},"affinity":{"podAntiAffinity":{"preferredDuringSchedulingIgnoredDuringExecution":[{"weight":100,"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app.kubernetes.io/instance","operator":"In","values":["splunk-stack1-indexer"]}]},"topologyKey":"kubernetes.io/hostname"}}]}},"schedulerName":"default-scheduler"}},"volumeClaimTemplates":[{"metadata":{"name":"pvc-etc","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-master1-indexer"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"10Gi"}}},"status":{}},{"metadata":{"name":"pvc-var","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-master1-indexer"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"100Gi"}}},"status":{}}],"serviceName":"splunk-stack1-indexer-headless","podManagementPolicy":"Parallel","updateStrategy":{"type":"OnDelete"}},"status":{"replicas":0}}`) // Block moving DefaultsURLApps to SPLUNK_DEFAULTS_URL for indexer cluster member cr.Spec.DefaultsURLApps = "/mnt/apps/apps.yml" - test(`{"kind":"StatefulSet","apiVersion":"apps/v1","metadata":{"name":"splunk-stack1-indexer","namespace":"test","creationTimestamp":null,"ownerReferences":[{"apiVersion":"","kind":"","name":"stack1","uid":"","controller":true}]},"spec":{"replicas":1,"selector":{"matchLabels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-master1-indexer"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-master1-indexer"},"annotations":{"traffic.sidecar.istio.io/excludeOutboundPorts":"8089,8191,9997,7777,9000,17000,17500,19000","traffic.sidecar.istio.io/includeInboundPorts":"8000,8088"}},"spec":{"volumes":[{"name":"mnt-splunk-secrets","secret":{"secretName":"splunk-stack1-indexer-secret-v1","defaultMode":420}}],"containers":[{"name":"splunk","image":"splunk/splunk","ports":[{"name":"http-splunkweb","containerPort":8000,"protocol":"TCP"},{"name":"http-hec","containerPort":8088,"protocol":"TCP"},{"name":"https-splunkd","containerPort":8089,"protocol":"TCP"},{"name":"tcp-s2s","containerPort":9997,"protocol":"TCP"},{"name":"user-defined","containerPort":32000,"protocol":"UDP"}],"env":[{"name":"SPLUNK_HOME","value":"/opt/splunk"},{"name":"SPLUNK_START_ARGS","value":"--accept-license"},{"name":"SPLUNK_DEFAULTS_URL","value":"/mnt/splunk-secrets/default.yml"},{"name":"SPLUNK_HOME_OWNERSHIP_ENFORCEMENT","value":"false"},{"name":"SPLUNK_ROLE","value":"splunk_indexer"},{"name":"SPLUNK_DECLARATIVE_ADMIN_PASSWORD","value":"true"},{"name":"SPLUNK_INDEXER_URL","value":"splunk-stack1-indexer-0.splunk-stack1-indexer-headless.test.svc.cluster.local"},{"name":"SPLUNK_CLUSTER_MASTER_URL","value":"splunk-master1-cluster-master-service"}],"resources":{"limits":{"cpu":"4","memory":"8Gi"},"requests":{"cpu":"100m","memory":"512Mi"}},"volumeMounts":[{"name":"pvc-etc","mountPath":"/opt/splunk/etc"},{"name":"pvc-var","mountPath":"/opt/splunk/var"},{"name":"mnt-splunk-secrets","mountPath":"/mnt/splunk-secrets"}],"livenessProbe":{"exec":{"command":["/sbin/checkstate.sh"]},"initialDelaySeconds":300,"timeoutSeconds":30,"periodSeconds":30},"readinessProbe":{"exec":{"command":["/bin/grep","started","/opt/container_artifact/splunk-container.state"]},"initialDelaySeconds":10,"timeoutSeconds":5,"periodSeconds":5},"imagePullPolicy":"IfNotPresent"}],"securityContext":{"runAsUser":41812,"fsGroup":41812},"affinity":{"podAntiAffinity":{"preferredDuringSchedulingIgnoredDuringExecution":[{"weight":100,"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app.kubernetes.io/instance","operator":"In","values":["splunk-stack1-indexer"]}]},"topologyKey":"kubernetes.io/hostname"}}]}},"schedulerName":"default-scheduler"}},"volumeClaimTemplates":[{"metadata":{"name":"pvc-etc","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-master1-indexer"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"10Gi"}}},"status":{}},{"metadata":{"name":"pvc-var","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-master1-indexer"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"100Gi"}}},"status":{}}],"serviceName":"splunk-stack1-indexer-headless","podManagementPolicy":"Parallel","updateStrategy":{"type":"OnDelete"}},"status":{"replicas":0}}`) + test(`{"kind":"StatefulSet","apiVersion":"apps/v1","metadata":{"name":"splunk-stack1-indexer","namespace":"test","creationTimestamp":null,"ownerReferences":[{"apiVersion":"","kind":"","name":"stack1","uid":"","controller":true}]},"spec":{"replicas":1,"selector":{"matchLabels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-master1-indexer"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-master1-indexer"},"annotations":{"traffic.sidecar.istio.io/excludeOutboundPorts":"8089,8191,9997,7777,9000,17000,17500,19000","traffic.sidecar.istio.io/includeInboundPorts":"8000,8088"}},"spec":{"volumes":[{"name":"mnt-splunk-secrets","secret":{"secretName":"splunk-stack1-indexer-secret-v1","defaultMode":420}}],"containers":[{"name":"splunk","image":"splunk/splunk","ports":[{"name":"http-splunkweb","containerPort":8000,"protocol":"TCP"},{"name":"http-hec","containerPort":8088,"protocol":"TCP"},{"name":"https-splunkd","containerPort":8089,"protocol":"TCP"},{"name":"tcp-s2s","containerPort":9997,"protocol":"TCP"},{"name":"user-defined","containerPort":32000,"protocol":"UDP"}],"env":[{"name":"SPLUNK_HOME","value":"/opt/splunk"},{"name":"SPLUNK_START_ARGS","value":"--accept-license"},{"name":"SPLUNK_DEFAULTS_URL","value":"/mnt/splunk-secrets/default.yml"},{"name":"SPLUNK_HOME_OWNERSHIP_ENFORCEMENT","value":"false"},{"name":"SPLUNK_ROLE","value":"splunk_indexer"},{"name":"SPLUNK_DECLARATIVE_ADMIN_PASSWORD","value":"true"},{"name":"SPLUNK_CLUSTER_MASTER_URL","value":"splunk-master1-cluster-master-service"}],"resources":{"limits":{"cpu":"4","memory":"8Gi"},"requests":{"cpu":"100m","memory":"512Mi"}},"volumeMounts":[{"name":"pvc-etc","mountPath":"/opt/splunk/etc"},{"name":"pvc-var","mountPath":"/opt/splunk/var"},{"name":"mnt-splunk-secrets","mountPath":"/mnt/splunk-secrets"}],"livenessProbe":{"exec":{"command":["/sbin/checkstate.sh"]},"initialDelaySeconds":300,"timeoutSeconds":30,"periodSeconds":30},"readinessProbe":{"exec":{"command":["/bin/grep","started","/opt/container_artifact/splunk-container.state"]},"initialDelaySeconds":10,"timeoutSeconds":5,"periodSeconds":5},"imagePullPolicy":"IfNotPresent"}],"securityContext":{"runAsUser":41812,"fsGroup":41812},"affinity":{"podAntiAffinity":{"preferredDuringSchedulingIgnoredDuringExecution":[{"weight":100,"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app.kubernetes.io/instance","operator":"In","values":["splunk-stack1-indexer"]}]},"topologyKey":"kubernetes.io/hostname"}}]}},"schedulerName":"default-scheduler"}},"volumeClaimTemplates":[{"metadata":{"name":"pvc-etc","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-master1-indexer"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"10Gi"}}},"status":{}},{"metadata":{"name":"pvc-var","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-master1-indexer"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"100Gi"}}},"status":{}}],"serviceName":"splunk-stack1-indexer-headless","podManagementPolicy":"Parallel","updateStrategy":{"type":"OnDelete"}},"status":{"replicas":0}}`) // Create a serviceaccount current := corev1.ServiceAccount{ @@ -1044,7 +1044,7 @@ func TestGetIndexerStatefulSet(t *testing.T) { } _ = splutil.CreateResource(c, ¤t) cr.Spec.ServiceAccount = "defaults" - test(`{"kind":"StatefulSet","apiVersion":"apps/v1","metadata":{"name":"splunk-stack1-indexer","namespace":"test","creationTimestamp":null,"ownerReferences":[{"apiVersion":"","kind":"","name":"stack1","uid":"","controller":true}]},"spec":{"replicas":1,"selector":{"matchLabels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-master1-indexer"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-master1-indexer"},"annotations":{"traffic.sidecar.istio.io/excludeOutboundPorts":"8089,8191,9997,7777,9000,17000,17500,19000","traffic.sidecar.istio.io/includeInboundPorts":"8000,8088"}},"spec":{"volumes":[{"name":"mnt-splunk-secrets","secret":{"secretName":"splunk-stack1-indexer-secret-v1","defaultMode":420}}],"containers":[{"name":"splunk","image":"splunk/splunk","ports":[{"name":"http-splunkweb","containerPort":8000,"protocol":"TCP"},{"name":"http-hec","containerPort":8088,"protocol":"TCP"},{"name":"https-splunkd","containerPort":8089,"protocol":"TCP"},{"name":"tcp-s2s","containerPort":9997,"protocol":"TCP"},{"name":"user-defined","containerPort":32000,"protocol":"UDP"}],"env":[{"name":"SPLUNK_HOME","value":"/opt/splunk"},{"name":"SPLUNK_START_ARGS","value":"--accept-license"},{"name":"SPLUNK_DEFAULTS_URL","value":"/mnt/splunk-secrets/default.yml"},{"name":"SPLUNK_HOME_OWNERSHIP_ENFORCEMENT","value":"false"},{"name":"SPLUNK_ROLE","value":"splunk_indexer"},{"name":"SPLUNK_DECLARATIVE_ADMIN_PASSWORD","value":"true"},{"name":"SPLUNK_INDEXER_URL","value":"splunk-stack1-indexer-0.splunk-stack1-indexer-headless.test.svc.cluster.local"},{"name":"SPLUNK_CLUSTER_MASTER_URL","value":"splunk-master1-cluster-master-service"}],"resources":{"limits":{"cpu":"4","memory":"8Gi"},"requests":{"cpu":"100m","memory":"512Mi"}},"volumeMounts":[{"name":"pvc-etc","mountPath":"/opt/splunk/etc"},{"name":"pvc-var","mountPath":"/opt/splunk/var"},{"name":"mnt-splunk-secrets","mountPath":"/mnt/splunk-secrets"}],"livenessProbe":{"exec":{"command":["/sbin/checkstate.sh"]},"initialDelaySeconds":300,"timeoutSeconds":30,"periodSeconds":30},"readinessProbe":{"exec":{"command":["/bin/grep","started","/opt/container_artifact/splunk-container.state"]},"initialDelaySeconds":10,"timeoutSeconds":5,"periodSeconds":5},"imagePullPolicy":"IfNotPresent"}],"serviceAccountName":"defaults","securityContext":{"runAsUser":41812,"fsGroup":41812},"affinity":{"podAntiAffinity":{"preferredDuringSchedulingIgnoredDuringExecution":[{"weight":100,"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app.kubernetes.io/instance","operator":"In","values":["splunk-stack1-indexer"]}]},"topologyKey":"kubernetes.io/hostname"}}]}},"schedulerName":"default-scheduler"}},"volumeClaimTemplates":[{"metadata":{"name":"pvc-etc","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-master1-indexer"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"10Gi"}}},"status":{}},{"metadata":{"name":"pvc-var","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-master1-indexer"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"100Gi"}}},"status":{}}],"serviceName":"splunk-stack1-indexer-headless","podManagementPolicy":"Parallel","updateStrategy":{"type":"OnDelete"}},"status":{"replicas":0}}`) + test(`{"kind":"StatefulSet","apiVersion":"apps/v1","metadata":{"name":"splunk-stack1-indexer","namespace":"test","creationTimestamp":null,"ownerReferences":[{"apiVersion":"","kind":"","name":"stack1","uid":"","controller":true}]},"spec":{"replicas":1,"selector":{"matchLabels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-master1-indexer"}},"template":{"metadata":{"creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-master1-indexer"},"annotations":{"traffic.sidecar.istio.io/excludeOutboundPorts":"8089,8191,9997,7777,9000,17000,17500,19000","traffic.sidecar.istio.io/includeInboundPorts":"8000,8088"}},"spec":{"volumes":[{"name":"mnt-splunk-secrets","secret":{"secretName":"splunk-stack1-indexer-secret-v1","defaultMode":420}}],"containers":[{"name":"splunk","image":"splunk/splunk","ports":[{"name":"http-splunkweb","containerPort":8000,"protocol":"TCP"},{"name":"http-hec","containerPort":8088,"protocol":"TCP"},{"name":"https-splunkd","containerPort":8089,"protocol":"TCP"},{"name":"tcp-s2s","containerPort":9997,"protocol":"TCP"},{"name":"user-defined","containerPort":32000,"protocol":"UDP"}],"env":[{"name":"SPLUNK_HOME","value":"/opt/splunk"},{"name":"SPLUNK_START_ARGS","value":"--accept-license"},{"name":"SPLUNK_DEFAULTS_URL","value":"/mnt/splunk-secrets/default.yml"},{"name":"SPLUNK_HOME_OWNERSHIP_ENFORCEMENT","value":"false"},{"name":"SPLUNK_ROLE","value":"splunk_indexer"},{"name":"SPLUNK_DECLARATIVE_ADMIN_PASSWORD","value":"true"},{"name":"SPLUNK_CLUSTER_MASTER_URL","value":"splunk-master1-cluster-master-service"}],"resources":{"limits":{"cpu":"4","memory":"8Gi"},"requests":{"cpu":"100m","memory":"512Mi"}},"volumeMounts":[{"name":"pvc-etc","mountPath":"/opt/splunk/etc"},{"name":"pvc-var","mountPath":"/opt/splunk/var"},{"name":"mnt-splunk-secrets","mountPath":"/mnt/splunk-secrets"}],"livenessProbe":{"exec":{"command":["/sbin/checkstate.sh"]},"initialDelaySeconds":300,"timeoutSeconds":30,"periodSeconds":30},"readinessProbe":{"exec":{"command":["/bin/grep","started","/opt/container_artifact/splunk-container.state"]},"initialDelaySeconds":10,"timeoutSeconds":5,"periodSeconds":5},"imagePullPolicy":"IfNotPresent"}],"serviceAccountName":"defaults","securityContext":{"runAsUser":41812,"fsGroup":41812},"affinity":{"podAntiAffinity":{"preferredDuringSchedulingIgnoredDuringExecution":[{"weight":100,"podAffinityTerm":{"labelSelector":{"matchExpressions":[{"key":"app.kubernetes.io/instance","operator":"In","values":["splunk-stack1-indexer"]}]},"topologyKey":"kubernetes.io/hostname"}}]}},"schedulerName":"default-scheduler"}},"volumeClaimTemplates":[{"metadata":{"name":"pvc-etc","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-master1-indexer"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"10Gi"}}},"status":{}},{"metadata":{"name":"pvc-var","namespace":"test","creationTimestamp":null,"labels":{"app.kubernetes.io/component":"indexer","app.kubernetes.io/instance":"splunk-stack1-indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-master1-indexer"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"100Gi"}}},"status":{}}],"serviceName":"splunk-stack1-indexer-headless","podManagementPolicy":"Parallel","updateStrategy":{"type":"OnDelete"}},"status":{"replicas":0}}`) cr.Spec.ClusterMasterRef.Namespace = "other" if err := validateIndexerClusterSpec(&cr); err == nil { diff --git a/pkg/splunk/enterprise/searchheadcluster.go b/pkg/splunk/enterprise/searchheadcluster.go index e151fe805..b8feb5899 100644 --- a/pkg/splunk/enterprise/searchheadcluster.go +++ b/pkg/splunk/enterprise/searchheadcluster.go @@ -347,7 +347,7 @@ func (mgr *searchHeadClusterPodManager) Update(c splcommon.ControllerClient, sta } // manage scaling and updates - return splctrl.UpdateStatefulSetPods(mgr.c, statefulSet, mgr, desiredReplicas, &mgr.cr.Status.SkipRecheckUpdate) + return splctrl.UpdateStatefulSetPods(mgr.c, statefulSet, mgr, desiredReplicas) } // PrepareScaleDown for searchHeadClusterPodManager prepares search head pod to be removed via scale down event; it returns true when ready diff --git a/pkg/splunk/enterprise/searchheadcluster_test.go b/pkg/splunk/enterprise/searchheadcluster_test.go index dc2f6bcd7..9a6c60f02 100644 --- a/pkg/splunk/enterprise/searchheadcluster_test.go +++ b/pkg/splunk/enterprise/searchheadcluster_test.go @@ -294,9 +294,6 @@ func TestSearchHeadClusterPodManager(t *testing.T) { extraCalls := []spltest.MockFuncCall{ {MetaName: "*v1.Pod-test-splunk-stack1-search-head-1"}, {MetaName: "*v1.Pod-test-splunk-stack1-search-head-1"}, - {MetaName: "*v1.StatefulSet-test-splunk-stack1"}, - {MetaName: "*v1.StatefulSet-test-splunk-stack1"}, - {MetaName: "*v1.Pod-test-splunk-stack1-0"}, } wantCalls = map[string][]spltest.MockFuncCall{"Get": {funcCalls[0], funcCalls[1], funcCalls[2]}, "Delete": pvcCalls, "Update": {funcCalls[0]}, "Create": {funcCalls[1]}} diff --git a/pkg/splunk/test/controller.go b/pkg/splunk/test/controller.go index 06cb2e02e..65b9eab1f 100644 --- a/pkg/splunk/test/controller.go +++ b/pkg/splunk/test/controller.go @@ -514,8 +514,6 @@ func PodManagerTester(t *testing.T, method string, mgr splcommon.StatefulSetPodM replicas = 2 current.Status.Replicas = 2 current.Status.ReadyReplicas = 1 - scaleUpCalls["Get"] = append(scaleUpCalls["Get"], funcCalls[0], funcCalls[0], funcCalls[1]) - scaleUpCalls["Update"] = append(scaleUpCalls["Update"], funcCalls[1]) methodPlus = fmt.Sprintf("%s(%s)", method, "ScalingUp, 1/2 ready") PodManagerUpdateTester(t, methodPlus, mgr, 2, splcommon.PhaseScalingUp, revised, scaleUpCalls, nil, current, pod) @@ -523,7 +521,6 @@ func PodManagerTester(t *testing.T, method string, mgr splcommon.StatefulSetPodM replicas = 1 current.Status.Replicas = 1 current.Status.ReadyReplicas = 1 - updateCalls["Get"] = append(updateCalls["Get"], funcCalls[0], funcCalls[0], funcCalls[1]) methodPlus = fmt.Sprintf("%s(%s)", method, "ScalingUp, Update Replicas 1=>2") PodManagerUpdateTester(t, methodPlus, mgr, 2, splcommon.PhaseScalingUp, revised, updateCalls, nil, current, pod) @@ -541,7 +538,7 @@ func PodManagerTester(t *testing.T, method string, mgr splcommon.StatefulSetPodM {MetaName: "*v1.PersistentVolumeClaim-test-pvc-var-splunk-stack1-1"}, } scaleDownCalls := map[string][]MockFuncCall{ - "Get": {funcCalls[0], funcCalls[0], funcCalls[0], funcCalls[1], pvcCalls[0], pvcCalls[1]}, + "Get": {funcCalls[0], pvcCalls[0], pvcCalls[1]}, "Update": {funcCalls[0]}, "Delete": pvcCalls, } @@ -575,9 +572,10 @@ func PodManagerTester(t *testing.T, method string, mgr splcommon.StatefulSetPodM listmockCall := []MockFuncCall{ {ListOpts: listOpts}} - updatePodCalls := map[string][]MockFuncCall{"Get": podCalls, "Delete": {}, "List": {listmockCall[0]}} + delCalls := []MockFuncCall{{MetaName: "*v1.Pod-test-splunk-stack1-0"}} + updatePodCalls := map[string][]MockFuncCall{"Get": podCalls, "Delete": delCalls} methodPlus = fmt.Sprintf("%s(%s)", method, "Recycle pod") - PodManagerUpdateTester(t, methodPlus, mgr, 1, splcommon.PhaseReady, revised, updatePodCalls, nil, current, pod) + PodManagerUpdateTester(t, methodPlus, mgr, 1, splcommon.PhaseUpdating, revised, updatePodCalls, nil, current, pod) // test all pods ready pod.ObjectMeta.Labels["controller-revision-hash"] = "v1" From ea496f068fe6430d76750336e463e16a10b9991a Mon Sep 17 00:00:00 2001 From: mgaldino Date: Thu, 4 Feb 2021 08:50:58 -0800 Subject: [PATCH 37/43] CSPL-817 Removes collapsible blocks for Github Pages compatibility --- docs/Ingress.md | 101 +++--------------------------------------------- 1 file changed, 5 insertions(+), 96 deletions(-) diff --git a/docs/Ingress.md b/docs/Ingress.md index 4a23a4ed7..c698ddb18 100644 --- a/docs/Ingress.md +++ b/docs/Ingress.md @@ -64,11 +64,7 @@ You can configure Istio to provide direct access to Splunk Web. #### Standalone Configuration -
Create a Gateway to receive traffic on port 80 - -

- ```yaml apiVersion: networking.istio.io/v1beta1 kind: Gateway @@ -86,14 +82,7 @@ spec: - "splunk.example.com" ``` -

-
- -
Create a virtual service to route traffic to your service, in this example we used a standalone. - -

- ```yaml apiVersion: networking.istio.io/v1beta1 kind: VirtualService @@ -113,8 +102,6 @@ spec: number: 8000 host: splunk-standalone-standalone-service ``` -

-
Get the External-IP for Istio using the command: ```shell @@ -130,11 +117,7 @@ http:// If your deployment has multiple hosts such as Search Heads and Cluster Master, use the following example as your Gateway and Virtual Service. -
Create Gateway for multiple hosts - -

- ```yaml apiVersion: networking.istio.io/v1beta1 kind: Gateway @@ -155,16 +138,9 @@ spec: - "license-master.splunk.example.com" ``` -

-
- Next, you will need to create VirtualServices for each of the components that you want to expose outside of Kubernetes: -
Create one virtual service for each component - -

- ```yaml apiVersion: networking.istio.io/v1beta1 kind: VirtualService @@ -238,17 +214,10 @@ spec: number: 8000 host: splunk-example-license-master-service ``` -

-
-Finally, you will need to create a DestinationRule to ensure user sessions are -sticky to specific search heads: - -
-DestinationRule for the Search Head - -

+Finally, you will need to create a DestinationRule to ensure user sessions are sticky to specific search heads: +Example DestinationRule for the Search Head ```yaml apiVersion: networking.istio.io/v1beta1 kind: DestinationRule @@ -264,10 +233,6 @@ spec: ttl: 3600s ``` -

-
- - ### Configuring Ingress for Splunk Forwarder data The pre-requisites for enabling inbound communications from Splunk Forwarders to the cluster are configuring the Istio Gateway and Istio Virtual Service: @@ -499,11 +464,7 @@ For all configurations below, we started with the standard yaml provided in the You can configure Nginx to provide direct access to Splunk Web. In the following example we use a standalone deployment to access Splunk Web. -
Create Ingress configuration - -

- ```yaml apiVersion: networking.k8s.io/v1beta1 kind: Ingress @@ -524,16 +485,9 @@ spec: servicePort: 8000 ``` -

-
- Likewise you can configure Splunk Web in multiple host configurations -
Create Ingress for multiple hosts - -

- ```yaml apiVersion: networking.k8s.io/v1beta1 kind: Ingress @@ -569,8 +523,6 @@ spec: serviceName: splunk-example-cluster-master-service servicePort: 8000 ``` -

-
### Configuring Ingress NGINX for Splunk Forwarders with End-to-End TLS @@ -579,11 +531,7 @@ Note: In this example we used port 9997 for non-encrypted communication, and 999 Update the default Ingress NGINX configuration to add the ConfigMap and Service ports: -
- Create a configMap to define the port-to-service routing - -

- +Create a configMap to define the port-to-service routing ```yaml apiVersion: v1 kind: ConfigMap @@ -595,14 +543,7 @@ data: 9998: "default/splunk-standalone-standalone-service:9998" ``` -

-
- -
Add the two ports into the Service used to configure the Load Balancer - -

- ```yaml apiVersion: v1 kind: Service @@ -642,10 +583,6 @@ spec: targetPort: 9998 ``` -

-
- - ##### Documentation tested on Ingress Nginx v1.19.4 and Kubernetes v1.17 ## Configuring Ingress Using NGINX Ingress Controller (Nginxinc) @@ -702,11 +639,7 @@ helm upgrade epat-eks-nginx nginx-stable/nginx-ingress The following ingress example yaml configures Splunk Web as well as HEC as an operator installed service. HEC is exposed via ssl and Splunk Web is non-ssl. -
Create Ingress - -

- ```yaml apiVersion: extensions/v1beta1 kind: Ingress @@ -749,17 +682,10 @@ status: loadBalancer: {} ``` -

-
- ##### Ingress Service for Splunk Forwarders Enable the global configuration to setup a listener and transport server -
Create GlobalConfiguration - -

- ```yaml apiVersion: k8s.nginx.org/v1alpha1 kind: GlobalConfiguration @@ -786,8 +712,6 @@ spec: action: pass: s2s-app ``` -

-
Edit the service to setup a node-port for the port being setup as the listener @@ -803,11 +727,7 @@ Edit the service and add the Splunk Forwarder ingress port kubectl edit service epat-eks-nginx-nginx-ingress ``` -
Sample Service - -

- ```yaml apiVersion: v1 kind: Service @@ -853,8 +773,6 @@ spec: sessionAffinity: None type: LoadBalancer ``` -

-
##### Documentation tested on Nginx Ingress Controller v1.9.0 and Kubernetes v1.18 @@ -867,9 +785,7 @@ used to enable secure (TLS) access to all Splunk components from outside of your Kubernetes cluster: -
Example configuration for NGINX -

- +Example configuration for NGINX ```yaml apiVersion: extensions/v1beta1 kind: Ingress @@ -949,11 +865,7 @@ tls: … ``` -

-
- -
Example configuration for Istio -

+Example configuration for Istio If you are using [cert-manager](https://docs.cert-manager.io/en/latest/getting-started/) with [Let’s Encrypt](https://letsencrypt.org/) to manage your TLS certificates @@ -1137,6 +1049,3 @@ spec: name: SPLUNK_ISTIO_SESSION ttl: 3600s ``` - -

-
From 3c4d78f8761b8244bbeeebfc539bce4d949da330 Mon Sep 17 00:00:00 2001 From: pogdin Date: Fri, 5 Feb 2021 13:18:19 -0500 Subject: [PATCH 38/43] Create hwf.yaml --- deploy/examples/advanced/hwf.yaml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 deploy/examples/advanced/hwf.yaml diff --git a/deploy/examples/advanced/hwf.yaml b/deploy/examples/advanced/hwf.yaml new file mode 100644 index 000000000..1a9f0bbf3 --- /dev/null +++ b/deploy/examples/advanced/hwf.yaml @@ -0,0 +1,17 @@ +apiVersion: enterprise.splunk.com/v1beta1 +kind: Standalone +metadata: + name: hwf-example + finalizers: + - enterprise.splunk.com/delete-pvc +spec: + clusterMasterRef: + name: + licenseMasterRef: + name: + replicas: + defaults: |- + splunk: + set_search_peers: false + #by default, setting the clusterMasterRef will establish a Search and Forwarding relationship from the Standalone to the Indexer Cluster + #use set_search_peers: false to disable the HWF from also searching the Indexer Cluster From 72ef704f6744fa6e332909332f46e429f9cf34fb Mon Sep 17 00:00:00 2001 From: pogdin Date: Fri, 5 Feb 2021 13:21:50 -0500 Subject: [PATCH 39/43] Create c1.yaml --- deploy/examples/advanced/c1.yaml | 47 ++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 deploy/examples/advanced/c1.yaml diff --git a/deploy/examples/advanced/c1.yaml b/deploy/examples/advanced/c1.yaml new file mode 100644 index 000000000..40097916f --- /dev/null +++ b/deploy/examples/advanced/c1.yaml @@ -0,0 +1,47 @@ +apiVersion: enterprise.splunk.com/v1beta1 +kind: LicenseMaster +metadata: + name: lm-example + finalizers: + - enterprise.splunk.com/delete-pvc +spec: + volumes: + - name: licenses + configMap: + name: splunk-licenses + licenseUrl: /mnt/licenses/enterprise.lic +--- +apiVersion: enterprise.splunk.com/v1beta1 +kind: ClusterMaster +metadata: + name: cm-example + finalizers: + - enterprise.splunk.com/delete-pvc +spec: + licenseMasterRef: + name: lm-example +--- +apiVersion: enterprise.splunk.com/v1beta1 +kind: IndexerCluster +metadata: + name: idxc-example + finalizers: + - enterprise.splunk.com/delete-pvc +spec: + replicas: + clusterMasterRef: + name: cm-example + licenseMasterRef: + name: lm-example +--- +apiVersion: enterprise.splunk.com/v1beta1 +kind: Standalone +metadata: + name: sh-example + finalizers: + - enterprise.splunk.com/delete-pvc +spec: + clusterMasterRef: + name: cm-example + licenseMasterRef: + name: lm-example From 5595ca29a7384fd07c89ccaf6b4035151983fea8 Mon Sep 17 00:00:00 2001 From: pogdin Date: Fri, 5 Feb 2021 13:25:34 -0500 Subject: [PATCH 40/43] Create c3.yaml --- deploy/examples/advanced/c3.yaml | 47 ++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 deploy/examples/advanced/c3.yaml diff --git a/deploy/examples/advanced/c3.yaml b/deploy/examples/advanced/c3.yaml new file mode 100644 index 000000000..8f1d26edf --- /dev/null +++ b/deploy/examples/advanced/c3.yaml @@ -0,0 +1,47 @@ +apiVersion: enterprise.splunk.com/v1beta1 +kind: LicenseMaster +metadata: + name: lm-example + finalizers: + - enterprise.splunk.com/delete-pvc +spec: + volumes: + - name: licenses + configMap: + name: splunk-licenses + licenseUrl: /mnt/licenses/enterprise.lic +--- +apiVersion: enterprise.splunk.com/v1beta1 +kind: ClusterMaster +metadata: + name: cm-example + finalizers: + - enterprise.splunk.com/delete-pvc +spec: + licenseMasterRef: + name: lm-example +--- +apiVersion: enterprise.splunk.com/v1beta1 +kind: IndexerCluster +metadata: + name: idxc-example + finalizers: + - enterprise.splunk.com/delete-pvc +spec: + replicas: + clusterMasterRef: + name: cm-example + licenseMasterRef: + name: lm-example +--- +apiVersion: enterprise.splunk.com/v1beta1 +kind: SearchHeadCluster +metadata: + name: shc-example + finalizers: + - enterprise.splunk.com/delete-pvc +spec: + clusterMasterRef: + name: cm-example + licenseMasterRef: + name: lm-example From 09f45629386811dce55331e2f940f21d23b97b26 Mon Sep 17 00:00:00 2001 From: pogdin Date: Fri, 5 Feb 2021 13:31:07 -0500 Subject: [PATCH 41/43] Create README.md --- deploy/examples/advanced/README.md | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 deploy/examples/advanced/README.md diff --git a/deploy/examples/advanced/README.md b/deploy/examples/advanced/README.md new file mode 100644 index 000000000..30056808b --- /dev/null +++ b/deploy/examples/advanced/README.md @@ -0,0 +1,2 @@ +These files are examples that will create, as closely as possible, the Splunk deployments described in the [Splunk Validated Architectures documentation](https://www.splunk.com/pdfs/technical-briefs/splunk-validated-architectures.pdf +). From 50ec225e48f7042e72ae8303fe5d23dbb102173b Mon Sep 17 00:00:00 2001 From: pogdin Date: Fri, 5 Feb 2021 13:32:15 -0500 Subject: [PATCH 42/43] Update hwf.yaml --- deploy/examples/advanced/hwf.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/deploy/examples/advanced/hwf.yaml b/deploy/examples/advanced/hwf.yaml index 1a9f0bbf3..7562d9dc2 100644 --- a/deploy/examples/advanced/hwf.yaml +++ b/deploy/examples/advanced/hwf.yaml @@ -6,9 +6,9 @@ metadata: - enterprise.splunk.com/delete-pvc spec: clusterMasterRef: - name: + name: cm-example licenseMasterRef: - name: + name: lm-example replicas: defaults: |- splunk: From b7ce672b5ca7cbe74a79c76c8d99963a4281eb02 Mon Sep 17 00:00:00 2001 From: akondur Date: Tue, 9 Feb 2021 16:29:50 -0800 Subject: [PATCH 43/43] Helper commit for 0.2.2 release --- ...erprise.splunk.com_clustermasters_crd.yaml | 2487 ++++++++++++++++ ...rprise.splunk.com_indexerclusters_crd.yaml | 2364 +++++++++++++++ ...erprise.splunk.com_licensemasters_crd.yaml | 2264 +++++++++++++++ ...ise.splunk.com_searchheadclusters_crd.yaml | 2414 ++++++++++++++++ .../enterprise.splunk.com_sparks_crd.yaml | 996 +++++++ ...enterprise.splunk.com_standalones_crd.yaml | 2544 +++++++++++++++++ .../splunk.v0.2.2.clusterserviceversion.yaml | 262 ++ deploy/operator.yaml | 2 +- docs/ChangeLog.md | 23 + docs/Install.md | 8 +- docs/README.md | 4 +- version/version.go | 2 +- 12 files changed, 13362 insertions(+), 8 deletions(-) create mode 100644 deploy/olm-catalog/splunk/0.2.2/enterprise.splunk.com_clustermasters_crd.yaml create mode 100644 deploy/olm-catalog/splunk/0.2.2/enterprise.splunk.com_indexerclusters_crd.yaml create mode 100644 deploy/olm-catalog/splunk/0.2.2/enterprise.splunk.com_licensemasters_crd.yaml create mode 100644 deploy/olm-catalog/splunk/0.2.2/enterprise.splunk.com_searchheadclusters_crd.yaml create mode 100644 deploy/olm-catalog/splunk/0.2.2/enterprise.splunk.com_sparks_crd.yaml create mode 100644 deploy/olm-catalog/splunk/0.2.2/enterprise.splunk.com_standalones_crd.yaml create mode 100644 deploy/olm-catalog/splunk/0.2.2/splunk.v0.2.2.clusterserviceversion.yaml diff --git a/deploy/olm-catalog/splunk/0.2.2/enterprise.splunk.com_clustermasters_crd.yaml b/deploy/olm-catalog/splunk/0.2.2/enterprise.splunk.com_clustermasters_crd.yaml new file mode 100644 index 000000000..c194ad2ed --- /dev/null +++ b/deploy/olm-catalog/splunk/0.2.2/enterprise.splunk.com_clustermasters_crd.yaml @@ -0,0 +1,2487 @@ +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: clustermasters.enterprise.splunk.com +spec: + additionalPrinterColumns: + - JSONPath: .status.phase + description: Status of cluster master + name: Phase + type: string + group: enterprise.splunk.com + names: + kind: ClusterMaster + listKind: ClusterMasterList + plural: clustermasters + shortNames: + - cm-idxc + singular: clustermaster + scope: Namespaced + subresources: + status: {} + validation: + openAPIV3Schema: + description: ClusterMaster is the Schema for the clustermasters API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: ClusterMasterSpec defines the desired state of ClusterMaster + properties: + Mock: + description: Mock to differentiate between UTs and actual reconcile + type: boolean + affinity: + description: Kubernetes Affinity rules that control how pods are assigned + to particular nodes. + properties: + nodeAffinity: + description: Describes node affinity scheduling rules for the pod. + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: The scheduler will prefer to schedule pods to nodes + that satisfy the affinity expressions specified by this field, + but it may choose a node that violates one or more of the + expressions. The node that is most preferred is the one with + the greatest sum of weights, i.e. for each node that meets + all of the scheduling requirements (resource request, requiredDuringScheduling + affinity expressions, etc.), compute a sum by iterating through + the elements of this field and adding "weight" to the sum + if the node matches the corresponding matchExpressions; the + node(s) with the highest sum are the most preferred. + items: + description: An empty preferred scheduling term matches all + objects with implicit weight 0 (i.e. it's a no-op). A null + preferred scheduling term matches no objects (i.e. is also + a no-op). + properties: + preference: + description: A node selector term, associated with the + corresponding weight. + properties: + matchExpressions: + description: A list of node selector requirements + by node's labels. + items: + description: A node selector requirement is a selector + that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: Represents a key's relationship + to a set of values. Valid operators are In, + NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: An array of string values. If the + operator is In or NotIn, the values array + must be non-empty. If the operator is Exists + or DoesNotExist, the values array must be + empty. If the operator is Gt or Lt, the values + array must have a single element, which will + be interpreted as an integer. This array is + replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchFields: + description: A list of node selector requirements + by node's fields. + items: + description: A node selector requirement is a selector + that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: Represents a key's relationship + to a set of values. Valid operators are In, + NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: An array of string values. If the + operator is In or NotIn, the values array + must be non-empty. If the operator is Exists + or DoesNotExist, the values array must be + empty. If the operator is Gt or Lt, the values + array must have a single element, which will + be interpreted as an integer. This array is + replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + type: object + weight: + description: Weight associated with matching the corresponding + nodeSelectorTerm, in the range 1-100. + format: int32 + type: integer + required: + - preference + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + description: If the affinity requirements specified by this + field are not met at scheduling time, the pod will not be + scheduled onto the node. If the affinity requirements specified + by this field cease to be met at some point during pod execution + (e.g. due to an update), the system may or may not try to + eventually evict the pod from its node. + properties: + nodeSelectorTerms: + description: Required. A list of node selector terms. The + terms are ORed. + items: + description: A null or empty node selector term matches + no objects. The requirements of them are ANDed. The + TopologySelectorTerm type implements a subset of the + NodeSelectorTerm. + properties: + matchExpressions: + description: A list of node selector requirements + by node's labels. + items: + description: A node selector requirement is a selector + that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: Represents a key's relationship + to a set of values. Valid operators are In, + NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: An array of string values. If the + operator is In or NotIn, the values array + must be non-empty. If the operator is Exists + or DoesNotExist, the values array must be + empty. If the operator is Gt or Lt, the values + array must have a single element, which will + be interpreted as an integer. This array is + replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchFields: + description: A list of node selector requirements + by node's fields. + items: + description: A node selector requirement is a selector + that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: Represents a key's relationship + to a set of values. Valid operators are In, + NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: An array of string values. If the + operator is In or NotIn, the values array + must be non-empty. If the operator is Exists + or DoesNotExist, the values array must be + empty. If the operator is Gt or Lt, the values + array must have a single element, which will + be interpreted as an integer. This array is + replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + type: object + type: array + required: + - nodeSelectorTerms + type: object + type: object + podAffinity: + description: Describes pod affinity scheduling rules (e.g. co-locate + this pod in the same node, zone, etc. as some other pod(s)). + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: The scheduler will prefer to schedule pods to nodes + that satisfy the affinity expressions specified by this field, + but it may choose a node that violates one or more of the + expressions. The node that is most preferred is the one with + the greatest sum of weights, i.e. for each node that meets + all of the scheduling requirements (resource request, requiredDuringScheduling + affinity expressions, etc.), compute a sum by iterating through + the elements of this field and adding "weight" to the sum + if the node has pods which matches the corresponding podAffinityTerm; + the node(s) with the highest sum are the most preferred. + items: + description: The weights of all of the matched WeightedPodAffinityTerm + fields are added per-node to find the most preferred node(s) + properties: + podAffinityTerm: + description: Required. A pod affinity term, associated + with the corresponding weight. + properties: + labelSelector: + description: A label query over a set of resources, + in this case pods. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: A label selector requirement is + a selector that contains values, a key, and + an operator that relates the key and values. + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: operator represents a key's + relationship to a set of values. Valid + operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: values is an array of string + values. If the operator is In or NotIn, + the values array must be non-empty. If + the operator is Exists or DoesNotExist, + the values array must be empty. This array + is replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, + whose key field is "key", the operator is "In", + and the values array contains only "value". + The requirements are ANDed. + type: object + type: object + namespaces: + description: namespaces specifies which namespaces + the labelSelector applies to (matches against); + null or empty list means "this pod's namespace" + items: + type: string + type: array + topologyKey: + description: This pod should be co-located (affinity) + or not co-located (anti-affinity) with the pods + matching the labelSelector in the specified namespaces, + where co-located is defined as running on a node + whose value of the label with key topologyKey matches + that of any node on which any of the selected pods + is running. Empty topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + weight: + description: weight associated with matching the corresponding + podAffinityTerm, in the range 1-100. + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + description: If the affinity requirements specified by this + field are not met at scheduling time, the pod will not be + scheduled onto the node. If the affinity requirements specified + by this field cease to be met at some point during pod execution + (e.g. due to a pod label update), the system may or may not + try to eventually evict the pod from its node. When there + are multiple elements, the lists of nodes corresponding to + each podAffinityTerm are intersected, i.e. all terms must + be satisfied. + items: + description: Defines a set of pods (namely those matching + the labelSelector relative to the given namespace(s)) that + this pod should be co-located (affinity) or not co-located + (anti-affinity) with, where co-located is defined as running + on a node whose value of the label with key + matches that of any node on which a pod of the set of pods + is running + properties: + labelSelector: + description: A label query over a set of resources, in + this case pods. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: A label selector requirement is a selector + that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: operator represents a key's relationship + to a set of values. Valid operators are In, + NotIn, Exists and DoesNotExist. + type: string + values: + description: values is an array of string values. + If the operator is In or NotIn, the values + array must be non-empty. If the operator is + Exists or DoesNotExist, the values array must + be empty. This array is replaced during a + strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} pairs. + A single {key,value} in the matchLabels map is equivalent + to an element of matchExpressions, whose key field + is "key", the operator is "In", and the values array + contains only "value". The requirements are ANDed. + type: object + type: object + namespaces: + description: namespaces specifies which namespaces the + labelSelector applies to (matches against); null or + empty list means "this pod's namespace" + items: + type: string + type: array + topologyKey: + description: This pod should be co-located (affinity) + or not co-located (anti-affinity) with the pods matching + the labelSelector in the specified namespaces, where + co-located is defined as running on a node whose value + of the label with key topologyKey matches that of any + node on which any of the selected pods is running. Empty + topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + type: array + type: object + podAntiAffinity: + description: Describes pod anti-affinity scheduling rules (e.g. + avoid putting this pod in the same node, zone, etc. as some other + pod(s)). + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: The scheduler will prefer to schedule pods to nodes + that satisfy the anti-affinity expressions specified by this + field, but it may choose a node that violates one or more + of the expressions. The node that is most preferred is the + one with the greatest sum of weights, i.e. for each node that + meets all of the scheduling requirements (resource request, + requiredDuringScheduling anti-affinity expressions, etc.), + compute a sum by iterating through the elements of this field + and adding "weight" to the sum if the node has pods which + matches the corresponding podAffinityTerm; the node(s) with + the highest sum are the most preferred. + items: + description: The weights of all of the matched WeightedPodAffinityTerm + fields are added per-node to find the most preferred node(s) + properties: + podAffinityTerm: + description: Required. A pod affinity term, associated + with the corresponding weight. + properties: + labelSelector: + description: A label query over a set of resources, + in this case pods. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: A label selector requirement is + a selector that contains values, a key, and + an operator that relates the key and values. + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: operator represents a key's + relationship to a set of values. Valid + operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: values is an array of string + values. If the operator is In or NotIn, + the values array must be non-empty. If + the operator is Exists or DoesNotExist, + the values array must be empty. This array + is replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, + whose key field is "key", the operator is "In", + and the values array contains only "value". + The requirements are ANDed. + type: object + type: object + namespaces: + description: namespaces specifies which namespaces + the labelSelector applies to (matches against); + null or empty list means "this pod's namespace" + items: + type: string + type: array + topologyKey: + description: This pod should be co-located (affinity) + or not co-located (anti-affinity) with the pods + matching the labelSelector in the specified namespaces, + where co-located is defined as running on a node + whose value of the label with key topologyKey matches + that of any node on which any of the selected pods + is running. Empty topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + weight: + description: weight associated with matching the corresponding + podAffinityTerm, in the range 1-100. + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + description: If the anti-affinity requirements specified by + this field are not met at scheduling time, the pod will not + be scheduled onto the node. If the anti-affinity requirements + specified by this field cease to be met at some point during + pod execution (e.g. due to a pod label update), the system + may or may not try to eventually evict the pod from its node. + When there are multiple elements, the lists of nodes corresponding + to each podAffinityTerm are intersected, i.e. all terms must + be satisfied. + items: + description: Defines a set of pods (namely those matching + the labelSelector relative to the given namespace(s)) that + this pod should be co-located (affinity) or not co-located + (anti-affinity) with, where co-located is defined as running + on a node whose value of the label with key + matches that of any node on which a pod of the set of pods + is running + properties: + labelSelector: + description: A label query over a set of resources, in + this case pods. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: A label selector requirement is a selector + that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: operator represents a key's relationship + to a set of values. Valid operators are In, + NotIn, Exists and DoesNotExist. + type: string + values: + description: values is an array of string values. + If the operator is In or NotIn, the values + array must be non-empty. If the operator is + Exists or DoesNotExist, the values array must + be empty. This array is replaced during a + strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} pairs. + A single {key,value} in the matchLabels map is equivalent + to an element of matchExpressions, whose key field + is "key", the operator is "In", and the values array + contains only "value". The requirements are ANDed. + type: object + type: object + namespaces: + description: namespaces specifies which namespaces the + labelSelector applies to (matches against); null or + empty list means "this pod's namespace" + items: + type: string + type: array + topologyKey: + description: This pod should be co-located (affinity) + or not co-located (anti-affinity) with the pods matching + the labelSelector in the specified namespaces, where + co-located is defined as running on a node whose value + of the label with key topologyKey matches that of any + node on which any of the selected pods is running. Empty + topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + type: array + type: object + type: object + clusterMasterRef: + description: ClusterMasterRef refers to a Splunk Enterprise indexer + cluster managed by the operator within Kubernetes + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: 'If referring to a piece of an object instead of an + entire object, this string should contain a valid JSON/Go field + access statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to a container within + a pod, this would take on a value like: "spec.containers{name}" + (where "name" refers to the name of the container that triggered + the event) or if no container name is specified "spec.containers[2]" + (container with index 2 in this pod). This syntax is chosen only + to have some well-defined way of referencing a part of an object. + TODO: this design is not final and this field is subject to change + in the future.' + type: string + kind: + description: 'Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + namespace: + description: 'Namespace of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/' + type: string + resourceVersion: + description: 'Specific resourceVersion to which this reference is + made, if any. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency' + type: string + uid: + description: 'UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids' + type: string + type: object + defaults: + description: Inline map of default.yml overrides used to initialize + the environment + type: string + defaultsUrl: + description: Full path or URL for one or more default.yml files, separated + by commas + type: string + defaultsUrlApps: + description: Full path or URL for one or more defaults.yml files specific + to App install, separated by commas. The defaults listed here will + be installed on the CM, standalone, search head deployer or license + master instance. + type: string + etcVolumeStorageConfig: + description: Storage configuration for /opt/splunk/etc volume + properties: + ephemeralStorage: + description: If true, ephemeral (emptyDir) storage will be used + type: boolean + storageCapacity: + description: Storage capacity to request persistent volume claims + (default=”10Gi” for etc and "100Gi" for var) + type: string + storageClassName: + description: Name of StorageClass to use for persistent volume claims + type: string + type: object + image: + description: Image to use for Splunk pod containers (overrides RELATED_IMAGE_SPLUNK_ENTERPRISE + environment variables) + type: string + imagePullPolicy: + description: 'Sets pull policy for all images (either “Always” or the + default: “IfNotPresent”)' + enum: + - Always + - IfNotPresent + type: string + licenseMasterRef: + description: LicenseMasterRef refers to a Splunk Enterprise license + master managed by the operator within Kubernetes + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: 'If referring to a piece of an object instead of an + entire object, this string should contain a valid JSON/Go field + access statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to a container within + a pod, this would take on a value like: "spec.containers{name}" + (where "name" refers to the name of the container that triggered + the event) or if no container name is specified "spec.containers[2]" + (container with index 2 in this pod). This syntax is chosen only + to have some well-defined way of referencing a part of an object. + TODO: this design is not final and this field is subject to change + in the future.' + type: string + kind: + description: 'Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + namespace: + description: 'Namespace of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/' + type: string + resourceVersion: + description: 'Specific resourceVersion to which this reference is + made, if any. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency' + type: string + uid: + description: 'UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids' + type: string + type: object + licenseUrl: + description: Full path or URL for a Splunk Enterprise license file + type: string + resources: + description: resource requirements for the pod containers + properties: + limits: + additionalProperties: + type: string + description: 'Limits describes the maximum amount of compute resources + allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/' + type: object + requests: + additionalProperties: + type: string + description: 'Requests describes the minimum amount of compute resources + required. If Requests is omitted for a container, it defaults + to Limits if that is explicitly specified, otherwise to an implementation-defined + value. More info: https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/' + type: object + type: object + schedulerName: + description: Name of Scheduler to use for pod placement (defaults to + “default-scheduler”) + type: string + serviceAccount: + description: ServiceAccount is the service account used by the pods + deployed by the CRD. If not specified uses the default serviceAccount + for the namespace as per https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/#use-the-default-service-account-to-access-the-api-server + type: string + serviceTemplate: + description: ServiceTemplate is a template used to create Kubernetes + services + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the + latest internal value, and may reject unrecognized values. More + info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource + this object represents. Servers may infer this from the endpoint + the client submits requests to. Cannot be updated. In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + description: 'Standard object''s metadata. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata' + type: object + spec: + description: Spec defines the behavior of a service. https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status + properties: + clusterIP: + description: 'clusterIP is the IP address of the service and + is usually assigned randomly by the master. If an address + is specified manually and is not in use by others, it will + be allocated to the service; otherwise, creation of the service + will fail. This field can not be changed through updates. + Valid values are "None", empty string (""), or a valid IP + address. "None" can be specified for headless services when + proxying is not required. Only applies to types ClusterIP, + NodePort, and LoadBalancer. Ignored if type is ExternalName. + More info: https://kubernetes.io/docs/concepts/services-networking/service/#virtual-ips-and-service-proxies' + type: string + externalIPs: + description: externalIPs is a list of IP addresses for which + nodes in the cluster will also accept traffic for this service. These + IPs are not managed by Kubernetes. The user is responsible + for ensuring that traffic arrives at a node with this IP. A + common example is external load-balancers that are not part + of the Kubernetes system. + items: + type: string + type: array + externalName: + description: externalName is the external reference that kubedns + or equivalent will return as a CNAME record for this service. + No proxying will be involved. Must be a valid RFC-1123 hostname + (https://tools.ietf.org/html/rfc1123) and requires Type to + be ExternalName. + type: string + externalTrafficPolicy: + description: externalTrafficPolicy denotes if this Service desires + to route external traffic to node-local or cluster-wide endpoints. + "Local" preserves the client source IP and avoids a second + hop for LoadBalancer and Nodeport type services, but risks + potentially imbalanced traffic spreading. "Cluster" obscures + the client source IP and may cause a second hop to another + node, but should have good overall load-spreading. + type: string + healthCheckNodePort: + description: healthCheckNodePort specifies the healthcheck nodePort + for the service. If not specified, HealthCheckNodePort is + created by the service api backend with the allocated nodePort. + Will use user-specified nodePort value if specified by the + client. Only effects when Type is set to LoadBalancer and + ExternalTrafficPolicy is set to Local. + format: int32 + type: integer + ipFamily: + description: ipFamily specifies whether this Service has a preference + for a particular IP family (e.g. IPv4 vs. IPv6). If a specific + IP family is requested, the clusterIP field will be allocated + from that family, if it is available in the cluster. If no + IP family is requested, the cluster's primary IP family will + be used. Other IP fields (loadBalancerIP, loadBalancerSourceRanges, + externalIPs) and controllers which allocate external load-balancers + should use the same IP family. Endpoints for this Service + will be of this family. This field is immutable after creation. + Assigning a ServiceIPFamily not available in the cluster (e.g. + IPv6 in IPv4 only cluster) is an error condition and will + fail during clusterIP assignment. + type: string + loadBalancerIP: + description: 'Only applies to Service Type: LoadBalancer LoadBalancer + will get created with the IP specified in this field. This + feature depends on whether the underlying cloud-provider supports + specifying the loadBalancerIP when a load balancer is created. + This field will be ignored if the cloud-provider does not + support the feature.' + type: string + loadBalancerSourceRanges: + description: 'If specified and supported by the platform, this + will restrict traffic through the cloud-provider load-balancer + will be restricted to the specified client IPs. This field + will be ignored if the cloud-provider does not support the + feature." More info: https://kubernetes.io/docs/tasks/access-application-cluster/configure-cloud-provider-firewall/' + items: + type: string + type: array + ports: + description: 'The list of ports that are exposed by this service. + More info: https://kubernetes.io/docs/concepts/services-networking/service/#virtual-ips-and-service-proxies' + items: + description: ServicePort contains information on service's + port. + properties: + name: + description: The name of this port within the service. + This must be a DNS_LABEL. All ports within a ServiceSpec + must have unique names. When considering the endpoints + for a Service, this must match the 'name' field in the + EndpointPort. Optional if only one ServicePort is defined + on this service. + type: string + nodePort: + description: 'The port on each node on which this service + is exposed when type=NodePort or LoadBalancer. Usually + assigned by the system. If specified, it will be allocated + to the service if unused or else creation of the service + will fail. Default is to auto-allocate a port if the + ServiceType of this Service requires one. More info: + https://kubernetes.io/docs/concepts/services-networking/service/#type-nodeport' + format: int32 + type: integer + port: + description: The port that will be exposed by this service. + format: int32 + type: integer + protocol: + description: The IP protocol for this port. Supports "TCP", + "UDP", and "SCTP". Default is TCP. + type: string + targetPort: + anyOf: + - type: integer + - type: string + description: 'Number or name of the port to access on + the pods targeted by the service. Number must be in + the range 1 to 65535. Name must be an IANA_SVC_NAME. + If this is a string, it will be looked up as a named + port in the target Pod''s container ports. If this is + not specified, the value of the ''port'' field is used + (an identity map). This field is ignored for services + with clusterIP=None, and should be omitted or set equal + to the ''port'' field. More info: https://kubernetes.io/docs/concepts/services-networking/service/#defining-a-service' + x-kubernetes-int-or-string: true + required: + - port + type: object + type: array + publishNotReadyAddresses: + description: publishNotReadyAddresses, when set to true, indicates + that DNS implementations must publish the notReadyAddresses + of subsets for the Endpoints associated with the Service. + The default value is false. The primary use case for setting + this field is to use a StatefulSet's Headless Service to propagate + SRV records for its Pods without respect to their readiness + for purpose of peer discovery. + type: boolean + selector: + additionalProperties: + type: string + description: 'Route service traffic to pods with label keys + and values matching this selector. If empty or not present, + the service is assumed to have an external process managing + its endpoints, which Kubernetes will not modify. Only applies + to types ClusterIP, NodePort, and LoadBalancer. Ignored if + type is ExternalName. More info: https://kubernetes.io/docs/concepts/services-networking/service/' + type: object + sessionAffinity: + description: 'Supports "ClientIP" and "None". Used to maintain + session affinity. Enable client IP based session affinity. + Must be ClientIP or None. Defaults to None. More info: https://kubernetes.io/docs/concepts/services-networking/service/#virtual-ips-and-service-proxies' + type: string + sessionAffinityConfig: + description: sessionAffinityConfig contains the configurations + of session affinity. + properties: + clientIP: + description: clientIP contains the configurations of Client + IP based session affinity. + properties: + timeoutSeconds: + description: timeoutSeconds specifies the seconds of + ClientIP type session sticky time. The value must + be >0 && <=86400(for 1 day) if ServiceAffinity == + "ClientIP". Default value is 10800(for 3 hours). + format: int32 + type: integer + type: object + type: object + type: + description: 'type determines how the Service is exposed. Defaults + to ClusterIP. Valid options are ExternalName, ClusterIP, NodePort, + and LoadBalancer. "ExternalName" maps to the specified externalName. + "ClusterIP" allocates a cluster-internal IP address for load-balancing + to endpoints. Endpoints are determined by the selector or + if that is not specified, by manual construction of an Endpoints + object. If clusterIP is "None", no virtual IP is allocated + and the endpoints are published as a set of endpoints rather + than a stable IP. "NodePort" builds on ClusterIP and allocates + a port on every node which routes to the clusterIP. "LoadBalancer" + builds on NodePort and creates an external load-balancer (if + supported in the current cloud) which routes to the clusterIP. + More info: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types' + type: string + type: object + status: + description: 'Most recently observed status of the service. Populated + by the system. Read-only. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status' + properties: + loadBalancer: + description: LoadBalancer contains the current status of the + load-balancer, if one is present. + properties: + ingress: + description: Ingress is a list containing ingress points + for the load-balancer. Traffic intended for the service + should be sent to these ingress points. + items: + description: 'LoadBalancerIngress represents the status + of a load-balancer ingress point: traffic intended for + the service should be sent to an ingress point.' + properties: + hostname: + description: Hostname is set for load-balancer ingress + points that are DNS based (typically AWS load-balancers) + type: string + ip: + description: IP is set for load-balancer ingress points + that are IP based (typically GCE or OpenStack load-balancers) + type: string + type: object + type: array + type: object + type: object + type: object + smartstore: + description: Splunk Smartstore configuration. Refer to indexes.conf.spec + and server.conf.spec on docs.splunk.com + properties: + cacheManager: + description: Defines Cache manager settings + properties: + evictionPadding: + description: Additional size beyond 'minFreeSize' before eviction + kicks in + type: integer + evictionPolicy: + description: Eviction policy to use + type: string + hotlistBloomFilterRecencyHours: + description: Time period relative to the bucket's age, during + which the bloom filter file is protected from cache eviction + type: integer + hotlistRecencySecs: + description: Time period relative to the bucket's age, during + which the bucket is protected from cache eviction + type: integer + maxCacheSize: + description: Max cache size per partition + type: integer + maxConcurrentDownloads: + description: Maximum number of buckets that can be downloaded + from remote storage in parallel + type: integer + maxConcurrentUploads: + description: Maximum number of buckets that can be uploaded + to remote storage in parallel + type: integer + type: object + defaults: + description: Default configuration for indexes + properties: + maxGlobalDataSizeMB: + description: MaxGlobalDataSizeMB defines the maximum amount + of space for warm and cold buckets of an index + type: integer + maxGlobalRawDataSizeMB: + description: MaxGlobalDataSizeMB defines the maximum amount + of cumulative space for warm and cold buckets of an index + type: integer + volumeName: + description: Remote Volume name + type: string + type: object + indexes: + description: List of Splunk indexes + items: + description: IndexSpec defines Splunk index name and storage path + properties: + hotlistBloomFilterRecencyHours: + description: Time period relative to the bucket's age, during + which the bloom filter file is protected from cache eviction + type: integer + hotlistRecencySecs: + description: Time period relative to the bucket's age, during + which the bucket is protected from cache eviction + type: integer + maxGlobalDataSizeMB: + description: MaxGlobalDataSizeMB defines the maximum amount + of space for warm and cold buckets of an index + type: integer + maxGlobalRawDataSizeMB: + description: MaxGlobalDataSizeMB defines the maximum amount + of cumulative space for warm and cold buckets of an index + type: integer + name: + description: Splunk index name + type: string + remotePath: + description: Index location relative to the remote volume + path + type: string + volumeName: + description: Remote Volume name + type: string + type: object + type: array + volumes: + description: List of remote storage volumes + items: + description: VolumeSpec defines remote volume name and remote + volume URI + properties: + endpoint: + description: Remote volume URI + type: string + name: + description: Remote volume name + type: string + path: + description: Remote volume path + type: string + secretRef: + description: Secret object name + type: string + type: object + type: array + type: object + sparkImage: + description: Image to use for Spark pod containers (overrides RELATED_IMAGE_SPLUNK_SPARK + environment variables). Also used on cluster master for init container + to setup the soft links from ../master-apps/splunk-operator/local/ + to /mnt/splunk-operator/local/ + type: string + tolerations: + description: Pod's tolerations for Kubernetes node's taint + items: + description: The pod this Toleration is attached to tolerates any + taint that matches the triple using the matching + operator . + properties: + effect: + description: Effect indicates the taint effect to match. Empty + means match all taint effects. When specified, allowed values + are NoSchedule, PreferNoSchedule and NoExecute. + type: string + key: + description: Key is the taint key that the toleration applies + to. Empty means match all taint keys. If the key is empty, operator + must be Exists; this combination means to match all values and + all keys. + type: string + operator: + description: Operator represents a key's relationship to the value. + Valid operators are Exists and Equal. Defaults to Equal. Exists + is equivalent to wildcard for value, so that a pod can tolerate + all taints of a particular category. + type: string + tolerationSeconds: + description: TolerationSeconds represents the period of time the + toleration (which must be of effect NoExecute, otherwise this + field is ignored) tolerates the taint. By default, it is not + set, which means tolerate the taint forever (do not evict). + Zero and negative values will be treated as 0 (evict immediately) + by the system. + format: int64 + type: integer + value: + description: Value is the taint value the toleration matches to. + If the operator is Exists, the value should be empty, otherwise + just a regular string. + type: string + type: object + type: array + varVolumeStorageConfig: + description: Storage configuration for /opt/splunk/var volume + properties: + ephemeralStorage: + description: If true, ephemeral (emptyDir) storage will be used + type: boolean + storageCapacity: + description: Storage capacity to request persistent volume claims + (default=”10Gi” for etc and "100Gi" for var) + type: string + storageClassName: + description: Name of StorageClass to use for persistent volume claims + type: string + type: object + volumes: + description: List of one or more Kubernetes volumes. These will be mounted + in all pod containers as as /mnt/ + items: + description: Volume represents a named volume in a pod that may be + accessed by any container in the pod. + properties: + awsElasticBlockStore: + description: 'AWSElasticBlockStore represents an AWS Disk resource + that is attached to a kubelet''s host machine and then exposed + to the pod. More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore' + properties: + fsType: + description: 'Filesystem type of the volume that you want + to mount. Tip: Ensure that the filesystem type is supported + by the host operating system. Examples: "ext4", "xfs", "ntfs". + Implicitly inferred to be "ext4" if unspecified. More info: + https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore + TODO: how do we prevent errors in the filesystem from compromising + the machine' + type: string + partition: + description: 'The partition in the volume that you want to + mount. If omitted, the default is to mount by volume name. + Examples: For volume /dev/sda1, you specify the partition + as "1". Similarly, the volume partition for /dev/sda is + "0" (or you can leave the property empty).' + format: int32 + type: integer + readOnly: + description: 'Specify "true" to force and set the ReadOnly + property in VolumeMounts to "true". If omitted, the default + is "false". More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore' + type: boolean + volumeID: + description: 'Unique ID of the persistent disk resource in + AWS (Amazon EBS volume). More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore' + type: string + required: + - volumeID + type: object + azureDisk: + description: AzureDisk represents an Azure Data Disk mount on + the host and bind mount to the pod. + properties: + cachingMode: + description: 'Host Caching mode: None, Read Only, Read Write.' + type: string + diskName: + description: The Name of the data disk in the blob storage + type: string + diskURI: + description: The URI the data disk in the blob storage + type: string + fsType: + description: Filesystem type to mount. Must be a filesystem + type supported by the host operating system. Ex. "ext4", + "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + type: string + kind: + description: 'Expected values Shared: multiple blob disks + per storage account Dedicated: single blob disk per storage + account Managed: azure managed data disk (only in managed + availability set). defaults to shared' + type: string + readOnly: + description: Defaults to false (read/write). ReadOnly here + will force the ReadOnly setting in VolumeMounts. + type: boolean + required: + - diskName + - diskURI + type: object + azureFile: + description: AzureFile represents an Azure File Service mount + on the host and bind mount to the pod. + properties: + readOnly: + description: Defaults to false (read/write). ReadOnly here + will force the ReadOnly setting in VolumeMounts. + type: boolean + secretName: + description: the name of secret that contains Azure Storage + Account Name and Key + type: string + shareName: + description: Share Name + type: string + required: + - secretName + - shareName + type: object + cephfs: + description: CephFS represents a Ceph FS mount on the host that + shares a pod's lifetime + properties: + monitors: + description: 'Required: Monitors is a collection of Ceph monitors + More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it' + items: + type: string + type: array + path: + description: 'Optional: Used as the mounted root, rather than + the full Ceph tree, default is /' + type: string + readOnly: + description: 'Optional: Defaults to false (read/write). ReadOnly + here will force the ReadOnly setting in VolumeMounts. More + info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it' + type: boolean + secretFile: + description: 'Optional: SecretFile is the path to key ring + for User, default is /etc/ceph/user.secret More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it' + type: string + secretRef: + description: 'Optional: SecretRef is reference to the authentication + secret for User, default is empty. More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it' + properties: + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + type: object + user: + description: 'Optional: User is the rados user name, default + is admin More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it' + type: string + required: + - monitors + type: object + cinder: + description: 'Cinder represents a cinder volume attached and mounted + on kubelets host machine. More info: https://examples.k8s.io/mysql-cinder-pd/README.md' + properties: + fsType: + description: 'Filesystem type to mount. Must be a filesystem + type supported by the host operating system. Examples: "ext4", + "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + More info: https://examples.k8s.io/mysql-cinder-pd/README.md' + type: string + readOnly: + description: 'Optional: Defaults to false (read/write). ReadOnly + here will force the ReadOnly setting in VolumeMounts. More + info: https://examples.k8s.io/mysql-cinder-pd/README.md' + type: boolean + secretRef: + description: 'Optional: points to a secret object containing + parameters used to connect to OpenStack.' + properties: + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + type: object + volumeID: + description: 'volume id used to identify the volume in cinder. + More info: https://examples.k8s.io/mysql-cinder-pd/README.md' + type: string + required: + - volumeID + type: object + configMap: + description: ConfigMap represents a configMap that should populate + this volume + properties: + defaultMode: + description: 'Optional: mode bits to use on created files + by default. Must be a value between 0 and 0777. Defaults + to 0644. Directories within the path are not affected by + this setting. This might be in conflict with other options + that affect the file mode, like fsGroup, and the result + can be other mode bits set.' + format: int32 + type: integer + items: + description: If unspecified, each key-value pair in the Data + field of the referenced ConfigMap will be projected into + the volume as a file whose name is the key and content is + the value. If specified, the listed keys will be projected + into the specified paths, and unlisted keys will not be + present. If a key is specified which is not present in the + ConfigMap, the volume setup will error unless it is marked + optional. Paths must be relative and may not contain the + '..' path or start with '..'. + items: + description: Maps a string key to a path within a volume. + properties: + key: + description: The key to project. + type: string + mode: + description: 'Optional: mode bits to use on this file, + must be a value between 0 and 0777. If not specified, + the volume defaultMode will be used. This might be + in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode + bits set.' + format: int32 + type: integer + path: + description: The relative path of the file to map the + key to. May not be an absolute path. May not contain + the path element '..'. May not start with the string + '..'. + type: string + required: + - key + - path + type: object + type: array + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + optional: + description: Specify whether the ConfigMap or its keys must + be defined + type: boolean + type: object + csi: + description: CSI (Container Storage Interface) represents storage + that is handled by an external CSI driver (Alpha feature). + properties: + driver: + description: Driver is the name of the CSI driver that handles + this volume. Consult with your admin for the correct name + as registered in the cluster. + type: string + fsType: + description: Filesystem type to mount. Ex. "ext4", "xfs", + "ntfs". If not provided, the empty value is passed to the + associated CSI driver which will determine the default filesystem + to apply. + type: string + nodePublishSecretRef: + description: NodePublishSecretRef is a reference to the secret + object containing sensitive information to pass to the CSI + driver to complete the CSI NodePublishVolume and NodeUnpublishVolume + calls. This field is optional, and may be empty if no secret + is required. If the secret object contains more than one + secret, all secret references are passed. + properties: + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + type: object + readOnly: + description: Specifies a read-only configuration for the volume. + Defaults to false (read/write). + type: boolean + volumeAttributes: + additionalProperties: + type: string + description: VolumeAttributes stores driver-specific properties + that are passed to the CSI driver. Consult your driver's + documentation for supported values. + type: object + required: + - driver + type: object + downwardAPI: + description: DownwardAPI represents downward API about the pod + that should populate this volume + properties: + defaultMode: + description: 'Optional: mode bits to use on created files + by default. Must be a value between 0 and 0777. Defaults + to 0644. Directories within the path are not affected by + this setting. This might be in conflict with other options + that affect the file mode, like fsGroup, and the result + can be other mode bits set.' + format: int32 + type: integer + items: + description: Items is a list of downward API volume file + items: + description: DownwardAPIVolumeFile represents information + to create the file containing the pod field + properties: + fieldRef: + description: 'Required: Selects a field of the pod: + only annotations, labels, name and namespace are supported.' + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in the + specified API version. + type: string + required: + - fieldPath + type: object + mode: + description: 'Optional: mode bits to use on this file, + must be a value between 0 and 0777. If not specified, + the volume defaultMode will be used. This might be + in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode + bits set.' + format: int32 + type: integer + path: + description: 'Required: Path is the relative path name + of the file to be created. Must not be absolute or + contain the ''..'' path. Must be utf-8 encoded. The + first item of the relative path must not start with + ''..''' + type: string + resourceFieldRef: + description: 'Selects a resource of the container: only + resources limits and requests (limits.cpu, limits.memory, + requests.cpu and requests.memory) are currently supported.' + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + description: Specifies the output format of the + exposed resources, defaults to "1" + type: string + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + required: + - path + type: object + type: array + type: object + emptyDir: + description: 'EmptyDir represents a temporary directory that shares + a pod''s lifetime. More info: https://kubernetes.io/docs/concepts/storage/volumes#emptydir' + properties: + medium: + description: 'What type of storage medium should back this + directory. The default is "" which means to use the node''s + default medium. Must be an empty string (default) or Memory. + More info: https://kubernetes.io/docs/concepts/storage/volumes#emptydir' + type: string + sizeLimit: + description: 'Total amount of local storage required for this + EmptyDir volume. The size limit is also applicable for memory + medium. The maximum usage on memory medium EmptyDir would + be the minimum value between the SizeLimit specified here + and the sum of memory limits of all containers in a pod. + The default is nil which means that the limit is undefined. + More info: http://kubernetes.io/docs/user-guide/volumes#emptydir' + type: string + type: object + fc: + description: FC represents a Fibre Channel resource that is attached + to a kubelet's host machine and then exposed to the pod. + properties: + fsType: + description: 'Filesystem type to mount. Must be a filesystem + type supported by the host operating system. Ex. "ext4", + "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + TODO: how do we prevent errors in the filesystem from compromising + the machine' + type: string + lun: + description: 'Optional: FC target lun number' + format: int32 + type: integer + readOnly: + description: 'Optional: Defaults to false (read/write). ReadOnly + here will force the ReadOnly setting in VolumeMounts.' + type: boolean + targetWWNs: + description: 'Optional: FC target worldwide names (WWNs)' + items: + type: string + type: array + wwids: + description: 'Optional: FC volume world wide identifiers (wwids) + Either wwids or combination of targetWWNs and lun must be + set, but not both simultaneously.' + items: + type: string + type: array + type: object + flexVolume: + description: FlexVolume represents a generic volume resource that + is provisioned/attached using an exec based plugin. + properties: + driver: + description: Driver is the name of the driver to use for this + volume. + type: string + fsType: + description: Filesystem type to mount. Must be a filesystem + type supported by the host operating system. Ex. "ext4", + "xfs", "ntfs". The default filesystem depends on FlexVolume + script. + type: string + options: + additionalProperties: + type: string + description: 'Optional: Extra command options if any.' + type: object + readOnly: + description: 'Optional: Defaults to false (read/write). ReadOnly + here will force the ReadOnly setting in VolumeMounts.' + type: boolean + secretRef: + description: 'Optional: SecretRef is reference to the secret + object containing sensitive information to pass to the plugin + scripts. This may be empty if no secret object is specified. + If the secret object contains more than one secret, all + secrets are passed to the plugin scripts.' + properties: + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + type: object + required: + - driver + type: object + flocker: + description: Flocker represents a Flocker volume attached to a + kubelet's host machine. This depends on the Flocker control + service being running + properties: + datasetName: + description: Name of the dataset stored as metadata -> name + on the dataset for Flocker should be considered as deprecated + type: string + datasetUUID: + description: UUID of the dataset. This is unique identifier + of a Flocker dataset + type: string + type: object + gcePersistentDisk: + description: 'GCEPersistentDisk represents a GCE Disk resource + that is attached to a kubelet''s host machine and then exposed + to the pod. More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk' + properties: + fsType: + description: 'Filesystem type of the volume that you want + to mount. Tip: Ensure that the filesystem type is supported + by the host operating system. Examples: "ext4", "xfs", "ntfs". + Implicitly inferred to be "ext4" if unspecified. More info: + https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk + TODO: how do we prevent errors in the filesystem from compromising + the machine' + type: string + partition: + description: 'The partition in the volume that you want to + mount. If omitted, the default is to mount by volume name. + Examples: For volume /dev/sda1, you specify the partition + as "1". Similarly, the volume partition for /dev/sda is + "0" (or you can leave the property empty). More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk' + format: int32 + type: integer + pdName: + description: 'Unique name of the PD resource in GCE. Used + to identify the disk in GCE. More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk' + type: string + readOnly: + description: 'ReadOnly here will force the ReadOnly setting + in VolumeMounts. Defaults to false. More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk' + type: boolean + required: + - pdName + type: object + gitRepo: + description: 'GitRepo represents a git repository at a particular + revision. DEPRECATED: GitRepo is deprecated. To provision a + container with a git repo, mount an EmptyDir into an InitContainer + that clones the repo using git, then mount the EmptyDir into + the Pod''s container.' + properties: + directory: + description: Target directory name. Must not contain or start + with '..'. If '.' is supplied, the volume directory will + be the git repository. Otherwise, if specified, the volume + will contain the git repository in the subdirectory with + the given name. + type: string + repository: + description: Repository URL + type: string + revision: + description: Commit hash for the specified revision. + type: string + required: + - repository + type: object + glusterfs: + description: 'Glusterfs represents a Glusterfs mount on the host + that shares a pod''s lifetime. More info: https://examples.k8s.io/volumes/glusterfs/README.md' + properties: + endpoints: + description: 'EndpointsName is the endpoint name that details + Glusterfs topology. More info: https://examples.k8s.io/volumes/glusterfs/README.md#create-a-pod' + type: string + path: + description: 'Path is the Glusterfs volume path. More info: + https://examples.k8s.io/volumes/glusterfs/README.md#create-a-pod' + type: string + readOnly: + description: 'ReadOnly here will force the Glusterfs volume + to be mounted with read-only permissions. Defaults to false. + More info: https://examples.k8s.io/volumes/glusterfs/README.md#create-a-pod' + type: boolean + required: + - endpoints + - path + type: object + hostPath: + description: 'HostPath represents a pre-existing file or directory + on the host machine that is directly exposed to the container. + This is generally used for system agents or other privileged + things that are allowed to see the host machine. Most containers + will NOT need this. More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath + --- TODO(jonesdl) We need to restrict who can use host directory + mounts and who can/can not mount host directories as read/write.' + properties: + path: + description: 'Path of the directory on the host. If the path + is a symlink, it will follow the link to the real path. + More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath' + type: string + type: + description: 'Type for HostPath Volume Defaults to "" More + info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath' + type: string + required: + - path + type: object + iscsi: + description: 'ISCSI represents an ISCSI Disk resource that is + attached to a kubelet''s host machine and then exposed to the + pod. More info: https://examples.k8s.io/volumes/iscsi/README.md' + properties: + chapAuthDiscovery: + description: whether support iSCSI Discovery CHAP authentication + type: boolean + chapAuthSession: + description: whether support iSCSI Session CHAP authentication + type: boolean + fsType: + description: 'Filesystem type of the volume that you want + to mount. Tip: Ensure that the filesystem type is supported + by the host operating system. Examples: "ext4", "xfs", "ntfs". + Implicitly inferred to be "ext4" if unspecified. More info: + https://kubernetes.io/docs/concepts/storage/volumes#iscsi + TODO: how do we prevent errors in the filesystem from compromising + the machine' + type: string + initiatorName: + description: Custom iSCSI Initiator Name. If initiatorName + is specified with iscsiInterface simultaneously, new iSCSI + interface : will be created + for the connection. + type: string + iqn: + description: Target iSCSI Qualified Name. + type: string + iscsiInterface: + description: iSCSI Interface Name that uses an iSCSI transport. + Defaults to 'default' (tcp). + type: string + lun: + description: iSCSI Target Lun number. + format: int32 + type: integer + portals: + description: iSCSI Target Portal List. The portal is either + an IP or ip_addr:port if the port is other than default + (typically TCP ports 860 and 3260). + items: + type: string + type: array + readOnly: + description: ReadOnly here will force the ReadOnly setting + in VolumeMounts. Defaults to false. + type: boolean + secretRef: + description: CHAP Secret for iSCSI target and initiator authentication + properties: + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + type: object + targetPortal: + description: iSCSI Target Portal. The Portal is either an + IP or ip_addr:port if the port is other than default (typically + TCP ports 860 and 3260). + type: string + required: + - iqn + - lun + - targetPortal + type: object + name: + description: 'Volume''s name. Must be a DNS_LABEL and unique within + the pod. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + nfs: + description: 'NFS represents an NFS mount on the host that shares + a pod''s lifetime More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs' + properties: + path: + description: 'Path that is exported by the NFS server. More + info: https://kubernetes.io/docs/concepts/storage/volumes#nfs' + type: string + readOnly: + description: 'ReadOnly here will force the NFS export to be + mounted with read-only permissions. Defaults to false. More + info: https://kubernetes.io/docs/concepts/storage/volumes#nfs' + type: boolean + server: + description: 'Server is the hostname or IP address of the + NFS server. More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs' + type: string + required: + - path + - server + type: object + persistentVolumeClaim: + description: 'PersistentVolumeClaimVolumeSource represents a reference + to a PersistentVolumeClaim in the same namespace. More info: + https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims' + properties: + claimName: + description: 'ClaimName is the name of a PersistentVolumeClaim + in the same namespace as the pod using this volume. More + info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims' + type: string + readOnly: + description: Will force the ReadOnly setting in VolumeMounts. + Default false. + type: boolean + required: + - claimName + type: object + photonPersistentDisk: + description: PhotonPersistentDisk represents a PhotonController + persistent disk attached and mounted on kubelets host machine + properties: + fsType: + description: Filesystem type to mount. Must be a filesystem + type supported by the host operating system. Ex. "ext4", + "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + type: string + pdID: + description: ID that identifies Photon Controller persistent + disk + type: string + required: + - pdID + type: object + portworxVolume: + description: PortworxVolume represents a portworx volume attached + and mounted on kubelets host machine + properties: + fsType: + description: FSType represents the filesystem type to mount + Must be a filesystem type supported by the host operating + system. Ex. "ext4", "xfs". Implicitly inferred to be "ext4" + if unspecified. + type: string + readOnly: + description: Defaults to false (read/write). ReadOnly here + will force the ReadOnly setting in VolumeMounts. + type: boolean + volumeID: + description: VolumeID uniquely identifies a Portworx volume + type: string + required: + - volumeID + type: object + projected: + description: Items for all in one resources secrets, configmaps, + and downward API + properties: + defaultMode: + description: Mode bits to use on created files by default. + Must be a value between 0 and 0777. Directories within the + path are not affected by this setting. This might be in + conflict with other options that affect the file mode, like + fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + sources: + description: list of volume projections + items: + description: Projection that may be projected along with + other supported volume types + properties: + configMap: + description: information about the configMap data to + project + properties: + items: + description: If unspecified, each key-value pair + in the Data field of the referenced ConfigMap + will be projected into the volume as a file whose + name is the key and content is the value. If specified, + the listed keys will be projected into the specified + paths, and unlisted keys will not be present. + If a key is specified which is not present in + the ConfigMap, the volume setup will error unless + it is marked optional. Paths must be relative + and may not contain the '..' path or start with + '..'. + items: + description: Maps a string key to a path within + a volume. + properties: + key: + description: The key to project. + type: string + mode: + description: 'Optional: mode bits to use on + this file, must be a value between 0 and + 0777. If not specified, the volume defaultMode + will be used. This might be in conflict + with other options that affect the file + mode, like fsGroup, and the result can be + other mode bits set.' + format: int32 + type: integer + path: + description: The relative path of the file + to map the key to. May not be an absolute + path. May not contain the path element '..'. + May not start with the string '..'. + type: string + required: + - key + - path + type: object + type: array + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, + uid?' + type: string + optional: + description: Specify whether the ConfigMap or its + keys must be defined + type: boolean + type: object + downwardAPI: + description: information about the downwardAPI data + to project + properties: + items: + description: Items is a list of DownwardAPIVolume + file + items: + description: DownwardAPIVolumeFile represents + information to create the file containing the + pod field + properties: + fieldRef: + description: 'Required: Selects a field of + the pod: only annotations, labels, name + and namespace are supported.' + properties: + apiVersion: + description: Version of the schema the + FieldPath is written in terms of, defaults + to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + mode: + description: 'Optional: mode bits to use on + this file, must be a value between 0 and + 0777. If not specified, the volume defaultMode + will be used. This might be in conflict + with other options that affect the file + mode, like fsGroup, and the result can be + other mode bits set.' + format: int32 + type: integer + path: + description: 'Required: Path is the relative + path name of the file to be created. Must + not be absolute or contain the ''..'' path. + Must be utf-8 encoded. The first item of + the relative path must not start with ''..''' + type: string + resourceFieldRef: + description: 'Selects a resource of the container: + only resources limits and requests (limits.cpu, + limits.memory, requests.cpu and requests.memory) + are currently supported.' + properties: + containerName: + description: 'Container name: required + for volumes, optional for env vars' + type: string + divisor: + description: Specifies the output format + of the exposed resources, defaults to + "1" + type: string + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + required: + - path + type: object + type: array + type: object + secret: + description: information about the secret data to project + properties: + items: + description: If unspecified, each key-value pair + in the Data field of the referenced Secret will + be projected into the volume as a file whose name + is the key and content is the value. If specified, + the listed keys will be projected into the specified + paths, and unlisted keys will not be present. + If a key is specified which is not present in + the Secret, the volume setup will error unless + it is marked optional. Paths must be relative + and may not contain the '..' path or start with + '..'. + items: + description: Maps a string key to a path within + a volume. + properties: + key: + description: The key to project. + type: string + mode: + description: 'Optional: mode bits to use on + this file, must be a value between 0 and + 0777. If not specified, the volume defaultMode + will be used. This might be in conflict + with other options that affect the file + mode, like fsGroup, and the result can be + other mode bits set.' + format: int32 + type: integer + path: + description: The relative path of the file + to map the key to. May not be an absolute + path. May not contain the path element '..'. + May not start with the string '..'. + type: string + required: + - key + - path + type: object + type: array + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, + uid?' + type: string + optional: + description: Specify whether the Secret or its key + must be defined + type: boolean + type: object + serviceAccountToken: + description: information about the serviceAccountToken + data to project + properties: + audience: + description: Audience is the intended audience of + the token. A recipient of a token must identify + itself with an identifier specified in the audience + of the token, and otherwise should reject the + token. The audience defaults to the identifier + of the apiserver. + type: string + expirationSeconds: + description: ExpirationSeconds is the requested + duration of validity of the service account token. + As the token approaches expiration, the kubelet + volume plugin will proactively rotate the service + account token. The kubelet will start trying to + rotate the token if the token is older than 80 + percent of its time to live or if the token is + older than 24 hours.Defaults to 1 hour and must + be at least 10 minutes. + format: int64 + type: integer + path: + description: Path is the path relative to the mount + point of the file to project the token into. + type: string + required: + - path + type: object + type: object + type: array + required: + - sources + type: object + quobyte: + description: Quobyte represents a Quobyte mount on the host that + shares a pod's lifetime + properties: + group: + description: Group to map volume access to Default is no group + type: string + readOnly: + description: ReadOnly here will force the Quobyte volume to + be mounted with read-only permissions. Defaults to false. + type: boolean + registry: + description: Registry represents a single or multiple Quobyte + Registry services specified as a string as host:port pair + (multiple entries are separated with commas) which acts + as the central registry for volumes + type: string + tenant: + description: Tenant owning the given Quobyte volume in the + Backend Used with dynamically provisioned Quobyte volumes, + value is set by the plugin + type: string + user: + description: User to map volume access to Defaults to serivceaccount + user + type: string + volume: + description: Volume is a string that references an already + created Quobyte volume by name. + type: string + required: + - registry + - volume + type: object + rbd: + description: 'RBD represents a Rados Block Device mount on the + host that shares a pod''s lifetime. More info: https://examples.k8s.io/volumes/rbd/README.md' + properties: + fsType: + description: 'Filesystem type of the volume that you want + to mount. Tip: Ensure that the filesystem type is supported + by the host operating system. Examples: "ext4", "xfs", "ntfs". + Implicitly inferred to be "ext4" if unspecified. More info: + https://kubernetes.io/docs/concepts/storage/volumes#rbd + TODO: how do we prevent errors in the filesystem from compromising + the machine' + type: string + image: + description: 'The rados image name. More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it' + type: string + keyring: + description: 'Keyring is the path to key ring for RBDUser. + Default is /etc/ceph/keyring. More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it' + type: string + monitors: + description: 'A collection of Ceph monitors. More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it' + items: + type: string + type: array + pool: + description: 'The rados pool name. Default is rbd. More info: + https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it' + type: string + readOnly: + description: 'ReadOnly here will force the ReadOnly setting + in VolumeMounts. Defaults to false. More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it' + type: boolean + secretRef: + description: 'SecretRef is name of the authentication secret + for RBDUser. If provided overrides keyring. Default is nil. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it' + properties: + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + type: object + user: + description: 'The rados user name. Default is admin. More + info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it' + type: string + required: + - image + - monitors + type: object + scaleIO: + description: ScaleIO represents a ScaleIO persistent volume attached + and mounted on Kubernetes nodes. + properties: + fsType: + description: Filesystem type to mount. Must be a filesystem + type supported by the host operating system. Ex. "ext4", + "xfs", "ntfs". Default is "xfs". + type: string + gateway: + description: The host address of the ScaleIO API Gateway. + type: string + protectionDomain: + description: The name of the ScaleIO Protection Domain for + the configured storage. + type: string + readOnly: + description: Defaults to false (read/write). ReadOnly here + will force the ReadOnly setting in VolumeMounts. + type: boolean + secretRef: + description: SecretRef references to the secret for ScaleIO + user and other sensitive information. If this is not provided, + Login operation will fail. + properties: + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + type: object + sslEnabled: + description: Flag to enable/disable SSL communication with + Gateway, default false + type: boolean + storageMode: + description: Indicates whether the storage for a volume should + be ThickProvisioned or ThinProvisioned. Default is ThinProvisioned. + type: string + storagePool: + description: The ScaleIO Storage Pool associated with the + protection domain. + type: string + system: + description: The name of the storage system as configured + in ScaleIO. + type: string + volumeName: + description: The name of a volume already created in the ScaleIO + system that is associated with this volume source. + type: string + required: + - gateway + - secretRef + - system + type: object + secret: + description: 'Secret represents a secret that should populate + this volume. More info: https://kubernetes.io/docs/concepts/storage/volumes#secret' + properties: + defaultMode: + description: 'Optional: mode bits to use on created files + by default. Must be a value between 0 and 0777. Defaults + to 0644. Directories within the path are not affected by + this setting. This might be in conflict with other options + that affect the file mode, like fsGroup, and the result + can be other mode bits set.' + format: int32 + type: integer + items: + description: If unspecified, each key-value pair in the Data + field of the referenced Secret will be projected into the + volume as a file whose name is the key and content is the + value. If specified, the listed keys will be projected into + the specified paths, and unlisted keys will not be present. + If a key is specified which is not present in the Secret, + the volume setup will error unless it is marked optional. + Paths must be relative and may not contain the '..' path + or start with '..'. + items: + description: Maps a string key to a path within a volume. + properties: + key: + description: The key to project. + type: string + mode: + description: 'Optional: mode bits to use on this file, + must be a value between 0 and 0777. If not specified, + the volume defaultMode will be used. This might be + in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode + bits set.' + format: int32 + type: integer + path: + description: The relative path of the file to map the + key to. May not be an absolute path. May not contain + the path element '..'. May not start with the string + '..'. + type: string + required: + - key + - path + type: object + type: array + optional: + description: Specify whether the Secret or its keys must be + defined + type: boolean + secretName: + description: 'Name of the secret in the pod''s namespace to + use. More info: https://kubernetes.io/docs/concepts/storage/volumes#secret' + type: string + type: object + storageos: + description: StorageOS represents a StorageOS volume attached + and mounted on Kubernetes nodes. + properties: + fsType: + description: Filesystem type to mount. Must be a filesystem + type supported by the host operating system. Ex. "ext4", + "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + type: string + readOnly: + description: Defaults to false (read/write). ReadOnly here + will force the ReadOnly setting in VolumeMounts. + type: boolean + secretRef: + description: SecretRef specifies the secret to use for obtaining + the StorageOS API credentials. If not specified, default + values will be attempted. + properties: + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + type: object + volumeName: + description: VolumeName is the human-readable name of the + StorageOS volume. Volume names are only unique within a + namespace. + type: string + volumeNamespace: + description: VolumeNamespace specifies the scope of the volume + within StorageOS. If no namespace is specified then the + Pod's namespace will be used. This allows the Kubernetes + name scoping to be mirrored within StorageOS for tighter + integration. Set VolumeName to any name to override the + default behaviour. Set to "default" if you are not using + namespaces within StorageOS. Namespaces that do not pre-exist + within StorageOS will be created. + type: string + type: object + vsphereVolume: + description: VsphereVolume represents a vSphere volume attached + and mounted on kubelets host machine + properties: + fsType: + description: Filesystem type to mount. Must be a filesystem + type supported by the host operating system. Ex. "ext4", + "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + type: string + storagePolicyID: + description: Storage Policy Based Management (SPBM) profile + ID associated with the StoragePolicyName. + type: string + storagePolicyName: + description: Storage Policy Based Management (SPBM) profile + name. + type: string + volumePath: + description: Path that identifies vSphere volume vmdk + type: string + required: + - volumePath + type: object + required: + - name + type: object + type: array + type: object + status: + description: ClusterMasterStatus defines the observed state of ClusterMaster + properties: + bundlePushInfo: + description: Bundle push status tracker + properties: + lastCheckInterval: + format: int64 + type: integer + needToPushMasterApps: + type: boolean + type: object + phase: + description: current phase of the cluster master + enum: + - Pending + - Ready + - Updating + - ScalingUp + - ScalingDown + - Terminating + - Error + type: string + resourceRevMap: + additionalProperties: + type: string + description: Resource Revision tracker + type: object + selector: + description: selector for pods, used by HorizontalPodAutoscaler + type: string + smartstore: + description: Splunk Smartstore configuration. Refer to indexes.conf.spec + and server.conf.spec on docs.splunk.com + properties: + cacheManager: + description: Defines Cache manager settings + properties: + evictionPadding: + description: Additional size beyond 'minFreeSize' before eviction + kicks in + type: integer + evictionPolicy: + description: Eviction policy to use + type: string + hotlistBloomFilterRecencyHours: + description: Time period relative to the bucket's age, during + which the bloom filter file is protected from cache eviction + type: integer + hotlistRecencySecs: + description: Time period relative to the bucket's age, during + which the bucket is protected from cache eviction + type: integer + maxCacheSize: + description: Max cache size per partition + type: integer + maxConcurrentDownloads: + description: Maximum number of buckets that can be downloaded + from remote storage in parallel + type: integer + maxConcurrentUploads: + description: Maximum number of buckets that can be uploaded + to remote storage in parallel + type: integer + type: object + defaults: + description: Default configuration for indexes + properties: + maxGlobalDataSizeMB: + description: MaxGlobalDataSizeMB defines the maximum amount + of space for warm and cold buckets of an index + type: integer + maxGlobalRawDataSizeMB: + description: MaxGlobalDataSizeMB defines the maximum amount + of cumulative space for warm and cold buckets of an index + type: integer + volumeName: + description: Remote Volume name + type: string + type: object + indexes: + description: List of Splunk indexes + items: + description: IndexSpec defines Splunk index name and storage path + properties: + hotlistBloomFilterRecencyHours: + description: Time period relative to the bucket's age, during + which the bloom filter file is protected from cache eviction + type: integer + hotlistRecencySecs: + description: Time period relative to the bucket's age, during + which the bucket is protected from cache eviction + type: integer + maxGlobalDataSizeMB: + description: MaxGlobalDataSizeMB defines the maximum amount + of space for warm and cold buckets of an index + type: integer + maxGlobalRawDataSizeMB: + description: MaxGlobalDataSizeMB defines the maximum amount + of cumulative space for warm and cold buckets of an index + type: integer + name: + description: Splunk index name + type: string + remotePath: + description: Index location relative to the remote volume + path + type: string + volumeName: + description: Remote Volume name + type: string + type: object + type: array + volumes: + description: List of remote storage volumes + items: + description: VolumeSpec defines remote volume name and remote + volume URI + properties: + endpoint: + description: Remote volume URI + type: string + name: + description: Remote volume name + type: string + path: + description: Remote volume path + type: string + secretRef: + description: Secret object name + type: string + type: object + type: array + type: object + type: object + type: object + version: v1beta1 + versions: + - name: v1beta1 + served: true + storage: true + - name: v1alpha3 + served: true + storage: false + - name: v1alpha2 + served: true + storage: false diff --git a/deploy/olm-catalog/splunk/0.2.2/enterprise.splunk.com_indexerclusters_crd.yaml b/deploy/olm-catalog/splunk/0.2.2/enterprise.splunk.com_indexerclusters_crd.yaml new file mode 100644 index 000000000..9bb2bce3b --- /dev/null +++ b/deploy/olm-catalog/splunk/0.2.2/enterprise.splunk.com_indexerclusters_crd.yaml @@ -0,0 +1,2364 @@ +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: indexerclusters.enterprise.splunk.com +spec: + additionalPrinterColumns: + - JSONPath: .status.phase + description: Status of indexer cluster + name: Phase + type: string + - JSONPath: .status.clusterMasterPhase + description: Status of cluster master + name: Master + type: string + - JSONPath: .status.replicas + description: Desired number of indexer peers + name: Desired + type: integer + - JSONPath: .status.readyReplicas + description: Current number of ready indexer peers + name: Ready + type: integer + - JSONPath: .metadata.creationTimestamp + description: Age of indexer cluster + name: Age + type: date + group: enterprise.splunk.com + names: + kind: IndexerCluster + listKind: IndexerClusterList + plural: indexerclusters + shortNames: + - idc + - idxc + singular: indexercluster + scope: Namespaced + subresources: + scale: + labelSelectorPath: .status.selector + specReplicasPath: .spec.replicas + statusReplicasPath: .status.replicas + status: {} + validation: + openAPIV3Schema: + description: IndexerCluster is the Schema for a Splunk Enterprise indexer cluster + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: IndexerClusterSpec defines the desired state of a Splunk Enterprise + indexer cluster + properties: + Mock: + description: Mock to differentiate between UTs and actual reconcile + type: boolean + affinity: + description: Kubernetes Affinity rules that control how pods are assigned + to particular nodes. + properties: + nodeAffinity: + description: Describes node affinity scheduling rules for the pod. + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: The scheduler will prefer to schedule pods to nodes + that satisfy the affinity expressions specified by this field, + but it may choose a node that violates one or more of the + expressions. The node that is most preferred is the one with + the greatest sum of weights, i.e. for each node that meets + all of the scheduling requirements (resource request, requiredDuringScheduling + affinity expressions, etc.), compute a sum by iterating through + the elements of this field and adding "weight" to the sum + if the node matches the corresponding matchExpressions; the + node(s) with the highest sum are the most preferred. + items: + description: An empty preferred scheduling term matches all + objects with implicit weight 0 (i.e. it's a no-op). A null + preferred scheduling term matches no objects (i.e. is also + a no-op). + properties: + preference: + description: A node selector term, associated with the + corresponding weight. + properties: + matchExpressions: + description: A list of node selector requirements + by node's labels. + items: + description: A node selector requirement is a selector + that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: Represents a key's relationship + to a set of values. Valid operators are In, + NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: An array of string values. If the + operator is In or NotIn, the values array + must be non-empty. If the operator is Exists + or DoesNotExist, the values array must be + empty. If the operator is Gt or Lt, the values + array must have a single element, which will + be interpreted as an integer. This array is + replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchFields: + description: A list of node selector requirements + by node's fields. + items: + description: A node selector requirement is a selector + that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: Represents a key's relationship + to a set of values. Valid operators are In, + NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: An array of string values. If the + operator is In or NotIn, the values array + must be non-empty. If the operator is Exists + or DoesNotExist, the values array must be + empty. If the operator is Gt or Lt, the values + array must have a single element, which will + be interpreted as an integer. This array is + replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + type: object + weight: + description: Weight associated with matching the corresponding + nodeSelectorTerm, in the range 1-100. + format: int32 + type: integer + required: + - preference + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + description: If the affinity requirements specified by this + field are not met at scheduling time, the pod will not be + scheduled onto the node. If the affinity requirements specified + by this field cease to be met at some point during pod execution + (e.g. due to an update), the system may or may not try to + eventually evict the pod from its node. + properties: + nodeSelectorTerms: + description: Required. A list of node selector terms. The + terms are ORed. + items: + description: A null or empty node selector term matches + no objects. The requirements of them are ANDed. The + TopologySelectorTerm type implements a subset of the + NodeSelectorTerm. + properties: + matchExpressions: + description: A list of node selector requirements + by node's labels. + items: + description: A node selector requirement is a selector + that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: Represents a key's relationship + to a set of values. Valid operators are In, + NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: An array of string values. If the + operator is In or NotIn, the values array + must be non-empty. If the operator is Exists + or DoesNotExist, the values array must be + empty. If the operator is Gt or Lt, the values + array must have a single element, which will + be interpreted as an integer. This array is + replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchFields: + description: A list of node selector requirements + by node's fields. + items: + description: A node selector requirement is a selector + that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: Represents a key's relationship + to a set of values. Valid operators are In, + NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: An array of string values. If the + operator is In or NotIn, the values array + must be non-empty. If the operator is Exists + or DoesNotExist, the values array must be + empty. If the operator is Gt or Lt, the values + array must have a single element, which will + be interpreted as an integer. This array is + replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + type: object + type: array + required: + - nodeSelectorTerms + type: object + type: object + podAffinity: + description: Describes pod affinity scheduling rules (e.g. co-locate + this pod in the same node, zone, etc. as some other pod(s)). + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: The scheduler will prefer to schedule pods to nodes + that satisfy the affinity expressions specified by this field, + but it may choose a node that violates one or more of the + expressions. The node that is most preferred is the one with + the greatest sum of weights, i.e. for each node that meets + all of the scheduling requirements (resource request, requiredDuringScheduling + affinity expressions, etc.), compute a sum by iterating through + the elements of this field and adding "weight" to the sum + if the node has pods which matches the corresponding podAffinityTerm; + the node(s) with the highest sum are the most preferred. + items: + description: The weights of all of the matched WeightedPodAffinityTerm + fields are added per-node to find the most preferred node(s) + properties: + podAffinityTerm: + description: Required. A pod affinity term, associated + with the corresponding weight. + properties: + labelSelector: + description: A label query over a set of resources, + in this case pods. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: A label selector requirement is + a selector that contains values, a key, and + an operator that relates the key and values. + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: operator represents a key's + relationship to a set of values. Valid + operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: values is an array of string + values. If the operator is In or NotIn, + the values array must be non-empty. If + the operator is Exists or DoesNotExist, + the values array must be empty. This array + is replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, + whose key field is "key", the operator is "In", + and the values array contains only "value". + The requirements are ANDed. + type: object + type: object + namespaces: + description: namespaces specifies which namespaces + the labelSelector applies to (matches against); + null or empty list means "this pod's namespace" + items: + type: string + type: array + topologyKey: + description: This pod should be co-located (affinity) + or not co-located (anti-affinity) with the pods + matching the labelSelector in the specified namespaces, + where co-located is defined as running on a node + whose value of the label with key topologyKey matches + that of any node on which any of the selected pods + is running. Empty topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + weight: + description: weight associated with matching the corresponding + podAffinityTerm, in the range 1-100. + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + description: If the affinity requirements specified by this + field are not met at scheduling time, the pod will not be + scheduled onto the node. If the affinity requirements specified + by this field cease to be met at some point during pod execution + (e.g. due to a pod label update), the system may or may not + try to eventually evict the pod from its node. When there + are multiple elements, the lists of nodes corresponding to + each podAffinityTerm are intersected, i.e. all terms must + be satisfied. + items: + description: Defines a set of pods (namely those matching + the labelSelector relative to the given namespace(s)) that + this pod should be co-located (affinity) or not co-located + (anti-affinity) with, where co-located is defined as running + on a node whose value of the label with key + matches that of any node on which a pod of the set of pods + is running + properties: + labelSelector: + description: A label query over a set of resources, in + this case pods. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: A label selector requirement is a selector + that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: operator represents a key's relationship + to a set of values. Valid operators are In, + NotIn, Exists and DoesNotExist. + type: string + values: + description: values is an array of string values. + If the operator is In or NotIn, the values + array must be non-empty. If the operator is + Exists or DoesNotExist, the values array must + be empty. This array is replaced during a + strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} pairs. + A single {key,value} in the matchLabels map is equivalent + to an element of matchExpressions, whose key field + is "key", the operator is "In", and the values array + contains only "value". The requirements are ANDed. + type: object + type: object + namespaces: + description: namespaces specifies which namespaces the + labelSelector applies to (matches against); null or + empty list means "this pod's namespace" + items: + type: string + type: array + topologyKey: + description: This pod should be co-located (affinity) + or not co-located (anti-affinity) with the pods matching + the labelSelector in the specified namespaces, where + co-located is defined as running on a node whose value + of the label with key topologyKey matches that of any + node on which any of the selected pods is running. Empty + topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + type: array + type: object + podAntiAffinity: + description: Describes pod anti-affinity scheduling rules (e.g. + avoid putting this pod in the same node, zone, etc. as some other + pod(s)). + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: The scheduler will prefer to schedule pods to nodes + that satisfy the anti-affinity expressions specified by this + field, but it may choose a node that violates one or more + of the expressions. The node that is most preferred is the + one with the greatest sum of weights, i.e. for each node that + meets all of the scheduling requirements (resource request, + requiredDuringScheduling anti-affinity expressions, etc.), + compute a sum by iterating through the elements of this field + and adding "weight" to the sum if the node has pods which + matches the corresponding podAffinityTerm; the node(s) with + the highest sum are the most preferred. + items: + description: The weights of all of the matched WeightedPodAffinityTerm + fields are added per-node to find the most preferred node(s) + properties: + podAffinityTerm: + description: Required. A pod affinity term, associated + with the corresponding weight. + properties: + labelSelector: + description: A label query over a set of resources, + in this case pods. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: A label selector requirement is + a selector that contains values, a key, and + an operator that relates the key and values. + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: operator represents a key's + relationship to a set of values. Valid + operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: values is an array of string + values. If the operator is In or NotIn, + the values array must be non-empty. If + the operator is Exists or DoesNotExist, + the values array must be empty. This array + is replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, + whose key field is "key", the operator is "In", + and the values array contains only "value". + The requirements are ANDed. + type: object + type: object + namespaces: + description: namespaces specifies which namespaces + the labelSelector applies to (matches against); + null or empty list means "this pod's namespace" + items: + type: string + type: array + topologyKey: + description: This pod should be co-located (affinity) + or not co-located (anti-affinity) with the pods + matching the labelSelector in the specified namespaces, + where co-located is defined as running on a node + whose value of the label with key topologyKey matches + that of any node on which any of the selected pods + is running. Empty topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + weight: + description: weight associated with matching the corresponding + podAffinityTerm, in the range 1-100. + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + description: If the anti-affinity requirements specified by + this field are not met at scheduling time, the pod will not + be scheduled onto the node. If the anti-affinity requirements + specified by this field cease to be met at some point during + pod execution (e.g. due to a pod label update), the system + may or may not try to eventually evict the pod from its node. + When there are multiple elements, the lists of nodes corresponding + to each podAffinityTerm are intersected, i.e. all terms must + be satisfied. + items: + description: Defines a set of pods (namely those matching + the labelSelector relative to the given namespace(s)) that + this pod should be co-located (affinity) or not co-located + (anti-affinity) with, where co-located is defined as running + on a node whose value of the label with key + matches that of any node on which a pod of the set of pods + is running + properties: + labelSelector: + description: A label query over a set of resources, in + this case pods. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: A label selector requirement is a selector + that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: operator represents a key's relationship + to a set of values. Valid operators are In, + NotIn, Exists and DoesNotExist. + type: string + values: + description: values is an array of string values. + If the operator is In or NotIn, the values + array must be non-empty. If the operator is + Exists or DoesNotExist, the values array must + be empty. This array is replaced during a + strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} pairs. + A single {key,value} in the matchLabels map is equivalent + to an element of matchExpressions, whose key field + is "key", the operator is "In", and the values array + contains only "value". The requirements are ANDed. + type: object + type: object + namespaces: + description: namespaces specifies which namespaces the + labelSelector applies to (matches against); null or + empty list means "this pod's namespace" + items: + type: string + type: array + topologyKey: + description: This pod should be co-located (affinity) + or not co-located (anti-affinity) with the pods matching + the labelSelector in the specified namespaces, where + co-located is defined as running on a node whose value + of the label with key topologyKey matches that of any + node on which any of the selected pods is running. Empty + topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + type: array + type: object + type: object + clusterMasterRef: + description: ClusterMasterRef refers to a Splunk Enterprise indexer + cluster managed by the operator within Kubernetes + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: 'If referring to a piece of an object instead of an + entire object, this string should contain a valid JSON/Go field + access statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to a container within + a pod, this would take on a value like: "spec.containers{name}" + (where "name" refers to the name of the container that triggered + the event) or if no container name is specified "spec.containers[2]" + (container with index 2 in this pod). This syntax is chosen only + to have some well-defined way of referencing a part of an object. + TODO: this design is not final and this field is subject to change + in the future.' + type: string + kind: + description: 'Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + namespace: + description: 'Namespace of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/' + type: string + resourceVersion: + description: 'Specific resourceVersion to which this reference is + made, if any. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency' + type: string + uid: + description: 'UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids' + type: string + type: object + defaults: + description: Inline map of default.yml overrides used to initialize + the environment + type: string + defaultsUrl: + description: Full path or URL for one or more default.yml files, separated + by commas + type: string + defaultsUrlApps: + description: Full path or URL for one or more defaults.yml files specific + to App install, separated by commas. The defaults listed here will + be installed on the CM, standalone, search head deployer or license + master instance. + type: string + etcVolumeStorageConfig: + description: Storage configuration for /opt/splunk/etc volume + properties: + ephemeralStorage: + description: If true, ephemeral (emptyDir) storage will be used + type: boolean + storageCapacity: + description: Storage capacity to request persistent volume claims + (default=”10Gi” for etc and "100Gi" for var) + type: string + storageClassName: + description: Name of StorageClass to use for persistent volume claims + type: string + type: object + image: + description: Image to use for Splunk pod containers (overrides RELATED_IMAGE_SPLUNK_ENTERPRISE + environment variables) + type: string + imagePullPolicy: + description: 'Sets pull policy for all images (either “Always” or the + default: “IfNotPresent”)' + enum: + - Always + - IfNotPresent + type: string + licenseMasterRef: + description: LicenseMasterRef refers to a Splunk Enterprise license + master managed by the operator within Kubernetes + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: 'If referring to a piece of an object instead of an + entire object, this string should contain a valid JSON/Go field + access statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to a container within + a pod, this would take on a value like: "spec.containers{name}" + (where "name" refers to the name of the container that triggered + the event) or if no container name is specified "spec.containers[2]" + (container with index 2 in this pod). This syntax is chosen only + to have some well-defined way of referencing a part of an object. + TODO: this design is not final and this field is subject to change + in the future.' + type: string + kind: + description: 'Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + namespace: + description: 'Namespace of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/' + type: string + resourceVersion: + description: 'Specific resourceVersion to which this reference is + made, if any. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency' + type: string + uid: + description: 'UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids' + type: string + type: object + licenseUrl: + description: Full path or URL for a Splunk Enterprise license file + type: string + replicas: + description: Number of search head pods; a search head cluster will + be created if > 1 + format: int32 + type: integer + resources: + description: resource requirements for the pod containers + properties: + limits: + additionalProperties: + type: string + description: 'Limits describes the maximum amount of compute resources + allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/' + type: object + requests: + additionalProperties: + type: string + description: 'Requests describes the minimum amount of compute resources + required. If Requests is omitted for a container, it defaults + to Limits if that is explicitly specified, otherwise to an implementation-defined + value. More info: https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/' + type: object + type: object + schedulerName: + description: Name of Scheduler to use for pod placement (defaults to + “default-scheduler”) + type: string + serviceAccount: + description: ServiceAccount is the service account used by the pods + deployed by the CRD. If not specified uses the default serviceAccount + for the namespace as per https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/#use-the-default-service-account-to-access-the-api-server + type: string + serviceTemplate: + description: ServiceTemplate is a template used to create Kubernetes + services + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the + latest internal value, and may reject unrecognized values. More + info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource + this object represents. Servers may infer this from the endpoint + the client submits requests to. Cannot be updated. In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + description: 'Standard object''s metadata. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata' + type: object + spec: + description: Spec defines the behavior of a service. https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status + properties: + clusterIP: + description: 'clusterIP is the IP address of the service and + is usually assigned randomly by the master. If an address + is specified manually and is not in use by others, it will + be allocated to the service; otherwise, creation of the service + will fail. This field can not be changed through updates. + Valid values are "None", empty string (""), or a valid IP + address. "None" can be specified for headless services when + proxying is not required. Only applies to types ClusterIP, + NodePort, and LoadBalancer. Ignored if type is ExternalName. + More info: https://kubernetes.io/docs/concepts/services-networking/service/#virtual-ips-and-service-proxies' + type: string + externalIPs: + description: externalIPs is a list of IP addresses for which + nodes in the cluster will also accept traffic for this service. These + IPs are not managed by Kubernetes. The user is responsible + for ensuring that traffic arrives at a node with this IP. A + common example is external load-balancers that are not part + of the Kubernetes system. + items: + type: string + type: array + externalName: + description: externalName is the external reference that kubedns + or equivalent will return as a CNAME record for this service. + No proxying will be involved. Must be a valid RFC-1123 hostname + (https://tools.ietf.org/html/rfc1123) and requires Type to + be ExternalName. + type: string + externalTrafficPolicy: + description: externalTrafficPolicy denotes if this Service desires + to route external traffic to node-local or cluster-wide endpoints. + "Local" preserves the client source IP and avoids a second + hop for LoadBalancer and Nodeport type services, but risks + potentially imbalanced traffic spreading. "Cluster" obscures + the client source IP and may cause a second hop to another + node, but should have good overall load-spreading. + type: string + healthCheckNodePort: + description: healthCheckNodePort specifies the healthcheck nodePort + for the service. If not specified, HealthCheckNodePort is + created by the service api backend with the allocated nodePort. + Will use user-specified nodePort value if specified by the + client. Only effects when Type is set to LoadBalancer and + ExternalTrafficPolicy is set to Local. + format: int32 + type: integer + ipFamily: + description: ipFamily specifies whether this Service has a preference + for a particular IP family (e.g. IPv4 vs. IPv6). If a specific + IP family is requested, the clusterIP field will be allocated + from that family, if it is available in the cluster. If no + IP family is requested, the cluster's primary IP family will + be used. Other IP fields (loadBalancerIP, loadBalancerSourceRanges, + externalIPs) and controllers which allocate external load-balancers + should use the same IP family. Endpoints for this Service + will be of this family. This field is immutable after creation. + Assigning a ServiceIPFamily not available in the cluster (e.g. + IPv6 in IPv4 only cluster) is an error condition and will + fail during clusterIP assignment. + type: string + loadBalancerIP: + description: 'Only applies to Service Type: LoadBalancer LoadBalancer + will get created with the IP specified in this field. This + feature depends on whether the underlying cloud-provider supports + specifying the loadBalancerIP when a load balancer is created. + This field will be ignored if the cloud-provider does not + support the feature.' + type: string + loadBalancerSourceRanges: + description: 'If specified and supported by the platform, this + will restrict traffic through the cloud-provider load-balancer + will be restricted to the specified client IPs. This field + will be ignored if the cloud-provider does not support the + feature." More info: https://kubernetes.io/docs/tasks/access-application-cluster/configure-cloud-provider-firewall/' + items: + type: string + type: array + ports: + description: 'The list of ports that are exposed by this service. + More info: https://kubernetes.io/docs/concepts/services-networking/service/#virtual-ips-and-service-proxies' + items: + description: ServicePort contains information on service's + port. + properties: + name: + description: The name of this port within the service. + This must be a DNS_LABEL. All ports within a ServiceSpec + must have unique names. When considering the endpoints + for a Service, this must match the 'name' field in the + EndpointPort. Optional if only one ServicePort is defined + on this service. + type: string + nodePort: + description: 'The port on each node on which this service + is exposed when type=NodePort or LoadBalancer. Usually + assigned by the system. If specified, it will be allocated + to the service if unused or else creation of the service + will fail. Default is to auto-allocate a port if the + ServiceType of this Service requires one. More info: + https://kubernetes.io/docs/concepts/services-networking/service/#type-nodeport' + format: int32 + type: integer + port: + description: The port that will be exposed by this service. + format: int32 + type: integer + protocol: + description: The IP protocol for this port. Supports "TCP", + "UDP", and "SCTP". Default is TCP. + type: string + targetPort: + anyOf: + - type: integer + - type: string + description: 'Number or name of the port to access on + the pods targeted by the service. Number must be in + the range 1 to 65535. Name must be an IANA_SVC_NAME. + If this is a string, it will be looked up as a named + port in the target Pod''s container ports. If this is + not specified, the value of the ''port'' field is used + (an identity map). This field is ignored for services + with clusterIP=None, and should be omitted or set equal + to the ''port'' field. More info: https://kubernetes.io/docs/concepts/services-networking/service/#defining-a-service' + x-kubernetes-int-or-string: true + required: + - port + type: object + type: array + publishNotReadyAddresses: + description: publishNotReadyAddresses, when set to true, indicates + that DNS implementations must publish the notReadyAddresses + of subsets for the Endpoints associated with the Service. + The default value is false. The primary use case for setting + this field is to use a StatefulSet's Headless Service to propagate + SRV records for its Pods without respect to their readiness + for purpose of peer discovery. + type: boolean + selector: + additionalProperties: + type: string + description: 'Route service traffic to pods with label keys + and values matching this selector. If empty or not present, + the service is assumed to have an external process managing + its endpoints, which Kubernetes will not modify. Only applies + to types ClusterIP, NodePort, and LoadBalancer. Ignored if + type is ExternalName. More info: https://kubernetes.io/docs/concepts/services-networking/service/' + type: object + sessionAffinity: + description: 'Supports "ClientIP" and "None". Used to maintain + session affinity. Enable client IP based session affinity. + Must be ClientIP or None. Defaults to None. More info: https://kubernetes.io/docs/concepts/services-networking/service/#virtual-ips-and-service-proxies' + type: string + sessionAffinityConfig: + description: sessionAffinityConfig contains the configurations + of session affinity. + properties: + clientIP: + description: clientIP contains the configurations of Client + IP based session affinity. + properties: + timeoutSeconds: + description: timeoutSeconds specifies the seconds of + ClientIP type session sticky time. The value must + be >0 && <=86400(for 1 day) if ServiceAffinity == + "ClientIP". Default value is 10800(for 3 hours). + format: int32 + type: integer + type: object + type: object + type: + description: 'type determines how the Service is exposed. Defaults + to ClusterIP. Valid options are ExternalName, ClusterIP, NodePort, + and LoadBalancer. "ExternalName" maps to the specified externalName. + "ClusterIP" allocates a cluster-internal IP address for load-balancing + to endpoints. Endpoints are determined by the selector or + if that is not specified, by manual construction of an Endpoints + object. If clusterIP is "None", no virtual IP is allocated + and the endpoints are published as a set of endpoints rather + than a stable IP. "NodePort" builds on ClusterIP and allocates + a port on every node which routes to the clusterIP. "LoadBalancer" + builds on NodePort and creates an external load-balancer (if + supported in the current cloud) which routes to the clusterIP. + More info: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types' + type: string + type: object + status: + description: 'Most recently observed status of the service. Populated + by the system. Read-only. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status' + properties: + loadBalancer: + description: LoadBalancer contains the current status of the + load-balancer, if one is present. + properties: + ingress: + description: Ingress is a list containing ingress points + for the load-balancer. Traffic intended for the service + should be sent to these ingress points. + items: + description: 'LoadBalancerIngress represents the status + of a load-balancer ingress point: traffic intended for + the service should be sent to an ingress point.' + properties: + hostname: + description: Hostname is set for load-balancer ingress + points that are DNS based (typically AWS load-balancers) + type: string + ip: + description: IP is set for load-balancer ingress points + that are IP based (typically GCE or OpenStack load-balancers) + type: string + type: object + type: array + type: object + type: object + type: object + tolerations: + description: Pod's tolerations for Kubernetes node's taint + items: + description: The pod this Toleration is attached to tolerates any + taint that matches the triple using the matching + operator . + properties: + effect: + description: Effect indicates the taint effect to match. Empty + means match all taint effects. When specified, allowed values + are NoSchedule, PreferNoSchedule and NoExecute. + type: string + key: + description: Key is the taint key that the toleration applies + to. Empty means match all taint keys. If the key is empty, operator + must be Exists; this combination means to match all values and + all keys. + type: string + operator: + description: Operator represents a key's relationship to the value. + Valid operators are Exists and Equal. Defaults to Equal. Exists + is equivalent to wildcard for value, so that a pod can tolerate + all taints of a particular category. + type: string + tolerationSeconds: + description: TolerationSeconds represents the period of time the + toleration (which must be of effect NoExecute, otherwise this + field is ignored) tolerates the taint. By default, it is not + set, which means tolerate the taint forever (do not evict). + Zero and negative values will be treated as 0 (evict immediately) + by the system. + format: int64 + type: integer + value: + description: Value is the taint value the toleration matches to. + If the operator is Exists, the value should be empty, otherwise + just a regular string. + type: string + type: object + type: array + varVolumeStorageConfig: + description: Storage configuration for /opt/splunk/var volume + properties: + ephemeralStorage: + description: If true, ephemeral (emptyDir) storage will be used + type: boolean + storageCapacity: + description: Storage capacity to request persistent volume claims + (default=”10Gi” for etc and "100Gi" for var) + type: string + storageClassName: + description: Name of StorageClass to use for persistent volume claims + type: string + type: object + volumes: + description: List of one or more Kubernetes volumes. These will be mounted + in all pod containers as as /mnt/ + items: + description: Volume represents a named volume in a pod that may be + accessed by any container in the pod. + properties: + awsElasticBlockStore: + description: 'AWSElasticBlockStore represents an AWS Disk resource + that is attached to a kubelet''s host machine and then exposed + to the pod. More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore' + properties: + fsType: + description: 'Filesystem type of the volume that you want + to mount. Tip: Ensure that the filesystem type is supported + by the host operating system. Examples: "ext4", "xfs", "ntfs". + Implicitly inferred to be "ext4" if unspecified. More info: + https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore + TODO: how do we prevent errors in the filesystem from compromising + the machine' + type: string + partition: + description: 'The partition in the volume that you want to + mount. If omitted, the default is to mount by volume name. + Examples: For volume /dev/sda1, you specify the partition + as "1". Similarly, the volume partition for /dev/sda is + "0" (or you can leave the property empty).' + format: int32 + type: integer + readOnly: + description: 'Specify "true" to force and set the ReadOnly + property in VolumeMounts to "true". If omitted, the default + is "false". More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore' + type: boolean + volumeID: + description: 'Unique ID of the persistent disk resource in + AWS (Amazon EBS volume). More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore' + type: string + required: + - volumeID + type: object + azureDisk: + description: AzureDisk represents an Azure Data Disk mount on + the host and bind mount to the pod. + properties: + cachingMode: + description: 'Host Caching mode: None, Read Only, Read Write.' + type: string + diskName: + description: The Name of the data disk in the blob storage + type: string + diskURI: + description: The URI the data disk in the blob storage + type: string + fsType: + description: Filesystem type to mount. Must be a filesystem + type supported by the host operating system. Ex. "ext4", + "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + type: string + kind: + description: 'Expected values Shared: multiple blob disks + per storage account Dedicated: single blob disk per storage + account Managed: azure managed data disk (only in managed + availability set). defaults to shared' + type: string + readOnly: + description: Defaults to false (read/write). ReadOnly here + will force the ReadOnly setting in VolumeMounts. + type: boolean + required: + - diskName + - diskURI + type: object + azureFile: + description: AzureFile represents an Azure File Service mount + on the host and bind mount to the pod. + properties: + readOnly: + description: Defaults to false (read/write). ReadOnly here + will force the ReadOnly setting in VolumeMounts. + type: boolean + secretName: + description: the name of secret that contains Azure Storage + Account Name and Key + type: string + shareName: + description: Share Name + type: string + required: + - secretName + - shareName + type: object + cephfs: + description: CephFS represents a Ceph FS mount on the host that + shares a pod's lifetime + properties: + monitors: + description: 'Required: Monitors is a collection of Ceph monitors + More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it' + items: + type: string + type: array + path: + description: 'Optional: Used as the mounted root, rather than + the full Ceph tree, default is /' + type: string + readOnly: + description: 'Optional: Defaults to false (read/write). ReadOnly + here will force the ReadOnly setting in VolumeMounts. More + info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it' + type: boolean + secretFile: + description: 'Optional: SecretFile is the path to key ring + for User, default is /etc/ceph/user.secret More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it' + type: string + secretRef: + description: 'Optional: SecretRef is reference to the authentication + secret for User, default is empty. More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it' + properties: + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + type: object + user: + description: 'Optional: User is the rados user name, default + is admin More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it' + type: string + required: + - monitors + type: object + cinder: + description: 'Cinder represents a cinder volume attached and mounted + on kubelets host machine. More info: https://examples.k8s.io/mysql-cinder-pd/README.md' + properties: + fsType: + description: 'Filesystem type to mount. Must be a filesystem + type supported by the host operating system. Examples: "ext4", + "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + More info: https://examples.k8s.io/mysql-cinder-pd/README.md' + type: string + readOnly: + description: 'Optional: Defaults to false (read/write). ReadOnly + here will force the ReadOnly setting in VolumeMounts. More + info: https://examples.k8s.io/mysql-cinder-pd/README.md' + type: boolean + secretRef: + description: 'Optional: points to a secret object containing + parameters used to connect to OpenStack.' + properties: + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + type: object + volumeID: + description: 'volume id used to identify the volume in cinder. + More info: https://examples.k8s.io/mysql-cinder-pd/README.md' + type: string + required: + - volumeID + type: object + configMap: + description: ConfigMap represents a configMap that should populate + this volume + properties: + defaultMode: + description: 'Optional: mode bits to use on created files + by default. Must be a value between 0 and 0777. Defaults + to 0644. Directories within the path are not affected by + this setting. This might be in conflict with other options + that affect the file mode, like fsGroup, and the result + can be other mode bits set.' + format: int32 + type: integer + items: + description: If unspecified, each key-value pair in the Data + field of the referenced ConfigMap will be projected into + the volume as a file whose name is the key and content is + the value. If specified, the listed keys will be projected + into the specified paths, and unlisted keys will not be + present. If a key is specified which is not present in the + ConfigMap, the volume setup will error unless it is marked + optional. Paths must be relative and may not contain the + '..' path or start with '..'. + items: + description: Maps a string key to a path within a volume. + properties: + key: + description: The key to project. + type: string + mode: + description: 'Optional: mode bits to use on this file, + must be a value between 0 and 0777. If not specified, + the volume defaultMode will be used. This might be + in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode + bits set.' + format: int32 + type: integer + path: + description: The relative path of the file to map the + key to. May not be an absolute path. May not contain + the path element '..'. May not start with the string + '..'. + type: string + required: + - key + - path + type: object + type: array + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + optional: + description: Specify whether the ConfigMap or its keys must + be defined + type: boolean + type: object + csi: + description: CSI (Container Storage Interface) represents storage + that is handled by an external CSI driver (Alpha feature). + properties: + driver: + description: Driver is the name of the CSI driver that handles + this volume. Consult with your admin for the correct name + as registered in the cluster. + type: string + fsType: + description: Filesystem type to mount. Ex. "ext4", "xfs", + "ntfs". If not provided, the empty value is passed to the + associated CSI driver which will determine the default filesystem + to apply. + type: string + nodePublishSecretRef: + description: NodePublishSecretRef is a reference to the secret + object containing sensitive information to pass to the CSI + driver to complete the CSI NodePublishVolume and NodeUnpublishVolume + calls. This field is optional, and may be empty if no secret + is required. If the secret object contains more than one + secret, all secret references are passed. + properties: + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + type: object + readOnly: + description: Specifies a read-only configuration for the volume. + Defaults to false (read/write). + type: boolean + volumeAttributes: + additionalProperties: + type: string + description: VolumeAttributes stores driver-specific properties + that are passed to the CSI driver. Consult your driver's + documentation for supported values. + type: object + required: + - driver + type: object + downwardAPI: + description: DownwardAPI represents downward API about the pod + that should populate this volume + properties: + defaultMode: + description: 'Optional: mode bits to use on created files + by default. Must be a value between 0 and 0777. Defaults + to 0644. Directories within the path are not affected by + this setting. This might be in conflict with other options + that affect the file mode, like fsGroup, and the result + can be other mode bits set.' + format: int32 + type: integer + items: + description: Items is a list of downward API volume file + items: + description: DownwardAPIVolumeFile represents information + to create the file containing the pod field + properties: + fieldRef: + description: 'Required: Selects a field of the pod: + only annotations, labels, name and namespace are supported.' + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in the + specified API version. + type: string + required: + - fieldPath + type: object + mode: + description: 'Optional: mode bits to use on this file, + must be a value between 0 and 0777. If not specified, + the volume defaultMode will be used. This might be + in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode + bits set.' + format: int32 + type: integer + path: + description: 'Required: Path is the relative path name + of the file to be created. Must not be absolute or + contain the ''..'' path. Must be utf-8 encoded. The + first item of the relative path must not start with + ''..''' + type: string + resourceFieldRef: + description: 'Selects a resource of the container: only + resources limits and requests (limits.cpu, limits.memory, + requests.cpu and requests.memory) are currently supported.' + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + description: Specifies the output format of the + exposed resources, defaults to "1" + type: string + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + required: + - path + type: object + type: array + type: object + emptyDir: + description: 'EmptyDir represents a temporary directory that shares + a pod''s lifetime. More info: https://kubernetes.io/docs/concepts/storage/volumes#emptydir' + properties: + medium: + description: 'What type of storage medium should back this + directory. The default is "" which means to use the node''s + default medium. Must be an empty string (default) or Memory. + More info: https://kubernetes.io/docs/concepts/storage/volumes#emptydir' + type: string + sizeLimit: + description: 'Total amount of local storage required for this + EmptyDir volume. The size limit is also applicable for memory + medium. The maximum usage on memory medium EmptyDir would + be the minimum value between the SizeLimit specified here + and the sum of memory limits of all containers in a pod. + The default is nil which means that the limit is undefined. + More info: http://kubernetes.io/docs/user-guide/volumes#emptydir' + type: string + type: object + fc: + description: FC represents a Fibre Channel resource that is attached + to a kubelet's host machine and then exposed to the pod. + properties: + fsType: + description: 'Filesystem type to mount. Must be a filesystem + type supported by the host operating system. Ex. "ext4", + "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + TODO: how do we prevent errors in the filesystem from compromising + the machine' + type: string + lun: + description: 'Optional: FC target lun number' + format: int32 + type: integer + readOnly: + description: 'Optional: Defaults to false (read/write). ReadOnly + here will force the ReadOnly setting in VolumeMounts.' + type: boolean + targetWWNs: + description: 'Optional: FC target worldwide names (WWNs)' + items: + type: string + type: array + wwids: + description: 'Optional: FC volume world wide identifiers (wwids) + Either wwids or combination of targetWWNs and lun must be + set, but not both simultaneously.' + items: + type: string + type: array + type: object + flexVolume: + description: FlexVolume represents a generic volume resource that + is provisioned/attached using an exec based plugin. + properties: + driver: + description: Driver is the name of the driver to use for this + volume. + type: string + fsType: + description: Filesystem type to mount. Must be a filesystem + type supported by the host operating system. Ex. "ext4", + "xfs", "ntfs". The default filesystem depends on FlexVolume + script. + type: string + options: + additionalProperties: + type: string + description: 'Optional: Extra command options if any.' + type: object + readOnly: + description: 'Optional: Defaults to false (read/write). ReadOnly + here will force the ReadOnly setting in VolumeMounts.' + type: boolean + secretRef: + description: 'Optional: SecretRef is reference to the secret + object containing sensitive information to pass to the plugin + scripts. This may be empty if no secret object is specified. + If the secret object contains more than one secret, all + secrets are passed to the plugin scripts.' + properties: + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + type: object + required: + - driver + type: object + flocker: + description: Flocker represents a Flocker volume attached to a + kubelet's host machine. This depends on the Flocker control + service being running + properties: + datasetName: + description: Name of the dataset stored as metadata -> name + on the dataset for Flocker should be considered as deprecated + type: string + datasetUUID: + description: UUID of the dataset. This is unique identifier + of a Flocker dataset + type: string + type: object + gcePersistentDisk: + description: 'GCEPersistentDisk represents a GCE Disk resource + that is attached to a kubelet''s host machine and then exposed + to the pod. More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk' + properties: + fsType: + description: 'Filesystem type of the volume that you want + to mount. Tip: Ensure that the filesystem type is supported + by the host operating system. Examples: "ext4", "xfs", "ntfs". + Implicitly inferred to be "ext4" if unspecified. More info: + https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk + TODO: how do we prevent errors in the filesystem from compromising + the machine' + type: string + partition: + description: 'The partition in the volume that you want to + mount. If omitted, the default is to mount by volume name. + Examples: For volume /dev/sda1, you specify the partition + as "1". Similarly, the volume partition for /dev/sda is + "0" (or you can leave the property empty). More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk' + format: int32 + type: integer + pdName: + description: 'Unique name of the PD resource in GCE. Used + to identify the disk in GCE. More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk' + type: string + readOnly: + description: 'ReadOnly here will force the ReadOnly setting + in VolumeMounts. Defaults to false. More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk' + type: boolean + required: + - pdName + type: object + gitRepo: + description: 'GitRepo represents a git repository at a particular + revision. DEPRECATED: GitRepo is deprecated. To provision a + container with a git repo, mount an EmptyDir into an InitContainer + that clones the repo using git, then mount the EmptyDir into + the Pod''s container.' + properties: + directory: + description: Target directory name. Must not contain or start + with '..'. If '.' is supplied, the volume directory will + be the git repository. Otherwise, if specified, the volume + will contain the git repository in the subdirectory with + the given name. + type: string + repository: + description: Repository URL + type: string + revision: + description: Commit hash for the specified revision. + type: string + required: + - repository + type: object + glusterfs: + description: 'Glusterfs represents a Glusterfs mount on the host + that shares a pod''s lifetime. More info: https://examples.k8s.io/volumes/glusterfs/README.md' + properties: + endpoints: + description: 'EndpointsName is the endpoint name that details + Glusterfs topology. More info: https://examples.k8s.io/volumes/glusterfs/README.md#create-a-pod' + type: string + path: + description: 'Path is the Glusterfs volume path. More info: + https://examples.k8s.io/volumes/glusterfs/README.md#create-a-pod' + type: string + readOnly: + description: 'ReadOnly here will force the Glusterfs volume + to be mounted with read-only permissions. Defaults to false. + More info: https://examples.k8s.io/volumes/glusterfs/README.md#create-a-pod' + type: boolean + required: + - endpoints + - path + type: object + hostPath: + description: 'HostPath represents a pre-existing file or directory + on the host machine that is directly exposed to the container. + This is generally used for system agents or other privileged + things that are allowed to see the host machine. Most containers + will NOT need this. More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath + --- TODO(jonesdl) We need to restrict who can use host directory + mounts and who can/can not mount host directories as read/write.' + properties: + path: + description: 'Path of the directory on the host. If the path + is a symlink, it will follow the link to the real path. + More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath' + type: string + type: + description: 'Type for HostPath Volume Defaults to "" More + info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath' + type: string + required: + - path + type: object + iscsi: + description: 'ISCSI represents an ISCSI Disk resource that is + attached to a kubelet''s host machine and then exposed to the + pod. More info: https://examples.k8s.io/volumes/iscsi/README.md' + properties: + chapAuthDiscovery: + description: whether support iSCSI Discovery CHAP authentication + type: boolean + chapAuthSession: + description: whether support iSCSI Session CHAP authentication + type: boolean + fsType: + description: 'Filesystem type of the volume that you want + to mount. Tip: Ensure that the filesystem type is supported + by the host operating system. Examples: "ext4", "xfs", "ntfs". + Implicitly inferred to be "ext4" if unspecified. More info: + https://kubernetes.io/docs/concepts/storage/volumes#iscsi + TODO: how do we prevent errors in the filesystem from compromising + the machine' + type: string + initiatorName: + description: Custom iSCSI Initiator Name. If initiatorName + is specified with iscsiInterface simultaneously, new iSCSI + interface : will be created + for the connection. + type: string + iqn: + description: Target iSCSI Qualified Name. + type: string + iscsiInterface: + description: iSCSI Interface Name that uses an iSCSI transport. + Defaults to 'default' (tcp). + type: string + lun: + description: iSCSI Target Lun number. + format: int32 + type: integer + portals: + description: iSCSI Target Portal List. The portal is either + an IP or ip_addr:port if the port is other than default + (typically TCP ports 860 and 3260). + items: + type: string + type: array + readOnly: + description: ReadOnly here will force the ReadOnly setting + in VolumeMounts. Defaults to false. + type: boolean + secretRef: + description: CHAP Secret for iSCSI target and initiator authentication + properties: + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + type: object + targetPortal: + description: iSCSI Target Portal. The Portal is either an + IP or ip_addr:port if the port is other than default (typically + TCP ports 860 and 3260). + type: string + required: + - iqn + - lun + - targetPortal + type: object + name: + description: 'Volume''s name. Must be a DNS_LABEL and unique within + the pod. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + nfs: + description: 'NFS represents an NFS mount on the host that shares + a pod''s lifetime More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs' + properties: + path: + description: 'Path that is exported by the NFS server. More + info: https://kubernetes.io/docs/concepts/storage/volumes#nfs' + type: string + readOnly: + description: 'ReadOnly here will force the NFS export to be + mounted with read-only permissions. Defaults to false. More + info: https://kubernetes.io/docs/concepts/storage/volumes#nfs' + type: boolean + server: + description: 'Server is the hostname or IP address of the + NFS server. More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs' + type: string + required: + - path + - server + type: object + persistentVolumeClaim: + description: 'PersistentVolumeClaimVolumeSource represents a reference + to a PersistentVolumeClaim in the same namespace. More info: + https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims' + properties: + claimName: + description: 'ClaimName is the name of a PersistentVolumeClaim + in the same namespace as the pod using this volume. More + info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims' + type: string + readOnly: + description: Will force the ReadOnly setting in VolumeMounts. + Default false. + type: boolean + required: + - claimName + type: object + photonPersistentDisk: + description: PhotonPersistentDisk represents a PhotonController + persistent disk attached and mounted on kubelets host machine + properties: + fsType: + description: Filesystem type to mount. Must be a filesystem + type supported by the host operating system. Ex. "ext4", + "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + type: string + pdID: + description: ID that identifies Photon Controller persistent + disk + type: string + required: + - pdID + type: object + portworxVolume: + description: PortworxVolume represents a portworx volume attached + and mounted on kubelets host machine + properties: + fsType: + description: FSType represents the filesystem type to mount + Must be a filesystem type supported by the host operating + system. Ex. "ext4", "xfs". Implicitly inferred to be "ext4" + if unspecified. + type: string + readOnly: + description: Defaults to false (read/write). ReadOnly here + will force the ReadOnly setting in VolumeMounts. + type: boolean + volumeID: + description: VolumeID uniquely identifies a Portworx volume + type: string + required: + - volumeID + type: object + projected: + description: Items for all in one resources secrets, configmaps, + and downward API + properties: + defaultMode: + description: Mode bits to use on created files by default. + Must be a value between 0 and 0777. Directories within the + path are not affected by this setting. This might be in + conflict with other options that affect the file mode, like + fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + sources: + description: list of volume projections + items: + description: Projection that may be projected along with + other supported volume types + properties: + configMap: + description: information about the configMap data to + project + properties: + items: + description: If unspecified, each key-value pair + in the Data field of the referenced ConfigMap + will be projected into the volume as a file whose + name is the key and content is the value. If specified, + the listed keys will be projected into the specified + paths, and unlisted keys will not be present. + If a key is specified which is not present in + the ConfigMap, the volume setup will error unless + it is marked optional. Paths must be relative + and may not contain the '..' path or start with + '..'. + items: + description: Maps a string key to a path within + a volume. + properties: + key: + description: The key to project. + type: string + mode: + description: 'Optional: mode bits to use on + this file, must be a value between 0 and + 0777. If not specified, the volume defaultMode + will be used. This might be in conflict + with other options that affect the file + mode, like fsGroup, and the result can be + other mode bits set.' + format: int32 + type: integer + path: + description: The relative path of the file + to map the key to. May not be an absolute + path. May not contain the path element '..'. + May not start with the string '..'. + type: string + required: + - key + - path + type: object + type: array + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, + uid?' + type: string + optional: + description: Specify whether the ConfigMap or its + keys must be defined + type: boolean + type: object + downwardAPI: + description: information about the downwardAPI data + to project + properties: + items: + description: Items is a list of DownwardAPIVolume + file + items: + description: DownwardAPIVolumeFile represents + information to create the file containing the + pod field + properties: + fieldRef: + description: 'Required: Selects a field of + the pod: only annotations, labels, name + and namespace are supported.' + properties: + apiVersion: + description: Version of the schema the + FieldPath is written in terms of, defaults + to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + mode: + description: 'Optional: mode bits to use on + this file, must be a value between 0 and + 0777. If not specified, the volume defaultMode + will be used. This might be in conflict + with other options that affect the file + mode, like fsGroup, and the result can be + other mode bits set.' + format: int32 + type: integer + path: + description: 'Required: Path is the relative + path name of the file to be created. Must + not be absolute or contain the ''..'' path. + Must be utf-8 encoded. The first item of + the relative path must not start with ''..''' + type: string + resourceFieldRef: + description: 'Selects a resource of the container: + only resources limits and requests (limits.cpu, + limits.memory, requests.cpu and requests.memory) + are currently supported.' + properties: + containerName: + description: 'Container name: required + for volumes, optional for env vars' + type: string + divisor: + description: Specifies the output format + of the exposed resources, defaults to + "1" + type: string + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + required: + - path + type: object + type: array + type: object + secret: + description: information about the secret data to project + properties: + items: + description: If unspecified, each key-value pair + in the Data field of the referenced Secret will + be projected into the volume as a file whose name + is the key and content is the value. If specified, + the listed keys will be projected into the specified + paths, and unlisted keys will not be present. + If a key is specified which is not present in + the Secret, the volume setup will error unless + it is marked optional. Paths must be relative + and may not contain the '..' path or start with + '..'. + items: + description: Maps a string key to a path within + a volume. + properties: + key: + description: The key to project. + type: string + mode: + description: 'Optional: mode bits to use on + this file, must be a value between 0 and + 0777. If not specified, the volume defaultMode + will be used. This might be in conflict + with other options that affect the file + mode, like fsGroup, and the result can be + other mode bits set.' + format: int32 + type: integer + path: + description: The relative path of the file + to map the key to. May not be an absolute + path. May not contain the path element '..'. + May not start with the string '..'. + type: string + required: + - key + - path + type: object + type: array + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, + uid?' + type: string + optional: + description: Specify whether the Secret or its key + must be defined + type: boolean + type: object + serviceAccountToken: + description: information about the serviceAccountToken + data to project + properties: + audience: + description: Audience is the intended audience of + the token. A recipient of a token must identify + itself with an identifier specified in the audience + of the token, and otherwise should reject the + token. The audience defaults to the identifier + of the apiserver. + type: string + expirationSeconds: + description: ExpirationSeconds is the requested + duration of validity of the service account token. + As the token approaches expiration, the kubelet + volume plugin will proactively rotate the service + account token. The kubelet will start trying to + rotate the token if the token is older than 80 + percent of its time to live or if the token is + older than 24 hours.Defaults to 1 hour and must + be at least 10 minutes. + format: int64 + type: integer + path: + description: Path is the path relative to the mount + point of the file to project the token into. + type: string + required: + - path + type: object + type: object + type: array + required: + - sources + type: object + quobyte: + description: Quobyte represents a Quobyte mount on the host that + shares a pod's lifetime + properties: + group: + description: Group to map volume access to Default is no group + type: string + readOnly: + description: ReadOnly here will force the Quobyte volume to + be mounted with read-only permissions. Defaults to false. + type: boolean + registry: + description: Registry represents a single or multiple Quobyte + Registry services specified as a string as host:port pair + (multiple entries are separated with commas) which acts + as the central registry for volumes + type: string + tenant: + description: Tenant owning the given Quobyte volume in the + Backend Used with dynamically provisioned Quobyte volumes, + value is set by the plugin + type: string + user: + description: User to map volume access to Defaults to serivceaccount + user + type: string + volume: + description: Volume is a string that references an already + created Quobyte volume by name. + type: string + required: + - registry + - volume + type: object + rbd: + description: 'RBD represents a Rados Block Device mount on the + host that shares a pod''s lifetime. More info: https://examples.k8s.io/volumes/rbd/README.md' + properties: + fsType: + description: 'Filesystem type of the volume that you want + to mount. Tip: Ensure that the filesystem type is supported + by the host operating system. Examples: "ext4", "xfs", "ntfs". + Implicitly inferred to be "ext4" if unspecified. More info: + https://kubernetes.io/docs/concepts/storage/volumes#rbd + TODO: how do we prevent errors in the filesystem from compromising + the machine' + type: string + image: + description: 'The rados image name. More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it' + type: string + keyring: + description: 'Keyring is the path to key ring for RBDUser. + Default is /etc/ceph/keyring. More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it' + type: string + monitors: + description: 'A collection of Ceph monitors. More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it' + items: + type: string + type: array + pool: + description: 'The rados pool name. Default is rbd. More info: + https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it' + type: string + readOnly: + description: 'ReadOnly here will force the ReadOnly setting + in VolumeMounts. Defaults to false. More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it' + type: boolean + secretRef: + description: 'SecretRef is name of the authentication secret + for RBDUser. If provided overrides keyring. Default is nil. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it' + properties: + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + type: object + user: + description: 'The rados user name. Default is admin. More + info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it' + type: string + required: + - image + - monitors + type: object + scaleIO: + description: ScaleIO represents a ScaleIO persistent volume attached + and mounted on Kubernetes nodes. + properties: + fsType: + description: Filesystem type to mount. Must be a filesystem + type supported by the host operating system. Ex. "ext4", + "xfs", "ntfs". Default is "xfs". + type: string + gateway: + description: The host address of the ScaleIO API Gateway. + type: string + protectionDomain: + description: The name of the ScaleIO Protection Domain for + the configured storage. + type: string + readOnly: + description: Defaults to false (read/write). ReadOnly here + will force the ReadOnly setting in VolumeMounts. + type: boolean + secretRef: + description: SecretRef references to the secret for ScaleIO + user and other sensitive information. If this is not provided, + Login operation will fail. + properties: + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + type: object + sslEnabled: + description: Flag to enable/disable SSL communication with + Gateway, default false + type: boolean + storageMode: + description: Indicates whether the storage for a volume should + be ThickProvisioned or ThinProvisioned. Default is ThinProvisioned. + type: string + storagePool: + description: The ScaleIO Storage Pool associated with the + protection domain. + type: string + system: + description: The name of the storage system as configured + in ScaleIO. + type: string + volumeName: + description: The name of a volume already created in the ScaleIO + system that is associated with this volume source. + type: string + required: + - gateway + - secretRef + - system + type: object + secret: + description: 'Secret represents a secret that should populate + this volume. More info: https://kubernetes.io/docs/concepts/storage/volumes#secret' + properties: + defaultMode: + description: 'Optional: mode bits to use on created files + by default. Must be a value between 0 and 0777. Defaults + to 0644. Directories within the path are not affected by + this setting. This might be in conflict with other options + that affect the file mode, like fsGroup, and the result + can be other mode bits set.' + format: int32 + type: integer + items: + description: If unspecified, each key-value pair in the Data + field of the referenced Secret will be projected into the + volume as a file whose name is the key and content is the + value. If specified, the listed keys will be projected into + the specified paths, and unlisted keys will not be present. + If a key is specified which is not present in the Secret, + the volume setup will error unless it is marked optional. + Paths must be relative and may not contain the '..' path + or start with '..'. + items: + description: Maps a string key to a path within a volume. + properties: + key: + description: The key to project. + type: string + mode: + description: 'Optional: mode bits to use on this file, + must be a value between 0 and 0777. If not specified, + the volume defaultMode will be used. This might be + in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode + bits set.' + format: int32 + type: integer + path: + description: The relative path of the file to map the + key to. May not be an absolute path. May not contain + the path element '..'. May not start with the string + '..'. + type: string + required: + - key + - path + type: object + type: array + optional: + description: Specify whether the Secret or its keys must be + defined + type: boolean + secretName: + description: 'Name of the secret in the pod''s namespace to + use. More info: https://kubernetes.io/docs/concepts/storage/volumes#secret' + type: string + type: object + storageos: + description: StorageOS represents a StorageOS volume attached + and mounted on Kubernetes nodes. + properties: + fsType: + description: Filesystem type to mount. Must be a filesystem + type supported by the host operating system. Ex. "ext4", + "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + type: string + readOnly: + description: Defaults to false (read/write). ReadOnly here + will force the ReadOnly setting in VolumeMounts. + type: boolean + secretRef: + description: SecretRef specifies the secret to use for obtaining + the StorageOS API credentials. If not specified, default + values will be attempted. + properties: + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + type: object + volumeName: + description: VolumeName is the human-readable name of the + StorageOS volume. Volume names are only unique within a + namespace. + type: string + volumeNamespace: + description: VolumeNamespace specifies the scope of the volume + within StorageOS. If no namespace is specified then the + Pod's namespace will be used. This allows the Kubernetes + name scoping to be mirrored within StorageOS for tighter + integration. Set VolumeName to any name to override the + default behaviour. Set to "default" if you are not using + namespaces within StorageOS. Namespaces that do not pre-exist + within StorageOS will be created. + type: string + type: object + vsphereVolume: + description: VsphereVolume represents a vSphere volume attached + and mounted on kubelets host machine + properties: + fsType: + description: Filesystem type to mount. Must be a filesystem + type supported by the host operating system. Ex. "ext4", + "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + type: string + storagePolicyID: + description: Storage Policy Based Management (SPBM) profile + ID associated with the StoragePolicyName. + type: string + storagePolicyName: + description: Storage Policy Based Management (SPBM) profile + name. + type: string + volumePath: + description: Path that identifies vSphere volume vmdk + type: string + required: + - volumePath + type: object + required: + - name + type: object + type: array + type: object + status: + description: IndexerClusterStatus defines the observed state of a Splunk + Enterprise indexer cluster + properties: + IdxcPasswordChangedSecrets: + additionalProperties: + type: boolean + description: Holds secrets whose IDXC password has changed + type: object + clusterMasterPhase: + description: current phase of the cluster master + enum: + - Pending + - Ready + - Updating + - ScalingUp + - ScalingDown + - Terminating + - Error + type: string + indexer_secret_changed_flag: + description: Indicates when the idxc_secret has been changed for a peer + items: + type: boolean + type: array + indexing_ready_flag: + description: Indicates if the cluster is ready for indexing. + type: boolean + initialized_flag: + description: Indicates if the cluster is initialized. + type: boolean + maintenance_mode: + description: Indicates if the cluster is in maintenance mode. + type: boolean + namespace_scoped_secret_resource_version: + description: Indicates resource version of namespace scoped secret + type: string + peers: + description: status of each indexer cluster peer + items: + description: IndexerClusterMemberStatus is used to track the status + of each indexer cluster peer. + properties: + active_bundle_id: + description: The ID of the configuration bundle currently being + used by the master. + type: string + bucket_count: + description: Count of the number of buckets on this peer, across + all indexes. + format: int64 + type: integer + guid: + description: Unique identifier or GUID for the peer + type: string + is_searchable: + description: Flag indicating if this peer belongs to the current + committed generation and is searchable. + type: boolean + name: + description: Name of the indexer cluster peer + type: string + status: + description: Status of the indexer cluster peer + type: string + type: object + type: array + phase: + description: current phase of the indexer cluster + enum: + - Pending + - Ready + - Updating + - ScalingUp + - ScalingDown + - Terminating + - Error + type: string + readyReplicas: + description: current number of ready indexer peers + format: int32 + type: integer + replicas: + description: desired number of indexer peers + format: int32 + type: integer + selector: + description: selector for pods, used by HorizontalPodAutoscaler + type: string + service_ready_flag: + description: Indicates whether the master is ready to begin servicing, + based on whether it is initialized. + type: boolean + type: object + type: object + version: v1beta1 + versions: + - name: v1beta1 + served: true + storage: true + - name: v1alpha3 + served: true + storage: false + - name: v1alpha2 + served: true + storage: false diff --git a/deploy/olm-catalog/splunk/0.2.2/enterprise.splunk.com_licensemasters_crd.yaml b/deploy/olm-catalog/splunk/0.2.2/enterprise.splunk.com_licensemasters_crd.yaml new file mode 100644 index 000000000..b2fe9ab56 --- /dev/null +++ b/deploy/olm-catalog/splunk/0.2.2/enterprise.splunk.com_licensemasters_crd.yaml @@ -0,0 +1,2264 @@ +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: licensemasters.enterprise.splunk.com +spec: + additionalPrinterColumns: + - JSONPath: .status.phase + description: Status of license master + name: Phase + type: string + - JSONPath: .metadata.creationTimestamp + description: Age of license master + name: Age + type: date + group: enterprise.splunk.com + names: + kind: LicenseMaster + listKind: LicenseMasterList + plural: licensemasters + shortNames: + - lm + singular: licensemaster + scope: Namespaced + subresources: + status: {} + validation: + openAPIV3Schema: + description: LicenseMaster is the Schema for a Splunk Enterprise license master. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: LicenseMasterSpec defines the desired state of a Splunk Enterprise + license master. + properties: + Mock: + description: Mock to differentiate between UTs and actual reconcile + type: boolean + affinity: + description: Kubernetes Affinity rules that control how pods are assigned + to particular nodes. + properties: + nodeAffinity: + description: Describes node affinity scheduling rules for the pod. + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: The scheduler will prefer to schedule pods to nodes + that satisfy the affinity expressions specified by this field, + but it may choose a node that violates one or more of the + expressions. The node that is most preferred is the one with + the greatest sum of weights, i.e. for each node that meets + all of the scheduling requirements (resource request, requiredDuringScheduling + affinity expressions, etc.), compute a sum by iterating through + the elements of this field and adding "weight" to the sum + if the node matches the corresponding matchExpressions; the + node(s) with the highest sum are the most preferred. + items: + description: An empty preferred scheduling term matches all + objects with implicit weight 0 (i.e. it's a no-op). A null + preferred scheduling term matches no objects (i.e. is also + a no-op). + properties: + preference: + description: A node selector term, associated with the + corresponding weight. + properties: + matchExpressions: + description: A list of node selector requirements + by node's labels. + items: + description: A node selector requirement is a selector + that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: Represents a key's relationship + to a set of values. Valid operators are In, + NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: An array of string values. If the + operator is In or NotIn, the values array + must be non-empty. If the operator is Exists + or DoesNotExist, the values array must be + empty. If the operator is Gt or Lt, the values + array must have a single element, which will + be interpreted as an integer. This array is + replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchFields: + description: A list of node selector requirements + by node's fields. + items: + description: A node selector requirement is a selector + that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: Represents a key's relationship + to a set of values. Valid operators are In, + NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: An array of string values. If the + operator is In or NotIn, the values array + must be non-empty. If the operator is Exists + or DoesNotExist, the values array must be + empty. If the operator is Gt or Lt, the values + array must have a single element, which will + be interpreted as an integer. This array is + replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + type: object + weight: + description: Weight associated with matching the corresponding + nodeSelectorTerm, in the range 1-100. + format: int32 + type: integer + required: + - preference + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + description: If the affinity requirements specified by this + field are not met at scheduling time, the pod will not be + scheduled onto the node. If the affinity requirements specified + by this field cease to be met at some point during pod execution + (e.g. due to an update), the system may or may not try to + eventually evict the pod from its node. + properties: + nodeSelectorTerms: + description: Required. A list of node selector terms. The + terms are ORed. + items: + description: A null or empty node selector term matches + no objects. The requirements of them are ANDed. The + TopologySelectorTerm type implements a subset of the + NodeSelectorTerm. + properties: + matchExpressions: + description: A list of node selector requirements + by node's labels. + items: + description: A node selector requirement is a selector + that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: Represents a key's relationship + to a set of values. Valid operators are In, + NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: An array of string values. If the + operator is In or NotIn, the values array + must be non-empty. If the operator is Exists + or DoesNotExist, the values array must be + empty. If the operator is Gt or Lt, the values + array must have a single element, which will + be interpreted as an integer. This array is + replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchFields: + description: A list of node selector requirements + by node's fields. + items: + description: A node selector requirement is a selector + that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: Represents a key's relationship + to a set of values. Valid operators are In, + NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: An array of string values. If the + operator is In or NotIn, the values array + must be non-empty. If the operator is Exists + or DoesNotExist, the values array must be + empty. If the operator is Gt or Lt, the values + array must have a single element, which will + be interpreted as an integer. This array is + replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + type: object + type: array + required: + - nodeSelectorTerms + type: object + type: object + podAffinity: + description: Describes pod affinity scheduling rules (e.g. co-locate + this pod in the same node, zone, etc. as some other pod(s)). + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: The scheduler will prefer to schedule pods to nodes + that satisfy the affinity expressions specified by this field, + but it may choose a node that violates one or more of the + expressions. The node that is most preferred is the one with + the greatest sum of weights, i.e. for each node that meets + all of the scheduling requirements (resource request, requiredDuringScheduling + affinity expressions, etc.), compute a sum by iterating through + the elements of this field and adding "weight" to the sum + if the node has pods which matches the corresponding podAffinityTerm; + the node(s) with the highest sum are the most preferred. + items: + description: The weights of all of the matched WeightedPodAffinityTerm + fields are added per-node to find the most preferred node(s) + properties: + podAffinityTerm: + description: Required. A pod affinity term, associated + with the corresponding weight. + properties: + labelSelector: + description: A label query over a set of resources, + in this case pods. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: A label selector requirement is + a selector that contains values, a key, and + an operator that relates the key and values. + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: operator represents a key's + relationship to a set of values. Valid + operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: values is an array of string + values. If the operator is In or NotIn, + the values array must be non-empty. If + the operator is Exists or DoesNotExist, + the values array must be empty. This array + is replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, + whose key field is "key", the operator is "In", + and the values array contains only "value". + The requirements are ANDed. + type: object + type: object + namespaces: + description: namespaces specifies which namespaces + the labelSelector applies to (matches against); + null or empty list means "this pod's namespace" + items: + type: string + type: array + topologyKey: + description: This pod should be co-located (affinity) + or not co-located (anti-affinity) with the pods + matching the labelSelector in the specified namespaces, + where co-located is defined as running on a node + whose value of the label with key topologyKey matches + that of any node on which any of the selected pods + is running. Empty topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + weight: + description: weight associated with matching the corresponding + podAffinityTerm, in the range 1-100. + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + description: If the affinity requirements specified by this + field are not met at scheduling time, the pod will not be + scheduled onto the node. If the affinity requirements specified + by this field cease to be met at some point during pod execution + (e.g. due to a pod label update), the system may or may not + try to eventually evict the pod from its node. When there + are multiple elements, the lists of nodes corresponding to + each podAffinityTerm are intersected, i.e. all terms must + be satisfied. + items: + description: Defines a set of pods (namely those matching + the labelSelector relative to the given namespace(s)) that + this pod should be co-located (affinity) or not co-located + (anti-affinity) with, where co-located is defined as running + on a node whose value of the label with key + matches that of any node on which a pod of the set of pods + is running + properties: + labelSelector: + description: A label query over a set of resources, in + this case pods. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: A label selector requirement is a selector + that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: operator represents a key's relationship + to a set of values. Valid operators are In, + NotIn, Exists and DoesNotExist. + type: string + values: + description: values is an array of string values. + If the operator is In or NotIn, the values + array must be non-empty. If the operator is + Exists or DoesNotExist, the values array must + be empty. This array is replaced during a + strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} pairs. + A single {key,value} in the matchLabels map is equivalent + to an element of matchExpressions, whose key field + is "key", the operator is "In", and the values array + contains only "value". The requirements are ANDed. + type: object + type: object + namespaces: + description: namespaces specifies which namespaces the + labelSelector applies to (matches against); null or + empty list means "this pod's namespace" + items: + type: string + type: array + topologyKey: + description: This pod should be co-located (affinity) + or not co-located (anti-affinity) with the pods matching + the labelSelector in the specified namespaces, where + co-located is defined as running on a node whose value + of the label with key topologyKey matches that of any + node on which any of the selected pods is running. Empty + topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + type: array + type: object + podAntiAffinity: + description: Describes pod anti-affinity scheduling rules (e.g. + avoid putting this pod in the same node, zone, etc. as some other + pod(s)). + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: The scheduler will prefer to schedule pods to nodes + that satisfy the anti-affinity expressions specified by this + field, but it may choose a node that violates one or more + of the expressions. The node that is most preferred is the + one with the greatest sum of weights, i.e. for each node that + meets all of the scheduling requirements (resource request, + requiredDuringScheduling anti-affinity expressions, etc.), + compute a sum by iterating through the elements of this field + and adding "weight" to the sum if the node has pods which + matches the corresponding podAffinityTerm; the node(s) with + the highest sum are the most preferred. + items: + description: The weights of all of the matched WeightedPodAffinityTerm + fields are added per-node to find the most preferred node(s) + properties: + podAffinityTerm: + description: Required. A pod affinity term, associated + with the corresponding weight. + properties: + labelSelector: + description: A label query over a set of resources, + in this case pods. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: A label selector requirement is + a selector that contains values, a key, and + an operator that relates the key and values. + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: operator represents a key's + relationship to a set of values. Valid + operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: values is an array of string + values. If the operator is In or NotIn, + the values array must be non-empty. If + the operator is Exists or DoesNotExist, + the values array must be empty. This array + is replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, + whose key field is "key", the operator is "In", + and the values array contains only "value". + The requirements are ANDed. + type: object + type: object + namespaces: + description: namespaces specifies which namespaces + the labelSelector applies to (matches against); + null or empty list means "this pod's namespace" + items: + type: string + type: array + topologyKey: + description: This pod should be co-located (affinity) + or not co-located (anti-affinity) with the pods + matching the labelSelector in the specified namespaces, + where co-located is defined as running on a node + whose value of the label with key topologyKey matches + that of any node on which any of the selected pods + is running. Empty topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + weight: + description: weight associated with matching the corresponding + podAffinityTerm, in the range 1-100. + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + description: If the anti-affinity requirements specified by + this field are not met at scheduling time, the pod will not + be scheduled onto the node. If the anti-affinity requirements + specified by this field cease to be met at some point during + pod execution (e.g. due to a pod label update), the system + may or may not try to eventually evict the pod from its node. + When there are multiple elements, the lists of nodes corresponding + to each podAffinityTerm are intersected, i.e. all terms must + be satisfied. + items: + description: Defines a set of pods (namely those matching + the labelSelector relative to the given namespace(s)) that + this pod should be co-located (affinity) or not co-located + (anti-affinity) with, where co-located is defined as running + on a node whose value of the label with key + matches that of any node on which a pod of the set of pods + is running + properties: + labelSelector: + description: A label query over a set of resources, in + this case pods. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: A label selector requirement is a selector + that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: operator represents a key's relationship + to a set of values. Valid operators are In, + NotIn, Exists and DoesNotExist. + type: string + values: + description: values is an array of string values. + If the operator is In or NotIn, the values + array must be non-empty. If the operator is + Exists or DoesNotExist, the values array must + be empty. This array is replaced during a + strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} pairs. + A single {key,value} in the matchLabels map is equivalent + to an element of matchExpressions, whose key field + is "key", the operator is "In", and the values array + contains only "value". The requirements are ANDed. + type: object + type: object + namespaces: + description: namespaces specifies which namespaces the + labelSelector applies to (matches against); null or + empty list means "this pod's namespace" + items: + type: string + type: array + topologyKey: + description: This pod should be co-located (affinity) + or not co-located (anti-affinity) with the pods matching + the labelSelector in the specified namespaces, where + co-located is defined as running on a node whose value + of the label with key topologyKey matches that of any + node on which any of the selected pods is running. Empty + topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + type: array + type: object + type: object + clusterMasterRef: + description: ClusterMasterRef refers to a Splunk Enterprise indexer + cluster managed by the operator within Kubernetes + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: 'If referring to a piece of an object instead of an + entire object, this string should contain a valid JSON/Go field + access statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to a container within + a pod, this would take on a value like: "spec.containers{name}" + (where "name" refers to the name of the container that triggered + the event) or if no container name is specified "spec.containers[2]" + (container with index 2 in this pod). This syntax is chosen only + to have some well-defined way of referencing a part of an object. + TODO: this design is not final and this field is subject to change + in the future.' + type: string + kind: + description: 'Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + namespace: + description: 'Namespace of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/' + type: string + resourceVersion: + description: 'Specific resourceVersion to which this reference is + made, if any. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency' + type: string + uid: + description: 'UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids' + type: string + type: object + defaults: + description: Inline map of default.yml overrides used to initialize + the environment + type: string + defaultsUrl: + description: Full path or URL for one or more default.yml files, separated + by commas + type: string + defaultsUrlApps: + description: Full path or URL for one or more defaults.yml files specific + to App install, separated by commas. The defaults listed here will + be installed on the CM, standalone, search head deployer or license + master instance. + type: string + etcVolumeStorageConfig: + description: Storage configuration for /opt/splunk/etc volume + properties: + ephemeralStorage: + description: If true, ephemeral (emptyDir) storage will be used + type: boolean + storageCapacity: + description: Storage capacity to request persistent volume claims + (default=”10Gi” for etc and "100Gi" for var) + type: string + storageClassName: + description: Name of StorageClass to use for persistent volume claims + type: string + type: object + image: + description: Image to use for Splunk pod containers (overrides RELATED_IMAGE_SPLUNK_ENTERPRISE + environment variables) + type: string + imagePullPolicy: + description: 'Sets pull policy for all images (either “Always” or the + default: “IfNotPresent”)' + enum: + - Always + - IfNotPresent + type: string + licenseMasterRef: + description: LicenseMasterRef refers to a Splunk Enterprise license + master managed by the operator within Kubernetes + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: 'If referring to a piece of an object instead of an + entire object, this string should contain a valid JSON/Go field + access statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to a container within + a pod, this would take on a value like: "spec.containers{name}" + (where "name" refers to the name of the container that triggered + the event) or if no container name is specified "spec.containers[2]" + (container with index 2 in this pod). This syntax is chosen only + to have some well-defined way of referencing a part of an object. + TODO: this design is not final and this field is subject to change + in the future.' + type: string + kind: + description: 'Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + namespace: + description: 'Namespace of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/' + type: string + resourceVersion: + description: 'Specific resourceVersion to which this reference is + made, if any. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency' + type: string + uid: + description: 'UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids' + type: string + type: object + licenseUrl: + description: Full path or URL for a Splunk Enterprise license file + type: string + resources: + description: resource requirements for the pod containers + properties: + limits: + additionalProperties: + type: string + description: 'Limits describes the maximum amount of compute resources + allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/' + type: object + requests: + additionalProperties: + type: string + description: 'Requests describes the minimum amount of compute resources + required. If Requests is omitted for a container, it defaults + to Limits if that is explicitly specified, otherwise to an implementation-defined + value. More info: https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/' + type: object + type: object + schedulerName: + description: Name of Scheduler to use for pod placement (defaults to + “default-scheduler”) + type: string + serviceAccount: + description: ServiceAccount is the service account used by the pods + deployed by the CRD. If not specified uses the default serviceAccount + for the namespace as per https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/#use-the-default-service-account-to-access-the-api-server + type: string + serviceTemplate: + description: ServiceTemplate is a template used to create Kubernetes + services + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the + latest internal value, and may reject unrecognized values. More + info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource + this object represents. Servers may infer this from the endpoint + the client submits requests to. Cannot be updated. In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + description: 'Standard object''s metadata. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata' + type: object + spec: + description: Spec defines the behavior of a service. https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status + properties: + clusterIP: + description: 'clusterIP is the IP address of the service and + is usually assigned randomly by the master. If an address + is specified manually and is not in use by others, it will + be allocated to the service; otherwise, creation of the service + will fail. This field can not be changed through updates. + Valid values are "None", empty string (""), or a valid IP + address. "None" can be specified for headless services when + proxying is not required. Only applies to types ClusterIP, + NodePort, and LoadBalancer. Ignored if type is ExternalName. + More info: https://kubernetes.io/docs/concepts/services-networking/service/#virtual-ips-and-service-proxies' + type: string + externalIPs: + description: externalIPs is a list of IP addresses for which + nodes in the cluster will also accept traffic for this service. These + IPs are not managed by Kubernetes. The user is responsible + for ensuring that traffic arrives at a node with this IP. A + common example is external load-balancers that are not part + of the Kubernetes system. + items: + type: string + type: array + externalName: + description: externalName is the external reference that kubedns + or equivalent will return as a CNAME record for this service. + No proxying will be involved. Must be a valid RFC-1123 hostname + (https://tools.ietf.org/html/rfc1123) and requires Type to + be ExternalName. + type: string + externalTrafficPolicy: + description: externalTrafficPolicy denotes if this Service desires + to route external traffic to node-local or cluster-wide endpoints. + "Local" preserves the client source IP and avoids a second + hop for LoadBalancer and Nodeport type services, but risks + potentially imbalanced traffic spreading. "Cluster" obscures + the client source IP and may cause a second hop to another + node, but should have good overall load-spreading. + type: string + healthCheckNodePort: + description: healthCheckNodePort specifies the healthcheck nodePort + for the service. If not specified, HealthCheckNodePort is + created by the service api backend with the allocated nodePort. + Will use user-specified nodePort value if specified by the + client. Only effects when Type is set to LoadBalancer and + ExternalTrafficPolicy is set to Local. + format: int32 + type: integer + ipFamily: + description: ipFamily specifies whether this Service has a preference + for a particular IP family (e.g. IPv4 vs. IPv6). If a specific + IP family is requested, the clusterIP field will be allocated + from that family, if it is available in the cluster. If no + IP family is requested, the cluster's primary IP family will + be used. Other IP fields (loadBalancerIP, loadBalancerSourceRanges, + externalIPs) and controllers which allocate external load-balancers + should use the same IP family. Endpoints for this Service + will be of this family. This field is immutable after creation. + Assigning a ServiceIPFamily not available in the cluster (e.g. + IPv6 in IPv4 only cluster) is an error condition and will + fail during clusterIP assignment. + type: string + loadBalancerIP: + description: 'Only applies to Service Type: LoadBalancer LoadBalancer + will get created with the IP specified in this field. This + feature depends on whether the underlying cloud-provider supports + specifying the loadBalancerIP when a load balancer is created. + This field will be ignored if the cloud-provider does not + support the feature.' + type: string + loadBalancerSourceRanges: + description: 'If specified and supported by the platform, this + will restrict traffic through the cloud-provider load-balancer + will be restricted to the specified client IPs. This field + will be ignored if the cloud-provider does not support the + feature." More info: https://kubernetes.io/docs/tasks/access-application-cluster/configure-cloud-provider-firewall/' + items: + type: string + type: array + ports: + description: 'The list of ports that are exposed by this service. + More info: https://kubernetes.io/docs/concepts/services-networking/service/#virtual-ips-and-service-proxies' + items: + description: ServicePort contains information on service's + port. + properties: + name: + description: The name of this port within the service. + This must be a DNS_LABEL. All ports within a ServiceSpec + must have unique names. When considering the endpoints + for a Service, this must match the 'name' field in the + EndpointPort. Optional if only one ServicePort is defined + on this service. + type: string + nodePort: + description: 'The port on each node on which this service + is exposed when type=NodePort or LoadBalancer. Usually + assigned by the system. If specified, it will be allocated + to the service if unused or else creation of the service + will fail. Default is to auto-allocate a port if the + ServiceType of this Service requires one. More info: + https://kubernetes.io/docs/concepts/services-networking/service/#type-nodeport' + format: int32 + type: integer + port: + description: The port that will be exposed by this service. + format: int32 + type: integer + protocol: + description: The IP protocol for this port. Supports "TCP", + "UDP", and "SCTP". Default is TCP. + type: string + targetPort: + anyOf: + - type: integer + - type: string + description: 'Number or name of the port to access on + the pods targeted by the service. Number must be in + the range 1 to 65535. Name must be an IANA_SVC_NAME. + If this is a string, it will be looked up as a named + port in the target Pod''s container ports. If this is + not specified, the value of the ''port'' field is used + (an identity map). This field is ignored for services + with clusterIP=None, and should be omitted or set equal + to the ''port'' field. More info: https://kubernetes.io/docs/concepts/services-networking/service/#defining-a-service' + x-kubernetes-int-or-string: true + required: + - port + type: object + type: array + publishNotReadyAddresses: + description: publishNotReadyAddresses, when set to true, indicates + that DNS implementations must publish the notReadyAddresses + of subsets for the Endpoints associated with the Service. + The default value is false. The primary use case for setting + this field is to use a StatefulSet's Headless Service to propagate + SRV records for its Pods without respect to their readiness + for purpose of peer discovery. + type: boolean + selector: + additionalProperties: + type: string + description: 'Route service traffic to pods with label keys + and values matching this selector. If empty or not present, + the service is assumed to have an external process managing + its endpoints, which Kubernetes will not modify. Only applies + to types ClusterIP, NodePort, and LoadBalancer. Ignored if + type is ExternalName. More info: https://kubernetes.io/docs/concepts/services-networking/service/' + type: object + sessionAffinity: + description: 'Supports "ClientIP" and "None". Used to maintain + session affinity. Enable client IP based session affinity. + Must be ClientIP or None. Defaults to None. More info: https://kubernetes.io/docs/concepts/services-networking/service/#virtual-ips-and-service-proxies' + type: string + sessionAffinityConfig: + description: sessionAffinityConfig contains the configurations + of session affinity. + properties: + clientIP: + description: clientIP contains the configurations of Client + IP based session affinity. + properties: + timeoutSeconds: + description: timeoutSeconds specifies the seconds of + ClientIP type session sticky time. The value must + be >0 && <=86400(for 1 day) if ServiceAffinity == + "ClientIP". Default value is 10800(for 3 hours). + format: int32 + type: integer + type: object + type: object + type: + description: 'type determines how the Service is exposed. Defaults + to ClusterIP. Valid options are ExternalName, ClusterIP, NodePort, + and LoadBalancer. "ExternalName" maps to the specified externalName. + "ClusterIP" allocates a cluster-internal IP address for load-balancing + to endpoints. Endpoints are determined by the selector or + if that is not specified, by manual construction of an Endpoints + object. If clusterIP is "None", no virtual IP is allocated + and the endpoints are published as a set of endpoints rather + than a stable IP. "NodePort" builds on ClusterIP and allocates + a port on every node which routes to the clusterIP. "LoadBalancer" + builds on NodePort and creates an external load-balancer (if + supported in the current cloud) which routes to the clusterIP. + More info: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types' + type: string + type: object + status: + description: 'Most recently observed status of the service. Populated + by the system. Read-only. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status' + properties: + loadBalancer: + description: LoadBalancer contains the current status of the + load-balancer, if one is present. + properties: + ingress: + description: Ingress is a list containing ingress points + for the load-balancer. Traffic intended for the service + should be sent to these ingress points. + items: + description: 'LoadBalancerIngress represents the status + of a load-balancer ingress point: traffic intended for + the service should be sent to an ingress point.' + properties: + hostname: + description: Hostname is set for load-balancer ingress + points that are DNS based (typically AWS load-balancers) + type: string + ip: + description: IP is set for load-balancer ingress points + that are IP based (typically GCE or OpenStack load-balancers) + type: string + type: object + type: array + type: object + type: object + type: object + tolerations: + description: Pod's tolerations for Kubernetes node's taint + items: + description: The pod this Toleration is attached to tolerates any + taint that matches the triple using the matching + operator . + properties: + effect: + description: Effect indicates the taint effect to match. Empty + means match all taint effects. When specified, allowed values + are NoSchedule, PreferNoSchedule and NoExecute. + type: string + key: + description: Key is the taint key that the toleration applies + to. Empty means match all taint keys. If the key is empty, operator + must be Exists; this combination means to match all values and + all keys. + type: string + operator: + description: Operator represents a key's relationship to the value. + Valid operators are Exists and Equal. Defaults to Equal. Exists + is equivalent to wildcard for value, so that a pod can tolerate + all taints of a particular category. + type: string + tolerationSeconds: + description: TolerationSeconds represents the period of time the + toleration (which must be of effect NoExecute, otherwise this + field is ignored) tolerates the taint. By default, it is not + set, which means tolerate the taint forever (do not evict). + Zero and negative values will be treated as 0 (evict immediately) + by the system. + format: int64 + type: integer + value: + description: Value is the taint value the toleration matches to. + If the operator is Exists, the value should be empty, otherwise + just a regular string. + type: string + type: object + type: array + varVolumeStorageConfig: + description: Storage configuration for /opt/splunk/var volume + properties: + ephemeralStorage: + description: If true, ephemeral (emptyDir) storage will be used + type: boolean + storageCapacity: + description: Storage capacity to request persistent volume claims + (default=”10Gi” for etc and "100Gi" for var) + type: string + storageClassName: + description: Name of StorageClass to use for persistent volume claims + type: string + type: object + volumes: + description: List of one or more Kubernetes volumes. These will be mounted + in all pod containers as as /mnt/ + items: + description: Volume represents a named volume in a pod that may be + accessed by any container in the pod. + properties: + awsElasticBlockStore: + description: 'AWSElasticBlockStore represents an AWS Disk resource + that is attached to a kubelet''s host machine and then exposed + to the pod. More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore' + properties: + fsType: + description: 'Filesystem type of the volume that you want + to mount. Tip: Ensure that the filesystem type is supported + by the host operating system. Examples: "ext4", "xfs", "ntfs". + Implicitly inferred to be "ext4" if unspecified. More info: + https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore + TODO: how do we prevent errors in the filesystem from compromising + the machine' + type: string + partition: + description: 'The partition in the volume that you want to + mount. If omitted, the default is to mount by volume name. + Examples: For volume /dev/sda1, you specify the partition + as "1". Similarly, the volume partition for /dev/sda is + "0" (or you can leave the property empty).' + format: int32 + type: integer + readOnly: + description: 'Specify "true" to force and set the ReadOnly + property in VolumeMounts to "true". If omitted, the default + is "false". More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore' + type: boolean + volumeID: + description: 'Unique ID of the persistent disk resource in + AWS (Amazon EBS volume). More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore' + type: string + required: + - volumeID + type: object + azureDisk: + description: AzureDisk represents an Azure Data Disk mount on + the host and bind mount to the pod. + properties: + cachingMode: + description: 'Host Caching mode: None, Read Only, Read Write.' + type: string + diskName: + description: The Name of the data disk in the blob storage + type: string + diskURI: + description: The URI the data disk in the blob storage + type: string + fsType: + description: Filesystem type to mount. Must be a filesystem + type supported by the host operating system. Ex. "ext4", + "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + type: string + kind: + description: 'Expected values Shared: multiple blob disks + per storage account Dedicated: single blob disk per storage + account Managed: azure managed data disk (only in managed + availability set). defaults to shared' + type: string + readOnly: + description: Defaults to false (read/write). ReadOnly here + will force the ReadOnly setting in VolumeMounts. + type: boolean + required: + - diskName + - diskURI + type: object + azureFile: + description: AzureFile represents an Azure File Service mount + on the host and bind mount to the pod. + properties: + readOnly: + description: Defaults to false (read/write). ReadOnly here + will force the ReadOnly setting in VolumeMounts. + type: boolean + secretName: + description: the name of secret that contains Azure Storage + Account Name and Key + type: string + shareName: + description: Share Name + type: string + required: + - secretName + - shareName + type: object + cephfs: + description: CephFS represents a Ceph FS mount on the host that + shares a pod's lifetime + properties: + monitors: + description: 'Required: Monitors is a collection of Ceph monitors + More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it' + items: + type: string + type: array + path: + description: 'Optional: Used as the mounted root, rather than + the full Ceph tree, default is /' + type: string + readOnly: + description: 'Optional: Defaults to false (read/write). ReadOnly + here will force the ReadOnly setting in VolumeMounts. More + info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it' + type: boolean + secretFile: + description: 'Optional: SecretFile is the path to key ring + for User, default is /etc/ceph/user.secret More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it' + type: string + secretRef: + description: 'Optional: SecretRef is reference to the authentication + secret for User, default is empty. More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it' + properties: + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + type: object + user: + description: 'Optional: User is the rados user name, default + is admin More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it' + type: string + required: + - monitors + type: object + cinder: + description: 'Cinder represents a cinder volume attached and mounted + on kubelets host machine. More info: https://examples.k8s.io/mysql-cinder-pd/README.md' + properties: + fsType: + description: 'Filesystem type to mount. Must be a filesystem + type supported by the host operating system. Examples: "ext4", + "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + More info: https://examples.k8s.io/mysql-cinder-pd/README.md' + type: string + readOnly: + description: 'Optional: Defaults to false (read/write). ReadOnly + here will force the ReadOnly setting in VolumeMounts. More + info: https://examples.k8s.io/mysql-cinder-pd/README.md' + type: boolean + secretRef: + description: 'Optional: points to a secret object containing + parameters used to connect to OpenStack.' + properties: + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + type: object + volumeID: + description: 'volume id used to identify the volume in cinder. + More info: https://examples.k8s.io/mysql-cinder-pd/README.md' + type: string + required: + - volumeID + type: object + configMap: + description: ConfigMap represents a configMap that should populate + this volume + properties: + defaultMode: + description: 'Optional: mode bits to use on created files + by default. Must be a value between 0 and 0777. Defaults + to 0644. Directories within the path are not affected by + this setting. This might be in conflict with other options + that affect the file mode, like fsGroup, and the result + can be other mode bits set.' + format: int32 + type: integer + items: + description: If unspecified, each key-value pair in the Data + field of the referenced ConfigMap will be projected into + the volume as a file whose name is the key and content is + the value. If specified, the listed keys will be projected + into the specified paths, and unlisted keys will not be + present. If a key is specified which is not present in the + ConfigMap, the volume setup will error unless it is marked + optional. Paths must be relative and may not contain the + '..' path or start with '..'. + items: + description: Maps a string key to a path within a volume. + properties: + key: + description: The key to project. + type: string + mode: + description: 'Optional: mode bits to use on this file, + must be a value between 0 and 0777. If not specified, + the volume defaultMode will be used. This might be + in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode + bits set.' + format: int32 + type: integer + path: + description: The relative path of the file to map the + key to. May not be an absolute path. May not contain + the path element '..'. May not start with the string + '..'. + type: string + required: + - key + - path + type: object + type: array + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + optional: + description: Specify whether the ConfigMap or its keys must + be defined + type: boolean + type: object + csi: + description: CSI (Container Storage Interface) represents storage + that is handled by an external CSI driver (Alpha feature). + properties: + driver: + description: Driver is the name of the CSI driver that handles + this volume. Consult with your admin for the correct name + as registered in the cluster. + type: string + fsType: + description: Filesystem type to mount. Ex. "ext4", "xfs", + "ntfs". If not provided, the empty value is passed to the + associated CSI driver which will determine the default filesystem + to apply. + type: string + nodePublishSecretRef: + description: NodePublishSecretRef is a reference to the secret + object containing sensitive information to pass to the CSI + driver to complete the CSI NodePublishVolume and NodeUnpublishVolume + calls. This field is optional, and may be empty if no secret + is required. If the secret object contains more than one + secret, all secret references are passed. + properties: + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + type: object + readOnly: + description: Specifies a read-only configuration for the volume. + Defaults to false (read/write). + type: boolean + volumeAttributes: + additionalProperties: + type: string + description: VolumeAttributes stores driver-specific properties + that are passed to the CSI driver. Consult your driver's + documentation for supported values. + type: object + required: + - driver + type: object + downwardAPI: + description: DownwardAPI represents downward API about the pod + that should populate this volume + properties: + defaultMode: + description: 'Optional: mode bits to use on created files + by default. Must be a value between 0 and 0777. Defaults + to 0644. Directories within the path are not affected by + this setting. This might be in conflict with other options + that affect the file mode, like fsGroup, and the result + can be other mode bits set.' + format: int32 + type: integer + items: + description: Items is a list of downward API volume file + items: + description: DownwardAPIVolumeFile represents information + to create the file containing the pod field + properties: + fieldRef: + description: 'Required: Selects a field of the pod: + only annotations, labels, name and namespace are supported.' + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in the + specified API version. + type: string + required: + - fieldPath + type: object + mode: + description: 'Optional: mode bits to use on this file, + must be a value between 0 and 0777. If not specified, + the volume defaultMode will be used. This might be + in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode + bits set.' + format: int32 + type: integer + path: + description: 'Required: Path is the relative path name + of the file to be created. Must not be absolute or + contain the ''..'' path. Must be utf-8 encoded. The + first item of the relative path must not start with + ''..''' + type: string + resourceFieldRef: + description: 'Selects a resource of the container: only + resources limits and requests (limits.cpu, limits.memory, + requests.cpu and requests.memory) are currently supported.' + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + description: Specifies the output format of the + exposed resources, defaults to "1" + type: string + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + required: + - path + type: object + type: array + type: object + emptyDir: + description: 'EmptyDir represents a temporary directory that shares + a pod''s lifetime. More info: https://kubernetes.io/docs/concepts/storage/volumes#emptydir' + properties: + medium: + description: 'What type of storage medium should back this + directory. The default is "" which means to use the node''s + default medium. Must be an empty string (default) or Memory. + More info: https://kubernetes.io/docs/concepts/storage/volumes#emptydir' + type: string + sizeLimit: + description: 'Total amount of local storage required for this + EmptyDir volume. The size limit is also applicable for memory + medium. The maximum usage on memory medium EmptyDir would + be the minimum value between the SizeLimit specified here + and the sum of memory limits of all containers in a pod. + The default is nil which means that the limit is undefined. + More info: http://kubernetes.io/docs/user-guide/volumes#emptydir' + type: string + type: object + fc: + description: FC represents a Fibre Channel resource that is attached + to a kubelet's host machine and then exposed to the pod. + properties: + fsType: + description: 'Filesystem type to mount. Must be a filesystem + type supported by the host operating system. Ex. "ext4", + "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + TODO: how do we prevent errors in the filesystem from compromising + the machine' + type: string + lun: + description: 'Optional: FC target lun number' + format: int32 + type: integer + readOnly: + description: 'Optional: Defaults to false (read/write). ReadOnly + here will force the ReadOnly setting in VolumeMounts.' + type: boolean + targetWWNs: + description: 'Optional: FC target worldwide names (WWNs)' + items: + type: string + type: array + wwids: + description: 'Optional: FC volume world wide identifiers (wwids) + Either wwids or combination of targetWWNs and lun must be + set, but not both simultaneously.' + items: + type: string + type: array + type: object + flexVolume: + description: FlexVolume represents a generic volume resource that + is provisioned/attached using an exec based plugin. + properties: + driver: + description: Driver is the name of the driver to use for this + volume. + type: string + fsType: + description: Filesystem type to mount. Must be a filesystem + type supported by the host operating system. Ex. "ext4", + "xfs", "ntfs". The default filesystem depends on FlexVolume + script. + type: string + options: + additionalProperties: + type: string + description: 'Optional: Extra command options if any.' + type: object + readOnly: + description: 'Optional: Defaults to false (read/write). ReadOnly + here will force the ReadOnly setting in VolumeMounts.' + type: boolean + secretRef: + description: 'Optional: SecretRef is reference to the secret + object containing sensitive information to pass to the plugin + scripts. This may be empty if no secret object is specified. + If the secret object contains more than one secret, all + secrets are passed to the plugin scripts.' + properties: + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + type: object + required: + - driver + type: object + flocker: + description: Flocker represents a Flocker volume attached to a + kubelet's host machine. This depends on the Flocker control + service being running + properties: + datasetName: + description: Name of the dataset stored as metadata -> name + on the dataset for Flocker should be considered as deprecated + type: string + datasetUUID: + description: UUID of the dataset. This is unique identifier + of a Flocker dataset + type: string + type: object + gcePersistentDisk: + description: 'GCEPersistentDisk represents a GCE Disk resource + that is attached to a kubelet''s host machine and then exposed + to the pod. More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk' + properties: + fsType: + description: 'Filesystem type of the volume that you want + to mount. Tip: Ensure that the filesystem type is supported + by the host operating system. Examples: "ext4", "xfs", "ntfs". + Implicitly inferred to be "ext4" if unspecified. More info: + https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk + TODO: how do we prevent errors in the filesystem from compromising + the machine' + type: string + partition: + description: 'The partition in the volume that you want to + mount. If omitted, the default is to mount by volume name. + Examples: For volume /dev/sda1, you specify the partition + as "1". Similarly, the volume partition for /dev/sda is + "0" (or you can leave the property empty). More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk' + format: int32 + type: integer + pdName: + description: 'Unique name of the PD resource in GCE. Used + to identify the disk in GCE. More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk' + type: string + readOnly: + description: 'ReadOnly here will force the ReadOnly setting + in VolumeMounts. Defaults to false. More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk' + type: boolean + required: + - pdName + type: object + gitRepo: + description: 'GitRepo represents a git repository at a particular + revision. DEPRECATED: GitRepo is deprecated. To provision a + container with a git repo, mount an EmptyDir into an InitContainer + that clones the repo using git, then mount the EmptyDir into + the Pod''s container.' + properties: + directory: + description: Target directory name. Must not contain or start + with '..'. If '.' is supplied, the volume directory will + be the git repository. Otherwise, if specified, the volume + will contain the git repository in the subdirectory with + the given name. + type: string + repository: + description: Repository URL + type: string + revision: + description: Commit hash for the specified revision. + type: string + required: + - repository + type: object + glusterfs: + description: 'Glusterfs represents a Glusterfs mount on the host + that shares a pod''s lifetime. More info: https://examples.k8s.io/volumes/glusterfs/README.md' + properties: + endpoints: + description: 'EndpointsName is the endpoint name that details + Glusterfs topology. More info: https://examples.k8s.io/volumes/glusterfs/README.md#create-a-pod' + type: string + path: + description: 'Path is the Glusterfs volume path. More info: + https://examples.k8s.io/volumes/glusterfs/README.md#create-a-pod' + type: string + readOnly: + description: 'ReadOnly here will force the Glusterfs volume + to be mounted with read-only permissions. Defaults to false. + More info: https://examples.k8s.io/volumes/glusterfs/README.md#create-a-pod' + type: boolean + required: + - endpoints + - path + type: object + hostPath: + description: 'HostPath represents a pre-existing file or directory + on the host machine that is directly exposed to the container. + This is generally used for system agents or other privileged + things that are allowed to see the host machine. Most containers + will NOT need this. More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath + --- TODO(jonesdl) We need to restrict who can use host directory + mounts and who can/can not mount host directories as read/write.' + properties: + path: + description: 'Path of the directory on the host. If the path + is a symlink, it will follow the link to the real path. + More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath' + type: string + type: + description: 'Type for HostPath Volume Defaults to "" More + info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath' + type: string + required: + - path + type: object + iscsi: + description: 'ISCSI represents an ISCSI Disk resource that is + attached to a kubelet''s host machine and then exposed to the + pod. More info: https://examples.k8s.io/volumes/iscsi/README.md' + properties: + chapAuthDiscovery: + description: whether support iSCSI Discovery CHAP authentication + type: boolean + chapAuthSession: + description: whether support iSCSI Session CHAP authentication + type: boolean + fsType: + description: 'Filesystem type of the volume that you want + to mount. Tip: Ensure that the filesystem type is supported + by the host operating system. Examples: "ext4", "xfs", "ntfs". + Implicitly inferred to be "ext4" if unspecified. More info: + https://kubernetes.io/docs/concepts/storage/volumes#iscsi + TODO: how do we prevent errors in the filesystem from compromising + the machine' + type: string + initiatorName: + description: Custom iSCSI Initiator Name. If initiatorName + is specified with iscsiInterface simultaneously, new iSCSI + interface : will be created + for the connection. + type: string + iqn: + description: Target iSCSI Qualified Name. + type: string + iscsiInterface: + description: iSCSI Interface Name that uses an iSCSI transport. + Defaults to 'default' (tcp). + type: string + lun: + description: iSCSI Target Lun number. + format: int32 + type: integer + portals: + description: iSCSI Target Portal List. The portal is either + an IP or ip_addr:port if the port is other than default + (typically TCP ports 860 and 3260). + items: + type: string + type: array + readOnly: + description: ReadOnly here will force the ReadOnly setting + in VolumeMounts. Defaults to false. + type: boolean + secretRef: + description: CHAP Secret for iSCSI target and initiator authentication + properties: + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + type: object + targetPortal: + description: iSCSI Target Portal. The Portal is either an + IP or ip_addr:port if the port is other than default (typically + TCP ports 860 and 3260). + type: string + required: + - iqn + - lun + - targetPortal + type: object + name: + description: 'Volume''s name. Must be a DNS_LABEL and unique within + the pod. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + nfs: + description: 'NFS represents an NFS mount on the host that shares + a pod''s lifetime More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs' + properties: + path: + description: 'Path that is exported by the NFS server. More + info: https://kubernetes.io/docs/concepts/storage/volumes#nfs' + type: string + readOnly: + description: 'ReadOnly here will force the NFS export to be + mounted with read-only permissions. Defaults to false. More + info: https://kubernetes.io/docs/concepts/storage/volumes#nfs' + type: boolean + server: + description: 'Server is the hostname or IP address of the + NFS server. More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs' + type: string + required: + - path + - server + type: object + persistentVolumeClaim: + description: 'PersistentVolumeClaimVolumeSource represents a reference + to a PersistentVolumeClaim in the same namespace. More info: + https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims' + properties: + claimName: + description: 'ClaimName is the name of a PersistentVolumeClaim + in the same namespace as the pod using this volume. More + info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims' + type: string + readOnly: + description: Will force the ReadOnly setting in VolumeMounts. + Default false. + type: boolean + required: + - claimName + type: object + photonPersistentDisk: + description: PhotonPersistentDisk represents a PhotonController + persistent disk attached and mounted on kubelets host machine + properties: + fsType: + description: Filesystem type to mount. Must be a filesystem + type supported by the host operating system. Ex. "ext4", + "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + type: string + pdID: + description: ID that identifies Photon Controller persistent + disk + type: string + required: + - pdID + type: object + portworxVolume: + description: PortworxVolume represents a portworx volume attached + and mounted on kubelets host machine + properties: + fsType: + description: FSType represents the filesystem type to mount + Must be a filesystem type supported by the host operating + system. Ex. "ext4", "xfs". Implicitly inferred to be "ext4" + if unspecified. + type: string + readOnly: + description: Defaults to false (read/write). ReadOnly here + will force the ReadOnly setting in VolumeMounts. + type: boolean + volumeID: + description: VolumeID uniquely identifies a Portworx volume + type: string + required: + - volumeID + type: object + projected: + description: Items for all in one resources secrets, configmaps, + and downward API + properties: + defaultMode: + description: Mode bits to use on created files by default. + Must be a value between 0 and 0777. Directories within the + path are not affected by this setting. This might be in + conflict with other options that affect the file mode, like + fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + sources: + description: list of volume projections + items: + description: Projection that may be projected along with + other supported volume types + properties: + configMap: + description: information about the configMap data to + project + properties: + items: + description: If unspecified, each key-value pair + in the Data field of the referenced ConfigMap + will be projected into the volume as a file whose + name is the key and content is the value. If specified, + the listed keys will be projected into the specified + paths, and unlisted keys will not be present. + If a key is specified which is not present in + the ConfigMap, the volume setup will error unless + it is marked optional. Paths must be relative + and may not contain the '..' path or start with + '..'. + items: + description: Maps a string key to a path within + a volume. + properties: + key: + description: The key to project. + type: string + mode: + description: 'Optional: mode bits to use on + this file, must be a value between 0 and + 0777. If not specified, the volume defaultMode + will be used. This might be in conflict + with other options that affect the file + mode, like fsGroup, and the result can be + other mode bits set.' + format: int32 + type: integer + path: + description: The relative path of the file + to map the key to. May not be an absolute + path. May not contain the path element '..'. + May not start with the string '..'. + type: string + required: + - key + - path + type: object + type: array + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, + uid?' + type: string + optional: + description: Specify whether the ConfigMap or its + keys must be defined + type: boolean + type: object + downwardAPI: + description: information about the downwardAPI data + to project + properties: + items: + description: Items is a list of DownwardAPIVolume + file + items: + description: DownwardAPIVolumeFile represents + information to create the file containing the + pod field + properties: + fieldRef: + description: 'Required: Selects a field of + the pod: only annotations, labels, name + and namespace are supported.' + properties: + apiVersion: + description: Version of the schema the + FieldPath is written in terms of, defaults + to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + mode: + description: 'Optional: mode bits to use on + this file, must be a value between 0 and + 0777. If not specified, the volume defaultMode + will be used. This might be in conflict + with other options that affect the file + mode, like fsGroup, and the result can be + other mode bits set.' + format: int32 + type: integer + path: + description: 'Required: Path is the relative + path name of the file to be created. Must + not be absolute or contain the ''..'' path. + Must be utf-8 encoded. The first item of + the relative path must not start with ''..''' + type: string + resourceFieldRef: + description: 'Selects a resource of the container: + only resources limits and requests (limits.cpu, + limits.memory, requests.cpu and requests.memory) + are currently supported.' + properties: + containerName: + description: 'Container name: required + for volumes, optional for env vars' + type: string + divisor: + description: Specifies the output format + of the exposed resources, defaults to + "1" + type: string + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + required: + - path + type: object + type: array + type: object + secret: + description: information about the secret data to project + properties: + items: + description: If unspecified, each key-value pair + in the Data field of the referenced Secret will + be projected into the volume as a file whose name + is the key and content is the value. If specified, + the listed keys will be projected into the specified + paths, and unlisted keys will not be present. + If a key is specified which is not present in + the Secret, the volume setup will error unless + it is marked optional. Paths must be relative + and may not contain the '..' path or start with + '..'. + items: + description: Maps a string key to a path within + a volume. + properties: + key: + description: The key to project. + type: string + mode: + description: 'Optional: mode bits to use on + this file, must be a value between 0 and + 0777. If not specified, the volume defaultMode + will be used. This might be in conflict + with other options that affect the file + mode, like fsGroup, and the result can be + other mode bits set.' + format: int32 + type: integer + path: + description: The relative path of the file + to map the key to. May not be an absolute + path. May not contain the path element '..'. + May not start with the string '..'. + type: string + required: + - key + - path + type: object + type: array + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, + uid?' + type: string + optional: + description: Specify whether the Secret or its key + must be defined + type: boolean + type: object + serviceAccountToken: + description: information about the serviceAccountToken + data to project + properties: + audience: + description: Audience is the intended audience of + the token. A recipient of a token must identify + itself with an identifier specified in the audience + of the token, and otherwise should reject the + token. The audience defaults to the identifier + of the apiserver. + type: string + expirationSeconds: + description: ExpirationSeconds is the requested + duration of validity of the service account token. + As the token approaches expiration, the kubelet + volume plugin will proactively rotate the service + account token. The kubelet will start trying to + rotate the token if the token is older than 80 + percent of its time to live or if the token is + older than 24 hours.Defaults to 1 hour and must + be at least 10 minutes. + format: int64 + type: integer + path: + description: Path is the path relative to the mount + point of the file to project the token into. + type: string + required: + - path + type: object + type: object + type: array + required: + - sources + type: object + quobyte: + description: Quobyte represents a Quobyte mount on the host that + shares a pod's lifetime + properties: + group: + description: Group to map volume access to Default is no group + type: string + readOnly: + description: ReadOnly here will force the Quobyte volume to + be mounted with read-only permissions. Defaults to false. + type: boolean + registry: + description: Registry represents a single or multiple Quobyte + Registry services specified as a string as host:port pair + (multiple entries are separated with commas) which acts + as the central registry for volumes + type: string + tenant: + description: Tenant owning the given Quobyte volume in the + Backend Used with dynamically provisioned Quobyte volumes, + value is set by the plugin + type: string + user: + description: User to map volume access to Defaults to serivceaccount + user + type: string + volume: + description: Volume is a string that references an already + created Quobyte volume by name. + type: string + required: + - registry + - volume + type: object + rbd: + description: 'RBD represents a Rados Block Device mount on the + host that shares a pod''s lifetime. More info: https://examples.k8s.io/volumes/rbd/README.md' + properties: + fsType: + description: 'Filesystem type of the volume that you want + to mount. Tip: Ensure that the filesystem type is supported + by the host operating system. Examples: "ext4", "xfs", "ntfs". + Implicitly inferred to be "ext4" if unspecified. More info: + https://kubernetes.io/docs/concepts/storage/volumes#rbd + TODO: how do we prevent errors in the filesystem from compromising + the machine' + type: string + image: + description: 'The rados image name. More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it' + type: string + keyring: + description: 'Keyring is the path to key ring for RBDUser. + Default is /etc/ceph/keyring. More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it' + type: string + monitors: + description: 'A collection of Ceph monitors. More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it' + items: + type: string + type: array + pool: + description: 'The rados pool name. Default is rbd. More info: + https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it' + type: string + readOnly: + description: 'ReadOnly here will force the ReadOnly setting + in VolumeMounts. Defaults to false. More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it' + type: boolean + secretRef: + description: 'SecretRef is name of the authentication secret + for RBDUser. If provided overrides keyring. Default is nil. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it' + properties: + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + type: object + user: + description: 'The rados user name. Default is admin. More + info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it' + type: string + required: + - image + - monitors + type: object + scaleIO: + description: ScaleIO represents a ScaleIO persistent volume attached + and mounted on Kubernetes nodes. + properties: + fsType: + description: Filesystem type to mount. Must be a filesystem + type supported by the host operating system. Ex. "ext4", + "xfs", "ntfs". Default is "xfs". + type: string + gateway: + description: The host address of the ScaleIO API Gateway. + type: string + protectionDomain: + description: The name of the ScaleIO Protection Domain for + the configured storage. + type: string + readOnly: + description: Defaults to false (read/write). ReadOnly here + will force the ReadOnly setting in VolumeMounts. + type: boolean + secretRef: + description: SecretRef references to the secret for ScaleIO + user and other sensitive information. If this is not provided, + Login operation will fail. + properties: + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + type: object + sslEnabled: + description: Flag to enable/disable SSL communication with + Gateway, default false + type: boolean + storageMode: + description: Indicates whether the storage for a volume should + be ThickProvisioned or ThinProvisioned. Default is ThinProvisioned. + type: string + storagePool: + description: The ScaleIO Storage Pool associated with the + protection domain. + type: string + system: + description: The name of the storage system as configured + in ScaleIO. + type: string + volumeName: + description: The name of a volume already created in the ScaleIO + system that is associated with this volume source. + type: string + required: + - gateway + - secretRef + - system + type: object + secret: + description: 'Secret represents a secret that should populate + this volume. More info: https://kubernetes.io/docs/concepts/storage/volumes#secret' + properties: + defaultMode: + description: 'Optional: mode bits to use on created files + by default. Must be a value between 0 and 0777. Defaults + to 0644. Directories within the path are not affected by + this setting. This might be in conflict with other options + that affect the file mode, like fsGroup, and the result + can be other mode bits set.' + format: int32 + type: integer + items: + description: If unspecified, each key-value pair in the Data + field of the referenced Secret will be projected into the + volume as a file whose name is the key and content is the + value. If specified, the listed keys will be projected into + the specified paths, and unlisted keys will not be present. + If a key is specified which is not present in the Secret, + the volume setup will error unless it is marked optional. + Paths must be relative and may not contain the '..' path + or start with '..'. + items: + description: Maps a string key to a path within a volume. + properties: + key: + description: The key to project. + type: string + mode: + description: 'Optional: mode bits to use on this file, + must be a value between 0 and 0777. If not specified, + the volume defaultMode will be used. This might be + in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode + bits set.' + format: int32 + type: integer + path: + description: The relative path of the file to map the + key to. May not be an absolute path. May not contain + the path element '..'. May not start with the string + '..'. + type: string + required: + - key + - path + type: object + type: array + optional: + description: Specify whether the Secret or its keys must be + defined + type: boolean + secretName: + description: 'Name of the secret in the pod''s namespace to + use. More info: https://kubernetes.io/docs/concepts/storage/volumes#secret' + type: string + type: object + storageos: + description: StorageOS represents a StorageOS volume attached + and mounted on Kubernetes nodes. + properties: + fsType: + description: Filesystem type to mount. Must be a filesystem + type supported by the host operating system. Ex. "ext4", + "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + type: string + readOnly: + description: Defaults to false (read/write). ReadOnly here + will force the ReadOnly setting in VolumeMounts. + type: boolean + secretRef: + description: SecretRef specifies the secret to use for obtaining + the StorageOS API credentials. If not specified, default + values will be attempted. + properties: + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + type: object + volumeName: + description: VolumeName is the human-readable name of the + StorageOS volume. Volume names are only unique within a + namespace. + type: string + volumeNamespace: + description: VolumeNamespace specifies the scope of the volume + within StorageOS. If no namespace is specified then the + Pod's namespace will be used. This allows the Kubernetes + name scoping to be mirrored within StorageOS for tighter + integration. Set VolumeName to any name to override the + default behaviour. Set to "default" if you are not using + namespaces within StorageOS. Namespaces that do not pre-exist + within StorageOS will be created. + type: string + type: object + vsphereVolume: + description: VsphereVolume represents a vSphere volume attached + and mounted on kubelets host machine + properties: + fsType: + description: Filesystem type to mount. Must be a filesystem + type supported by the host operating system. Ex. "ext4", + "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + type: string + storagePolicyID: + description: Storage Policy Based Management (SPBM) profile + ID associated with the StoragePolicyName. + type: string + storagePolicyName: + description: Storage Policy Based Management (SPBM) profile + name. + type: string + volumePath: + description: Path that identifies vSphere volume vmdk + type: string + required: + - volumePath + type: object + required: + - name + type: object + type: array + type: object + status: + description: LicenseMasterStatus defines the observed state of a Splunk + Enterprise license master. + properties: + phase: + description: current phase of the license master + enum: + - Pending + - Ready + - Updating + - ScalingUp + - ScalingDown + - Terminating + - Error + type: string + type: object + type: object + version: v1beta1 + versions: + - name: v1beta1 + served: true + storage: true + - name: v1alpha3 + served: true + storage: false + - name: v1alpha2 + served: true + storage: false diff --git a/deploy/olm-catalog/splunk/0.2.2/enterprise.splunk.com_searchheadclusters_crd.yaml b/deploy/olm-catalog/splunk/0.2.2/enterprise.splunk.com_searchheadclusters_crd.yaml new file mode 100644 index 000000000..4210949c2 --- /dev/null +++ b/deploy/olm-catalog/splunk/0.2.2/enterprise.splunk.com_searchheadclusters_crd.yaml @@ -0,0 +1,2414 @@ +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: searchheadclusters.enterprise.splunk.com +spec: + additionalPrinterColumns: + - JSONPath: .status.phase + description: Status of search head cluster + name: Phase + type: string + - JSONPath: .status.deployerPhase + description: Status of the deployer + name: Deployer + type: string + - JSONPath: .status.replicas + description: Desired number of search head cluster members + name: Desired + type: integer + - JSONPath: .status.readyReplicas + description: Current number of ready search head cluster members + name: Ready + type: integer + - JSONPath: .metadata.creationTimestamp + description: Age of search head cluster + name: Age + type: date + group: enterprise.splunk.com + names: + kind: SearchHeadCluster + listKind: SearchHeadClusterList + plural: searchheadclusters + shortNames: + - shc + singular: searchheadcluster + scope: Namespaced + subresources: + scale: + labelSelectorPath: .status.selector + specReplicasPath: .spec.replicas + statusReplicasPath: .status.replicas + status: {} + validation: + openAPIV3Schema: + description: SearchHeadCluster is the Schema for a Splunk Enterprise search + head cluster + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: SearchHeadClusterSpec defines the desired state of a Splunk + Enterprise search head cluster + properties: + Mock: + description: Mock to differentiate between UTs and actual reconcile + type: boolean + affinity: + description: Kubernetes Affinity rules that control how pods are assigned + to particular nodes. + properties: + nodeAffinity: + description: Describes node affinity scheduling rules for the pod. + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: The scheduler will prefer to schedule pods to nodes + that satisfy the affinity expressions specified by this field, + but it may choose a node that violates one or more of the + expressions. The node that is most preferred is the one with + the greatest sum of weights, i.e. for each node that meets + all of the scheduling requirements (resource request, requiredDuringScheduling + affinity expressions, etc.), compute a sum by iterating through + the elements of this field and adding "weight" to the sum + if the node matches the corresponding matchExpressions; the + node(s) with the highest sum are the most preferred. + items: + description: An empty preferred scheduling term matches all + objects with implicit weight 0 (i.e. it's a no-op). A null + preferred scheduling term matches no objects (i.e. is also + a no-op). + properties: + preference: + description: A node selector term, associated with the + corresponding weight. + properties: + matchExpressions: + description: A list of node selector requirements + by node's labels. + items: + description: A node selector requirement is a selector + that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: Represents a key's relationship + to a set of values. Valid operators are In, + NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: An array of string values. If the + operator is In or NotIn, the values array + must be non-empty. If the operator is Exists + or DoesNotExist, the values array must be + empty. If the operator is Gt or Lt, the values + array must have a single element, which will + be interpreted as an integer. This array is + replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchFields: + description: A list of node selector requirements + by node's fields. + items: + description: A node selector requirement is a selector + that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: Represents a key's relationship + to a set of values. Valid operators are In, + NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: An array of string values. If the + operator is In or NotIn, the values array + must be non-empty. If the operator is Exists + or DoesNotExist, the values array must be + empty. If the operator is Gt or Lt, the values + array must have a single element, which will + be interpreted as an integer. This array is + replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + type: object + weight: + description: Weight associated with matching the corresponding + nodeSelectorTerm, in the range 1-100. + format: int32 + type: integer + required: + - preference + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + description: If the affinity requirements specified by this + field are not met at scheduling time, the pod will not be + scheduled onto the node. If the affinity requirements specified + by this field cease to be met at some point during pod execution + (e.g. due to an update), the system may or may not try to + eventually evict the pod from its node. + properties: + nodeSelectorTerms: + description: Required. A list of node selector terms. The + terms are ORed. + items: + description: A null or empty node selector term matches + no objects. The requirements of them are ANDed. The + TopologySelectorTerm type implements a subset of the + NodeSelectorTerm. + properties: + matchExpressions: + description: A list of node selector requirements + by node's labels. + items: + description: A node selector requirement is a selector + that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: Represents a key's relationship + to a set of values. Valid operators are In, + NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: An array of string values. If the + operator is In or NotIn, the values array + must be non-empty. If the operator is Exists + or DoesNotExist, the values array must be + empty. If the operator is Gt or Lt, the values + array must have a single element, which will + be interpreted as an integer. This array is + replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchFields: + description: A list of node selector requirements + by node's fields. + items: + description: A node selector requirement is a selector + that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: Represents a key's relationship + to a set of values. Valid operators are In, + NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: An array of string values. If the + operator is In or NotIn, the values array + must be non-empty. If the operator is Exists + or DoesNotExist, the values array must be + empty. If the operator is Gt or Lt, the values + array must have a single element, which will + be interpreted as an integer. This array is + replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + type: object + type: array + required: + - nodeSelectorTerms + type: object + type: object + podAffinity: + description: Describes pod affinity scheduling rules (e.g. co-locate + this pod in the same node, zone, etc. as some other pod(s)). + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: The scheduler will prefer to schedule pods to nodes + that satisfy the affinity expressions specified by this field, + but it may choose a node that violates one or more of the + expressions. The node that is most preferred is the one with + the greatest sum of weights, i.e. for each node that meets + all of the scheduling requirements (resource request, requiredDuringScheduling + affinity expressions, etc.), compute a sum by iterating through + the elements of this field and adding "weight" to the sum + if the node has pods which matches the corresponding podAffinityTerm; + the node(s) with the highest sum are the most preferred. + items: + description: The weights of all of the matched WeightedPodAffinityTerm + fields are added per-node to find the most preferred node(s) + properties: + podAffinityTerm: + description: Required. A pod affinity term, associated + with the corresponding weight. + properties: + labelSelector: + description: A label query over a set of resources, + in this case pods. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: A label selector requirement is + a selector that contains values, a key, and + an operator that relates the key and values. + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: operator represents a key's + relationship to a set of values. Valid + operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: values is an array of string + values. If the operator is In or NotIn, + the values array must be non-empty. If + the operator is Exists or DoesNotExist, + the values array must be empty. This array + is replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, + whose key field is "key", the operator is "In", + and the values array contains only "value". + The requirements are ANDed. + type: object + type: object + namespaces: + description: namespaces specifies which namespaces + the labelSelector applies to (matches against); + null or empty list means "this pod's namespace" + items: + type: string + type: array + topologyKey: + description: This pod should be co-located (affinity) + or not co-located (anti-affinity) with the pods + matching the labelSelector in the specified namespaces, + where co-located is defined as running on a node + whose value of the label with key topologyKey matches + that of any node on which any of the selected pods + is running. Empty topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + weight: + description: weight associated with matching the corresponding + podAffinityTerm, in the range 1-100. + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + description: If the affinity requirements specified by this + field are not met at scheduling time, the pod will not be + scheduled onto the node. If the affinity requirements specified + by this field cease to be met at some point during pod execution + (e.g. due to a pod label update), the system may or may not + try to eventually evict the pod from its node. When there + are multiple elements, the lists of nodes corresponding to + each podAffinityTerm are intersected, i.e. all terms must + be satisfied. + items: + description: Defines a set of pods (namely those matching + the labelSelector relative to the given namespace(s)) that + this pod should be co-located (affinity) or not co-located + (anti-affinity) with, where co-located is defined as running + on a node whose value of the label with key + matches that of any node on which a pod of the set of pods + is running + properties: + labelSelector: + description: A label query over a set of resources, in + this case pods. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: A label selector requirement is a selector + that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: operator represents a key's relationship + to a set of values. Valid operators are In, + NotIn, Exists and DoesNotExist. + type: string + values: + description: values is an array of string values. + If the operator is In or NotIn, the values + array must be non-empty. If the operator is + Exists or DoesNotExist, the values array must + be empty. This array is replaced during a + strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} pairs. + A single {key,value} in the matchLabels map is equivalent + to an element of matchExpressions, whose key field + is "key", the operator is "In", and the values array + contains only "value". The requirements are ANDed. + type: object + type: object + namespaces: + description: namespaces specifies which namespaces the + labelSelector applies to (matches against); null or + empty list means "this pod's namespace" + items: + type: string + type: array + topologyKey: + description: This pod should be co-located (affinity) + or not co-located (anti-affinity) with the pods matching + the labelSelector in the specified namespaces, where + co-located is defined as running on a node whose value + of the label with key topologyKey matches that of any + node on which any of the selected pods is running. Empty + topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + type: array + type: object + podAntiAffinity: + description: Describes pod anti-affinity scheduling rules (e.g. + avoid putting this pod in the same node, zone, etc. as some other + pod(s)). + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: The scheduler will prefer to schedule pods to nodes + that satisfy the anti-affinity expressions specified by this + field, but it may choose a node that violates one or more + of the expressions. The node that is most preferred is the + one with the greatest sum of weights, i.e. for each node that + meets all of the scheduling requirements (resource request, + requiredDuringScheduling anti-affinity expressions, etc.), + compute a sum by iterating through the elements of this field + and adding "weight" to the sum if the node has pods which + matches the corresponding podAffinityTerm; the node(s) with + the highest sum are the most preferred. + items: + description: The weights of all of the matched WeightedPodAffinityTerm + fields are added per-node to find the most preferred node(s) + properties: + podAffinityTerm: + description: Required. A pod affinity term, associated + with the corresponding weight. + properties: + labelSelector: + description: A label query over a set of resources, + in this case pods. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: A label selector requirement is + a selector that contains values, a key, and + an operator that relates the key and values. + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: operator represents a key's + relationship to a set of values. Valid + operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: values is an array of string + values. If the operator is In or NotIn, + the values array must be non-empty. If + the operator is Exists or DoesNotExist, + the values array must be empty. This array + is replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, + whose key field is "key", the operator is "In", + and the values array contains only "value". + The requirements are ANDed. + type: object + type: object + namespaces: + description: namespaces specifies which namespaces + the labelSelector applies to (matches against); + null or empty list means "this pod's namespace" + items: + type: string + type: array + topologyKey: + description: This pod should be co-located (affinity) + or not co-located (anti-affinity) with the pods + matching the labelSelector in the specified namespaces, + where co-located is defined as running on a node + whose value of the label with key topologyKey matches + that of any node on which any of the selected pods + is running. Empty topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + weight: + description: weight associated with matching the corresponding + podAffinityTerm, in the range 1-100. + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + description: If the anti-affinity requirements specified by + this field are not met at scheduling time, the pod will not + be scheduled onto the node. If the anti-affinity requirements + specified by this field cease to be met at some point during + pod execution (e.g. due to a pod label update), the system + may or may not try to eventually evict the pod from its node. + When there are multiple elements, the lists of nodes corresponding + to each podAffinityTerm are intersected, i.e. all terms must + be satisfied. + items: + description: Defines a set of pods (namely those matching + the labelSelector relative to the given namespace(s)) that + this pod should be co-located (affinity) or not co-located + (anti-affinity) with, where co-located is defined as running + on a node whose value of the label with key + matches that of any node on which a pod of the set of pods + is running + properties: + labelSelector: + description: A label query over a set of resources, in + this case pods. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: A label selector requirement is a selector + that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: operator represents a key's relationship + to a set of values. Valid operators are In, + NotIn, Exists and DoesNotExist. + type: string + values: + description: values is an array of string values. + If the operator is In or NotIn, the values + array must be non-empty. If the operator is + Exists or DoesNotExist, the values array must + be empty. This array is replaced during a + strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} pairs. + A single {key,value} in the matchLabels map is equivalent + to an element of matchExpressions, whose key field + is "key", the operator is "In", and the values array + contains only "value". The requirements are ANDed. + type: object + type: object + namespaces: + description: namespaces specifies which namespaces the + labelSelector applies to (matches against); null or + empty list means "this pod's namespace" + items: + type: string + type: array + topologyKey: + description: This pod should be co-located (affinity) + or not co-located (anti-affinity) with the pods matching + the labelSelector in the specified namespaces, where + co-located is defined as running on a node whose value + of the label with key topologyKey matches that of any + node on which any of the selected pods is running. Empty + topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + type: array + type: object + type: object + clusterMasterRef: + description: ClusterMasterRef refers to a Splunk Enterprise indexer + cluster managed by the operator within Kubernetes + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: 'If referring to a piece of an object instead of an + entire object, this string should contain a valid JSON/Go field + access statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to a container within + a pod, this would take on a value like: "spec.containers{name}" + (where "name" refers to the name of the container that triggered + the event) or if no container name is specified "spec.containers[2]" + (container with index 2 in this pod). This syntax is chosen only + to have some well-defined way of referencing a part of an object. + TODO: this design is not final and this field is subject to change + in the future.' + type: string + kind: + description: 'Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + namespace: + description: 'Namespace of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/' + type: string + resourceVersion: + description: 'Specific resourceVersion to which this reference is + made, if any. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency' + type: string + uid: + description: 'UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids' + type: string + type: object + defaults: + description: Inline map of default.yml overrides used to initialize + the environment + type: string + defaultsUrl: + description: Full path or URL for one or more default.yml files, separated + by commas + type: string + defaultsUrlApps: + description: Full path or URL for one or more defaults.yml files specific + to App install, separated by commas. The defaults listed here will + be installed on the CM, standalone, search head deployer or license + master instance. + type: string + etcVolumeStorageConfig: + description: Storage configuration for /opt/splunk/etc volume + properties: + ephemeralStorage: + description: If true, ephemeral (emptyDir) storage will be used + type: boolean + storageCapacity: + description: Storage capacity to request persistent volume claims + (default=”10Gi” for etc and "100Gi" for var) + type: string + storageClassName: + description: Name of StorageClass to use for persistent volume claims + type: string + type: object + image: + description: Image to use for Splunk pod containers (overrides RELATED_IMAGE_SPLUNK_ENTERPRISE + environment variables) + type: string + imagePullPolicy: + description: 'Sets pull policy for all images (either “Always” or the + default: “IfNotPresent”)' + enum: + - Always + - IfNotPresent + type: string + licenseMasterRef: + description: LicenseMasterRef refers to a Splunk Enterprise license + master managed by the operator within Kubernetes + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: 'If referring to a piece of an object instead of an + entire object, this string should contain a valid JSON/Go field + access statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to a container within + a pod, this would take on a value like: "spec.containers{name}" + (where "name" refers to the name of the container that triggered + the event) or if no container name is specified "spec.containers[2]" + (container with index 2 in this pod). This syntax is chosen only + to have some well-defined way of referencing a part of an object. + TODO: this design is not final and this field is subject to change + in the future.' + type: string + kind: + description: 'Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + namespace: + description: 'Namespace of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/' + type: string + resourceVersion: + description: 'Specific resourceVersion to which this reference is + made, if any. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency' + type: string + uid: + description: 'UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids' + type: string + type: object + licenseUrl: + description: Full path or URL for a Splunk Enterprise license file + type: string + replicas: + description: Number of search head pods; a search head cluster will + be created if > 1 + format: int32 + type: integer + resources: + description: resource requirements for the pod containers + properties: + limits: + additionalProperties: + type: string + description: 'Limits describes the maximum amount of compute resources + allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/' + type: object + requests: + additionalProperties: + type: string + description: 'Requests describes the minimum amount of compute resources + required. If Requests is omitted for a container, it defaults + to Limits if that is explicitly specified, otherwise to an implementation-defined + value. More info: https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/' + type: object + type: object + schedulerName: + description: Name of Scheduler to use for pod placement (defaults to + “default-scheduler”) + type: string + serviceAccount: + description: ServiceAccount is the service account used by the pods + deployed by the CRD. If not specified uses the default serviceAccount + for the namespace as per https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/#use-the-default-service-account-to-access-the-api-server + type: string + serviceTemplate: + description: ServiceTemplate is a template used to create Kubernetes + services + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the + latest internal value, and may reject unrecognized values. More + info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource + this object represents. Servers may infer this from the endpoint + the client submits requests to. Cannot be updated. In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + description: 'Standard object''s metadata. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata' + type: object + spec: + description: Spec defines the behavior of a service. https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status + properties: + clusterIP: + description: 'clusterIP is the IP address of the service and + is usually assigned randomly by the master. If an address + is specified manually and is not in use by others, it will + be allocated to the service; otherwise, creation of the service + will fail. This field can not be changed through updates. + Valid values are "None", empty string (""), or a valid IP + address. "None" can be specified for headless services when + proxying is not required. Only applies to types ClusterIP, + NodePort, and LoadBalancer. Ignored if type is ExternalName. + More info: https://kubernetes.io/docs/concepts/services-networking/service/#virtual-ips-and-service-proxies' + type: string + externalIPs: + description: externalIPs is a list of IP addresses for which + nodes in the cluster will also accept traffic for this service. These + IPs are not managed by Kubernetes. The user is responsible + for ensuring that traffic arrives at a node with this IP. A + common example is external load-balancers that are not part + of the Kubernetes system. + items: + type: string + type: array + externalName: + description: externalName is the external reference that kubedns + or equivalent will return as a CNAME record for this service. + No proxying will be involved. Must be a valid RFC-1123 hostname + (https://tools.ietf.org/html/rfc1123) and requires Type to + be ExternalName. + type: string + externalTrafficPolicy: + description: externalTrafficPolicy denotes if this Service desires + to route external traffic to node-local or cluster-wide endpoints. + "Local" preserves the client source IP and avoids a second + hop for LoadBalancer and Nodeport type services, but risks + potentially imbalanced traffic spreading. "Cluster" obscures + the client source IP and may cause a second hop to another + node, but should have good overall load-spreading. + type: string + healthCheckNodePort: + description: healthCheckNodePort specifies the healthcheck nodePort + for the service. If not specified, HealthCheckNodePort is + created by the service api backend with the allocated nodePort. + Will use user-specified nodePort value if specified by the + client. Only effects when Type is set to LoadBalancer and + ExternalTrafficPolicy is set to Local. + format: int32 + type: integer + ipFamily: + description: ipFamily specifies whether this Service has a preference + for a particular IP family (e.g. IPv4 vs. IPv6). If a specific + IP family is requested, the clusterIP field will be allocated + from that family, if it is available in the cluster. If no + IP family is requested, the cluster's primary IP family will + be used. Other IP fields (loadBalancerIP, loadBalancerSourceRanges, + externalIPs) and controllers which allocate external load-balancers + should use the same IP family. Endpoints for this Service + will be of this family. This field is immutable after creation. + Assigning a ServiceIPFamily not available in the cluster (e.g. + IPv6 in IPv4 only cluster) is an error condition and will + fail during clusterIP assignment. + type: string + loadBalancerIP: + description: 'Only applies to Service Type: LoadBalancer LoadBalancer + will get created with the IP specified in this field. This + feature depends on whether the underlying cloud-provider supports + specifying the loadBalancerIP when a load balancer is created. + This field will be ignored if the cloud-provider does not + support the feature.' + type: string + loadBalancerSourceRanges: + description: 'If specified and supported by the platform, this + will restrict traffic through the cloud-provider load-balancer + will be restricted to the specified client IPs. This field + will be ignored if the cloud-provider does not support the + feature." More info: https://kubernetes.io/docs/tasks/access-application-cluster/configure-cloud-provider-firewall/' + items: + type: string + type: array + ports: + description: 'The list of ports that are exposed by this service. + More info: https://kubernetes.io/docs/concepts/services-networking/service/#virtual-ips-and-service-proxies' + items: + description: ServicePort contains information on service's + port. + properties: + name: + description: The name of this port within the service. + This must be a DNS_LABEL. All ports within a ServiceSpec + must have unique names. When considering the endpoints + for a Service, this must match the 'name' field in the + EndpointPort. Optional if only one ServicePort is defined + on this service. + type: string + nodePort: + description: 'The port on each node on which this service + is exposed when type=NodePort or LoadBalancer. Usually + assigned by the system. If specified, it will be allocated + to the service if unused or else creation of the service + will fail. Default is to auto-allocate a port if the + ServiceType of this Service requires one. More info: + https://kubernetes.io/docs/concepts/services-networking/service/#type-nodeport' + format: int32 + type: integer + port: + description: The port that will be exposed by this service. + format: int32 + type: integer + protocol: + description: The IP protocol for this port. Supports "TCP", + "UDP", and "SCTP". Default is TCP. + type: string + targetPort: + anyOf: + - type: integer + - type: string + description: 'Number or name of the port to access on + the pods targeted by the service. Number must be in + the range 1 to 65535. Name must be an IANA_SVC_NAME. + If this is a string, it will be looked up as a named + port in the target Pod''s container ports. If this is + not specified, the value of the ''port'' field is used + (an identity map). This field is ignored for services + with clusterIP=None, and should be omitted or set equal + to the ''port'' field. More info: https://kubernetes.io/docs/concepts/services-networking/service/#defining-a-service' + x-kubernetes-int-or-string: true + required: + - port + type: object + type: array + publishNotReadyAddresses: + description: publishNotReadyAddresses, when set to true, indicates + that DNS implementations must publish the notReadyAddresses + of subsets for the Endpoints associated with the Service. + The default value is false. The primary use case for setting + this field is to use a StatefulSet's Headless Service to propagate + SRV records for its Pods without respect to their readiness + for purpose of peer discovery. + type: boolean + selector: + additionalProperties: + type: string + description: 'Route service traffic to pods with label keys + and values matching this selector. If empty or not present, + the service is assumed to have an external process managing + its endpoints, which Kubernetes will not modify. Only applies + to types ClusterIP, NodePort, and LoadBalancer. Ignored if + type is ExternalName. More info: https://kubernetes.io/docs/concepts/services-networking/service/' + type: object + sessionAffinity: + description: 'Supports "ClientIP" and "None". Used to maintain + session affinity. Enable client IP based session affinity. + Must be ClientIP or None. Defaults to None. More info: https://kubernetes.io/docs/concepts/services-networking/service/#virtual-ips-and-service-proxies' + type: string + sessionAffinityConfig: + description: sessionAffinityConfig contains the configurations + of session affinity. + properties: + clientIP: + description: clientIP contains the configurations of Client + IP based session affinity. + properties: + timeoutSeconds: + description: timeoutSeconds specifies the seconds of + ClientIP type session sticky time. The value must + be >0 && <=86400(for 1 day) if ServiceAffinity == + "ClientIP". Default value is 10800(for 3 hours). + format: int32 + type: integer + type: object + type: object + type: + description: 'type determines how the Service is exposed. Defaults + to ClusterIP. Valid options are ExternalName, ClusterIP, NodePort, + and LoadBalancer. "ExternalName" maps to the specified externalName. + "ClusterIP" allocates a cluster-internal IP address for load-balancing + to endpoints. Endpoints are determined by the selector or + if that is not specified, by manual construction of an Endpoints + object. If clusterIP is "None", no virtual IP is allocated + and the endpoints are published as a set of endpoints rather + than a stable IP. "NodePort" builds on ClusterIP and allocates + a port on every node which routes to the clusterIP. "LoadBalancer" + builds on NodePort and creates an external load-balancer (if + supported in the current cloud) which routes to the clusterIP. + More info: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types' + type: string + type: object + status: + description: 'Most recently observed status of the service. Populated + by the system. Read-only. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status' + properties: + loadBalancer: + description: LoadBalancer contains the current status of the + load-balancer, if one is present. + properties: + ingress: + description: Ingress is a list containing ingress points + for the load-balancer. Traffic intended for the service + should be sent to these ingress points. + items: + description: 'LoadBalancerIngress represents the status + of a load-balancer ingress point: traffic intended for + the service should be sent to an ingress point.' + properties: + hostname: + description: Hostname is set for load-balancer ingress + points that are DNS based (typically AWS load-balancers) + type: string + ip: + description: IP is set for load-balancer ingress points + that are IP based (typically GCE or OpenStack load-balancers) + type: string + type: object + type: array + type: object + type: object + type: object + sparkImage: + description: Image to use for Spark pod containers (overrides RELATED_IMAGE_SPLUNK_SPARK + environment variables) + type: string + sparkRef: + description: SparkRef refers to a Spark cluster managed by the operator + within Kubernetes When defined, Data Fabric Search (DFS) will be enabled + and configured to use the Spark cluster. + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: 'If referring to a piece of an object instead of an + entire object, this string should contain a valid JSON/Go field + access statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to a container within + a pod, this would take on a value like: "spec.containers{name}" + (where "name" refers to the name of the container that triggered + the event) or if no container name is specified "spec.containers[2]" + (container with index 2 in this pod). This syntax is chosen only + to have some well-defined way of referencing a part of an object. + TODO: this design is not final and this field is subject to change + in the future.' + type: string + kind: + description: 'Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + namespace: + description: 'Namespace of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/' + type: string + resourceVersion: + description: 'Specific resourceVersion to which this reference is + made, if any. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency' + type: string + uid: + description: 'UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids' + type: string + type: object + tolerations: + description: Pod's tolerations for Kubernetes node's taint + items: + description: The pod this Toleration is attached to tolerates any + taint that matches the triple using the matching + operator . + properties: + effect: + description: Effect indicates the taint effect to match. Empty + means match all taint effects. When specified, allowed values + are NoSchedule, PreferNoSchedule and NoExecute. + type: string + key: + description: Key is the taint key that the toleration applies + to. Empty means match all taint keys. If the key is empty, operator + must be Exists; this combination means to match all values and + all keys. + type: string + operator: + description: Operator represents a key's relationship to the value. + Valid operators are Exists and Equal. Defaults to Equal. Exists + is equivalent to wildcard for value, so that a pod can tolerate + all taints of a particular category. + type: string + tolerationSeconds: + description: TolerationSeconds represents the period of time the + toleration (which must be of effect NoExecute, otherwise this + field is ignored) tolerates the taint. By default, it is not + set, which means tolerate the taint forever (do not evict). + Zero and negative values will be treated as 0 (evict immediately) + by the system. + format: int64 + type: integer + value: + description: Value is the taint value the toleration matches to. + If the operator is Exists, the value should be empty, otherwise + just a regular string. + type: string + type: object + type: array + varVolumeStorageConfig: + description: Storage configuration for /opt/splunk/var volume + properties: + ephemeralStorage: + description: If true, ephemeral (emptyDir) storage will be used + type: boolean + storageCapacity: + description: Storage capacity to request persistent volume claims + (default=”10Gi” for etc and "100Gi" for var) + type: string + storageClassName: + description: Name of StorageClass to use for persistent volume claims + type: string + type: object + volumes: + description: List of one or more Kubernetes volumes. These will be mounted + in all pod containers as as /mnt/ + items: + description: Volume represents a named volume in a pod that may be + accessed by any container in the pod. + properties: + awsElasticBlockStore: + description: 'AWSElasticBlockStore represents an AWS Disk resource + that is attached to a kubelet''s host machine and then exposed + to the pod. More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore' + properties: + fsType: + description: 'Filesystem type of the volume that you want + to mount. Tip: Ensure that the filesystem type is supported + by the host operating system. Examples: "ext4", "xfs", "ntfs". + Implicitly inferred to be "ext4" if unspecified. More info: + https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore + TODO: how do we prevent errors in the filesystem from compromising + the machine' + type: string + partition: + description: 'The partition in the volume that you want to + mount. If omitted, the default is to mount by volume name. + Examples: For volume /dev/sda1, you specify the partition + as "1". Similarly, the volume partition for /dev/sda is + "0" (or you can leave the property empty).' + format: int32 + type: integer + readOnly: + description: 'Specify "true" to force and set the ReadOnly + property in VolumeMounts to "true". If omitted, the default + is "false". More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore' + type: boolean + volumeID: + description: 'Unique ID of the persistent disk resource in + AWS (Amazon EBS volume). More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore' + type: string + required: + - volumeID + type: object + azureDisk: + description: AzureDisk represents an Azure Data Disk mount on + the host and bind mount to the pod. + properties: + cachingMode: + description: 'Host Caching mode: None, Read Only, Read Write.' + type: string + diskName: + description: The Name of the data disk in the blob storage + type: string + diskURI: + description: The URI the data disk in the blob storage + type: string + fsType: + description: Filesystem type to mount. Must be a filesystem + type supported by the host operating system. Ex. "ext4", + "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + type: string + kind: + description: 'Expected values Shared: multiple blob disks + per storage account Dedicated: single blob disk per storage + account Managed: azure managed data disk (only in managed + availability set). defaults to shared' + type: string + readOnly: + description: Defaults to false (read/write). ReadOnly here + will force the ReadOnly setting in VolumeMounts. + type: boolean + required: + - diskName + - diskURI + type: object + azureFile: + description: AzureFile represents an Azure File Service mount + on the host and bind mount to the pod. + properties: + readOnly: + description: Defaults to false (read/write). ReadOnly here + will force the ReadOnly setting in VolumeMounts. + type: boolean + secretName: + description: the name of secret that contains Azure Storage + Account Name and Key + type: string + shareName: + description: Share Name + type: string + required: + - secretName + - shareName + type: object + cephfs: + description: CephFS represents a Ceph FS mount on the host that + shares a pod's lifetime + properties: + monitors: + description: 'Required: Monitors is a collection of Ceph monitors + More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it' + items: + type: string + type: array + path: + description: 'Optional: Used as the mounted root, rather than + the full Ceph tree, default is /' + type: string + readOnly: + description: 'Optional: Defaults to false (read/write). ReadOnly + here will force the ReadOnly setting in VolumeMounts. More + info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it' + type: boolean + secretFile: + description: 'Optional: SecretFile is the path to key ring + for User, default is /etc/ceph/user.secret More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it' + type: string + secretRef: + description: 'Optional: SecretRef is reference to the authentication + secret for User, default is empty. More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it' + properties: + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + type: object + user: + description: 'Optional: User is the rados user name, default + is admin More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it' + type: string + required: + - monitors + type: object + cinder: + description: 'Cinder represents a cinder volume attached and mounted + on kubelets host machine. More info: https://examples.k8s.io/mysql-cinder-pd/README.md' + properties: + fsType: + description: 'Filesystem type to mount. Must be a filesystem + type supported by the host operating system. Examples: "ext4", + "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + More info: https://examples.k8s.io/mysql-cinder-pd/README.md' + type: string + readOnly: + description: 'Optional: Defaults to false (read/write). ReadOnly + here will force the ReadOnly setting in VolumeMounts. More + info: https://examples.k8s.io/mysql-cinder-pd/README.md' + type: boolean + secretRef: + description: 'Optional: points to a secret object containing + parameters used to connect to OpenStack.' + properties: + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + type: object + volumeID: + description: 'volume id used to identify the volume in cinder. + More info: https://examples.k8s.io/mysql-cinder-pd/README.md' + type: string + required: + - volumeID + type: object + configMap: + description: ConfigMap represents a configMap that should populate + this volume + properties: + defaultMode: + description: 'Optional: mode bits to use on created files + by default. Must be a value between 0 and 0777. Defaults + to 0644. Directories within the path are not affected by + this setting. This might be in conflict with other options + that affect the file mode, like fsGroup, and the result + can be other mode bits set.' + format: int32 + type: integer + items: + description: If unspecified, each key-value pair in the Data + field of the referenced ConfigMap will be projected into + the volume as a file whose name is the key and content is + the value. If specified, the listed keys will be projected + into the specified paths, and unlisted keys will not be + present. If a key is specified which is not present in the + ConfigMap, the volume setup will error unless it is marked + optional. Paths must be relative and may not contain the + '..' path or start with '..'. + items: + description: Maps a string key to a path within a volume. + properties: + key: + description: The key to project. + type: string + mode: + description: 'Optional: mode bits to use on this file, + must be a value between 0 and 0777. If not specified, + the volume defaultMode will be used. This might be + in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode + bits set.' + format: int32 + type: integer + path: + description: The relative path of the file to map the + key to. May not be an absolute path. May not contain + the path element '..'. May not start with the string + '..'. + type: string + required: + - key + - path + type: object + type: array + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + optional: + description: Specify whether the ConfigMap or its keys must + be defined + type: boolean + type: object + csi: + description: CSI (Container Storage Interface) represents storage + that is handled by an external CSI driver (Alpha feature). + properties: + driver: + description: Driver is the name of the CSI driver that handles + this volume. Consult with your admin for the correct name + as registered in the cluster. + type: string + fsType: + description: Filesystem type to mount. Ex. "ext4", "xfs", + "ntfs". If not provided, the empty value is passed to the + associated CSI driver which will determine the default filesystem + to apply. + type: string + nodePublishSecretRef: + description: NodePublishSecretRef is a reference to the secret + object containing sensitive information to pass to the CSI + driver to complete the CSI NodePublishVolume and NodeUnpublishVolume + calls. This field is optional, and may be empty if no secret + is required. If the secret object contains more than one + secret, all secret references are passed. + properties: + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + type: object + readOnly: + description: Specifies a read-only configuration for the volume. + Defaults to false (read/write). + type: boolean + volumeAttributes: + additionalProperties: + type: string + description: VolumeAttributes stores driver-specific properties + that are passed to the CSI driver. Consult your driver's + documentation for supported values. + type: object + required: + - driver + type: object + downwardAPI: + description: DownwardAPI represents downward API about the pod + that should populate this volume + properties: + defaultMode: + description: 'Optional: mode bits to use on created files + by default. Must be a value between 0 and 0777. Defaults + to 0644. Directories within the path are not affected by + this setting. This might be in conflict with other options + that affect the file mode, like fsGroup, and the result + can be other mode bits set.' + format: int32 + type: integer + items: + description: Items is a list of downward API volume file + items: + description: DownwardAPIVolumeFile represents information + to create the file containing the pod field + properties: + fieldRef: + description: 'Required: Selects a field of the pod: + only annotations, labels, name and namespace are supported.' + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in the + specified API version. + type: string + required: + - fieldPath + type: object + mode: + description: 'Optional: mode bits to use on this file, + must be a value between 0 and 0777. If not specified, + the volume defaultMode will be used. This might be + in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode + bits set.' + format: int32 + type: integer + path: + description: 'Required: Path is the relative path name + of the file to be created. Must not be absolute or + contain the ''..'' path. Must be utf-8 encoded. The + first item of the relative path must not start with + ''..''' + type: string + resourceFieldRef: + description: 'Selects a resource of the container: only + resources limits and requests (limits.cpu, limits.memory, + requests.cpu and requests.memory) are currently supported.' + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + description: Specifies the output format of the + exposed resources, defaults to "1" + type: string + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + required: + - path + type: object + type: array + type: object + emptyDir: + description: 'EmptyDir represents a temporary directory that shares + a pod''s lifetime. More info: https://kubernetes.io/docs/concepts/storage/volumes#emptydir' + properties: + medium: + description: 'What type of storage medium should back this + directory. The default is "" which means to use the node''s + default medium. Must be an empty string (default) or Memory. + More info: https://kubernetes.io/docs/concepts/storage/volumes#emptydir' + type: string + sizeLimit: + description: 'Total amount of local storage required for this + EmptyDir volume. The size limit is also applicable for memory + medium. The maximum usage on memory medium EmptyDir would + be the minimum value between the SizeLimit specified here + and the sum of memory limits of all containers in a pod. + The default is nil which means that the limit is undefined. + More info: http://kubernetes.io/docs/user-guide/volumes#emptydir' + type: string + type: object + fc: + description: FC represents a Fibre Channel resource that is attached + to a kubelet's host machine and then exposed to the pod. + properties: + fsType: + description: 'Filesystem type to mount. Must be a filesystem + type supported by the host operating system. Ex. "ext4", + "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + TODO: how do we prevent errors in the filesystem from compromising + the machine' + type: string + lun: + description: 'Optional: FC target lun number' + format: int32 + type: integer + readOnly: + description: 'Optional: Defaults to false (read/write). ReadOnly + here will force the ReadOnly setting in VolumeMounts.' + type: boolean + targetWWNs: + description: 'Optional: FC target worldwide names (WWNs)' + items: + type: string + type: array + wwids: + description: 'Optional: FC volume world wide identifiers (wwids) + Either wwids or combination of targetWWNs and lun must be + set, but not both simultaneously.' + items: + type: string + type: array + type: object + flexVolume: + description: FlexVolume represents a generic volume resource that + is provisioned/attached using an exec based plugin. + properties: + driver: + description: Driver is the name of the driver to use for this + volume. + type: string + fsType: + description: Filesystem type to mount. Must be a filesystem + type supported by the host operating system. Ex. "ext4", + "xfs", "ntfs". The default filesystem depends on FlexVolume + script. + type: string + options: + additionalProperties: + type: string + description: 'Optional: Extra command options if any.' + type: object + readOnly: + description: 'Optional: Defaults to false (read/write). ReadOnly + here will force the ReadOnly setting in VolumeMounts.' + type: boolean + secretRef: + description: 'Optional: SecretRef is reference to the secret + object containing sensitive information to pass to the plugin + scripts. This may be empty if no secret object is specified. + If the secret object contains more than one secret, all + secrets are passed to the plugin scripts.' + properties: + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + type: object + required: + - driver + type: object + flocker: + description: Flocker represents a Flocker volume attached to a + kubelet's host machine. This depends on the Flocker control + service being running + properties: + datasetName: + description: Name of the dataset stored as metadata -> name + on the dataset for Flocker should be considered as deprecated + type: string + datasetUUID: + description: UUID of the dataset. This is unique identifier + of a Flocker dataset + type: string + type: object + gcePersistentDisk: + description: 'GCEPersistentDisk represents a GCE Disk resource + that is attached to a kubelet''s host machine and then exposed + to the pod. More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk' + properties: + fsType: + description: 'Filesystem type of the volume that you want + to mount. Tip: Ensure that the filesystem type is supported + by the host operating system. Examples: "ext4", "xfs", "ntfs". + Implicitly inferred to be "ext4" if unspecified. More info: + https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk + TODO: how do we prevent errors in the filesystem from compromising + the machine' + type: string + partition: + description: 'The partition in the volume that you want to + mount. If omitted, the default is to mount by volume name. + Examples: For volume /dev/sda1, you specify the partition + as "1". Similarly, the volume partition for /dev/sda is + "0" (or you can leave the property empty). More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk' + format: int32 + type: integer + pdName: + description: 'Unique name of the PD resource in GCE. Used + to identify the disk in GCE. More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk' + type: string + readOnly: + description: 'ReadOnly here will force the ReadOnly setting + in VolumeMounts. Defaults to false. More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk' + type: boolean + required: + - pdName + type: object + gitRepo: + description: 'GitRepo represents a git repository at a particular + revision. DEPRECATED: GitRepo is deprecated. To provision a + container with a git repo, mount an EmptyDir into an InitContainer + that clones the repo using git, then mount the EmptyDir into + the Pod''s container.' + properties: + directory: + description: Target directory name. Must not contain or start + with '..'. If '.' is supplied, the volume directory will + be the git repository. Otherwise, if specified, the volume + will contain the git repository in the subdirectory with + the given name. + type: string + repository: + description: Repository URL + type: string + revision: + description: Commit hash for the specified revision. + type: string + required: + - repository + type: object + glusterfs: + description: 'Glusterfs represents a Glusterfs mount on the host + that shares a pod''s lifetime. More info: https://examples.k8s.io/volumes/glusterfs/README.md' + properties: + endpoints: + description: 'EndpointsName is the endpoint name that details + Glusterfs topology. More info: https://examples.k8s.io/volumes/glusterfs/README.md#create-a-pod' + type: string + path: + description: 'Path is the Glusterfs volume path. More info: + https://examples.k8s.io/volumes/glusterfs/README.md#create-a-pod' + type: string + readOnly: + description: 'ReadOnly here will force the Glusterfs volume + to be mounted with read-only permissions. Defaults to false. + More info: https://examples.k8s.io/volumes/glusterfs/README.md#create-a-pod' + type: boolean + required: + - endpoints + - path + type: object + hostPath: + description: 'HostPath represents a pre-existing file or directory + on the host machine that is directly exposed to the container. + This is generally used for system agents or other privileged + things that are allowed to see the host machine. Most containers + will NOT need this. More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath + --- TODO(jonesdl) We need to restrict who can use host directory + mounts and who can/can not mount host directories as read/write.' + properties: + path: + description: 'Path of the directory on the host. If the path + is a symlink, it will follow the link to the real path. + More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath' + type: string + type: + description: 'Type for HostPath Volume Defaults to "" More + info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath' + type: string + required: + - path + type: object + iscsi: + description: 'ISCSI represents an ISCSI Disk resource that is + attached to a kubelet''s host machine and then exposed to the + pod. More info: https://examples.k8s.io/volumes/iscsi/README.md' + properties: + chapAuthDiscovery: + description: whether support iSCSI Discovery CHAP authentication + type: boolean + chapAuthSession: + description: whether support iSCSI Session CHAP authentication + type: boolean + fsType: + description: 'Filesystem type of the volume that you want + to mount. Tip: Ensure that the filesystem type is supported + by the host operating system. Examples: "ext4", "xfs", "ntfs". + Implicitly inferred to be "ext4" if unspecified. More info: + https://kubernetes.io/docs/concepts/storage/volumes#iscsi + TODO: how do we prevent errors in the filesystem from compromising + the machine' + type: string + initiatorName: + description: Custom iSCSI Initiator Name. If initiatorName + is specified with iscsiInterface simultaneously, new iSCSI + interface : will be created + for the connection. + type: string + iqn: + description: Target iSCSI Qualified Name. + type: string + iscsiInterface: + description: iSCSI Interface Name that uses an iSCSI transport. + Defaults to 'default' (tcp). + type: string + lun: + description: iSCSI Target Lun number. + format: int32 + type: integer + portals: + description: iSCSI Target Portal List. The portal is either + an IP or ip_addr:port if the port is other than default + (typically TCP ports 860 and 3260). + items: + type: string + type: array + readOnly: + description: ReadOnly here will force the ReadOnly setting + in VolumeMounts. Defaults to false. + type: boolean + secretRef: + description: CHAP Secret for iSCSI target and initiator authentication + properties: + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + type: object + targetPortal: + description: iSCSI Target Portal. The Portal is either an + IP or ip_addr:port if the port is other than default (typically + TCP ports 860 and 3260). + type: string + required: + - iqn + - lun + - targetPortal + type: object + name: + description: 'Volume''s name. Must be a DNS_LABEL and unique within + the pod. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + nfs: + description: 'NFS represents an NFS mount on the host that shares + a pod''s lifetime More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs' + properties: + path: + description: 'Path that is exported by the NFS server. More + info: https://kubernetes.io/docs/concepts/storage/volumes#nfs' + type: string + readOnly: + description: 'ReadOnly here will force the NFS export to be + mounted with read-only permissions. Defaults to false. More + info: https://kubernetes.io/docs/concepts/storage/volumes#nfs' + type: boolean + server: + description: 'Server is the hostname or IP address of the + NFS server. More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs' + type: string + required: + - path + - server + type: object + persistentVolumeClaim: + description: 'PersistentVolumeClaimVolumeSource represents a reference + to a PersistentVolumeClaim in the same namespace. More info: + https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims' + properties: + claimName: + description: 'ClaimName is the name of a PersistentVolumeClaim + in the same namespace as the pod using this volume. More + info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims' + type: string + readOnly: + description: Will force the ReadOnly setting in VolumeMounts. + Default false. + type: boolean + required: + - claimName + type: object + photonPersistentDisk: + description: PhotonPersistentDisk represents a PhotonController + persistent disk attached and mounted on kubelets host machine + properties: + fsType: + description: Filesystem type to mount. Must be a filesystem + type supported by the host operating system. Ex. "ext4", + "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + type: string + pdID: + description: ID that identifies Photon Controller persistent + disk + type: string + required: + - pdID + type: object + portworxVolume: + description: PortworxVolume represents a portworx volume attached + and mounted on kubelets host machine + properties: + fsType: + description: FSType represents the filesystem type to mount + Must be a filesystem type supported by the host operating + system. Ex. "ext4", "xfs". Implicitly inferred to be "ext4" + if unspecified. + type: string + readOnly: + description: Defaults to false (read/write). ReadOnly here + will force the ReadOnly setting in VolumeMounts. + type: boolean + volumeID: + description: VolumeID uniquely identifies a Portworx volume + type: string + required: + - volumeID + type: object + projected: + description: Items for all in one resources secrets, configmaps, + and downward API + properties: + defaultMode: + description: Mode bits to use on created files by default. + Must be a value between 0 and 0777. Directories within the + path are not affected by this setting. This might be in + conflict with other options that affect the file mode, like + fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + sources: + description: list of volume projections + items: + description: Projection that may be projected along with + other supported volume types + properties: + configMap: + description: information about the configMap data to + project + properties: + items: + description: If unspecified, each key-value pair + in the Data field of the referenced ConfigMap + will be projected into the volume as a file whose + name is the key and content is the value. If specified, + the listed keys will be projected into the specified + paths, and unlisted keys will not be present. + If a key is specified which is not present in + the ConfigMap, the volume setup will error unless + it is marked optional. Paths must be relative + and may not contain the '..' path or start with + '..'. + items: + description: Maps a string key to a path within + a volume. + properties: + key: + description: The key to project. + type: string + mode: + description: 'Optional: mode bits to use on + this file, must be a value between 0 and + 0777. If not specified, the volume defaultMode + will be used. This might be in conflict + with other options that affect the file + mode, like fsGroup, and the result can be + other mode bits set.' + format: int32 + type: integer + path: + description: The relative path of the file + to map the key to. May not be an absolute + path. May not contain the path element '..'. + May not start with the string '..'. + type: string + required: + - key + - path + type: object + type: array + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, + uid?' + type: string + optional: + description: Specify whether the ConfigMap or its + keys must be defined + type: boolean + type: object + downwardAPI: + description: information about the downwardAPI data + to project + properties: + items: + description: Items is a list of DownwardAPIVolume + file + items: + description: DownwardAPIVolumeFile represents + information to create the file containing the + pod field + properties: + fieldRef: + description: 'Required: Selects a field of + the pod: only annotations, labels, name + and namespace are supported.' + properties: + apiVersion: + description: Version of the schema the + FieldPath is written in terms of, defaults + to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + mode: + description: 'Optional: mode bits to use on + this file, must be a value between 0 and + 0777. If not specified, the volume defaultMode + will be used. This might be in conflict + with other options that affect the file + mode, like fsGroup, and the result can be + other mode bits set.' + format: int32 + type: integer + path: + description: 'Required: Path is the relative + path name of the file to be created. Must + not be absolute or contain the ''..'' path. + Must be utf-8 encoded. The first item of + the relative path must not start with ''..''' + type: string + resourceFieldRef: + description: 'Selects a resource of the container: + only resources limits and requests (limits.cpu, + limits.memory, requests.cpu and requests.memory) + are currently supported.' + properties: + containerName: + description: 'Container name: required + for volumes, optional for env vars' + type: string + divisor: + description: Specifies the output format + of the exposed resources, defaults to + "1" + type: string + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + required: + - path + type: object + type: array + type: object + secret: + description: information about the secret data to project + properties: + items: + description: If unspecified, each key-value pair + in the Data field of the referenced Secret will + be projected into the volume as a file whose name + is the key and content is the value. If specified, + the listed keys will be projected into the specified + paths, and unlisted keys will not be present. + If a key is specified which is not present in + the Secret, the volume setup will error unless + it is marked optional. Paths must be relative + and may not contain the '..' path or start with + '..'. + items: + description: Maps a string key to a path within + a volume. + properties: + key: + description: The key to project. + type: string + mode: + description: 'Optional: mode bits to use on + this file, must be a value between 0 and + 0777. If not specified, the volume defaultMode + will be used. This might be in conflict + with other options that affect the file + mode, like fsGroup, and the result can be + other mode bits set.' + format: int32 + type: integer + path: + description: The relative path of the file + to map the key to. May not be an absolute + path. May not contain the path element '..'. + May not start with the string '..'. + type: string + required: + - key + - path + type: object + type: array + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, + uid?' + type: string + optional: + description: Specify whether the Secret or its key + must be defined + type: boolean + type: object + serviceAccountToken: + description: information about the serviceAccountToken + data to project + properties: + audience: + description: Audience is the intended audience of + the token. A recipient of a token must identify + itself with an identifier specified in the audience + of the token, and otherwise should reject the + token. The audience defaults to the identifier + of the apiserver. + type: string + expirationSeconds: + description: ExpirationSeconds is the requested + duration of validity of the service account token. + As the token approaches expiration, the kubelet + volume plugin will proactively rotate the service + account token. The kubelet will start trying to + rotate the token if the token is older than 80 + percent of its time to live or if the token is + older than 24 hours.Defaults to 1 hour and must + be at least 10 minutes. + format: int64 + type: integer + path: + description: Path is the path relative to the mount + point of the file to project the token into. + type: string + required: + - path + type: object + type: object + type: array + required: + - sources + type: object + quobyte: + description: Quobyte represents a Quobyte mount on the host that + shares a pod's lifetime + properties: + group: + description: Group to map volume access to Default is no group + type: string + readOnly: + description: ReadOnly here will force the Quobyte volume to + be mounted with read-only permissions. Defaults to false. + type: boolean + registry: + description: Registry represents a single or multiple Quobyte + Registry services specified as a string as host:port pair + (multiple entries are separated with commas) which acts + as the central registry for volumes + type: string + tenant: + description: Tenant owning the given Quobyte volume in the + Backend Used with dynamically provisioned Quobyte volumes, + value is set by the plugin + type: string + user: + description: User to map volume access to Defaults to serivceaccount + user + type: string + volume: + description: Volume is a string that references an already + created Quobyte volume by name. + type: string + required: + - registry + - volume + type: object + rbd: + description: 'RBD represents a Rados Block Device mount on the + host that shares a pod''s lifetime. More info: https://examples.k8s.io/volumes/rbd/README.md' + properties: + fsType: + description: 'Filesystem type of the volume that you want + to mount. Tip: Ensure that the filesystem type is supported + by the host operating system. Examples: "ext4", "xfs", "ntfs". + Implicitly inferred to be "ext4" if unspecified. More info: + https://kubernetes.io/docs/concepts/storage/volumes#rbd + TODO: how do we prevent errors in the filesystem from compromising + the machine' + type: string + image: + description: 'The rados image name. More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it' + type: string + keyring: + description: 'Keyring is the path to key ring for RBDUser. + Default is /etc/ceph/keyring. More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it' + type: string + monitors: + description: 'A collection of Ceph monitors. More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it' + items: + type: string + type: array + pool: + description: 'The rados pool name. Default is rbd. More info: + https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it' + type: string + readOnly: + description: 'ReadOnly here will force the ReadOnly setting + in VolumeMounts. Defaults to false. More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it' + type: boolean + secretRef: + description: 'SecretRef is name of the authentication secret + for RBDUser. If provided overrides keyring. Default is nil. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it' + properties: + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + type: object + user: + description: 'The rados user name. Default is admin. More + info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it' + type: string + required: + - image + - monitors + type: object + scaleIO: + description: ScaleIO represents a ScaleIO persistent volume attached + and mounted on Kubernetes nodes. + properties: + fsType: + description: Filesystem type to mount. Must be a filesystem + type supported by the host operating system. Ex. "ext4", + "xfs", "ntfs". Default is "xfs". + type: string + gateway: + description: The host address of the ScaleIO API Gateway. + type: string + protectionDomain: + description: The name of the ScaleIO Protection Domain for + the configured storage. + type: string + readOnly: + description: Defaults to false (read/write). ReadOnly here + will force the ReadOnly setting in VolumeMounts. + type: boolean + secretRef: + description: SecretRef references to the secret for ScaleIO + user and other sensitive information. If this is not provided, + Login operation will fail. + properties: + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + type: object + sslEnabled: + description: Flag to enable/disable SSL communication with + Gateway, default false + type: boolean + storageMode: + description: Indicates whether the storage for a volume should + be ThickProvisioned or ThinProvisioned. Default is ThinProvisioned. + type: string + storagePool: + description: The ScaleIO Storage Pool associated with the + protection domain. + type: string + system: + description: The name of the storage system as configured + in ScaleIO. + type: string + volumeName: + description: The name of a volume already created in the ScaleIO + system that is associated with this volume source. + type: string + required: + - gateway + - secretRef + - system + type: object + secret: + description: 'Secret represents a secret that should populate + this volume. More info: https://kubernetes.io/docs/concepts/storage/volumes#secret' + properties: + defaultMode: + description: 'Optional: mode bits to use on created files + by default. Must be a value between 0 and 0777. Defaults + to 0644. Directories within the path are not affected by + this setting. This might be in conflict with other options + that affect the file mode, like fsGroup, and the result + can be other mode bits set.' + format: int32 + type: integer + items: + description: If unspecified, each key-value pair in the Data + field of the referenced Secret will be projected into the + volume as a file whose name is the key and content is the + value. If specified, the listed keys will be projected into + the specified paths, and unlisted keys will not be present. + If a key is specified which is not present in the Secret, + the volume setup will error unless it is marked optional. + Paths must be relative and may not contain the '..' path + or start with '..'. + items: + description: Maps a string key to a path within a volume. + properties: + key: + description: The key to project. + type: string + mode: + description: 'Optional: mode bits to use on this file, + must be a value between 0 and 0777. If not specified, + the volume defaultMode will be used. This might be + in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode + bits set.' + format: int32 + type: integer + path: + description: The relative path of the file to map the + key to. May not be an absolute path. May not contain + the path element '..'. May not start with the string + '..'. + type: string + required: + - key + - path + type: object + type: array + optional: + description: Specify whether the Secret or its keys must be + defined + type: boolean + secretName: + description: 'Name of the secret in the pod''s namespace to + use. More info: https://kubernetes.io/docs/concepts/storage/volumes#secret' + type: string + type: object + storageos: + description: StorageOS represents a StorageOS volume attached + and mounted on Kubernetes nodes. + properties: + fsType: + description: Filesystem type to mount. Must be a filesystem + type supported by the host operating system. Ex. "ext4", + "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + type: string + readOnly: + description: Defaults to false (read/write). ReadOnly here + will force the ReadOnly setting in VolumeMounts. + type: boolean + secretRef: + description: SecretRef specifies the secret to use for obtaining + the StorageOS API credentials. If not specified, default + values will be attempted. + properties: + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + type: object + volumeName: + description: VolumeName is the human-readable name of the + StorageOS volume. Volume names are only unique within a + namespace. + type: string + volumeNamespace: + description: VolumeNamespace specifies the scope of the volume + within StorageOS. If no namespace is specified then the + Pod's namespace will be used. This allows the Kubernetes + name scoping to be mirrored within StorageOS for tighter + integration. Set VolumeName to any name to override the + default behaviour. Set to "default" if you are not using + namespaces within StorageOS. Namespaces that do not pre-exist + within StorageOS will be created. + type: string + type: object + vsphereVolume: + description: VsphereVolume represents a vSphere volume attached + and mounted on kubelets host machine + properties: + fsType: + description: Filesystem type to mount. Must be a filesystem + type supported by the host operating system. Ex. "ext4", + "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + type: string + storagePolicyID: + description: Storage Policy Based Management (SPBM) profile + ID associated with the StoragePolicyName. + type: string + storagePolicyName: + description: Storage Policy Based Management (SPBM) profile + name. + type: string + volumePath: + description: Path that identifies vSphere volume vmdk + type: string + required: + - volumePath + type: object + required: + - name + type: object + type: array + type: object + status: + description: SearchHeadClusterStatus defines the observed state of a Splunk + Enterprise search head cluster + properties: + adminPasswordChangedSecrets: + additionalProperties: + type: boolean + description: Holds secrets whose admin password has changed + type: object + adminSecretChangedFlag: + description: Indicates when the admin password has been changed for + a peer + items: + type: boolean + type: array + captain: + description: name or label of the search head captain + type: string + captainReady: + description: true if the search head cluster's captain is ready to service + requests + type: boolean + deployerPhase: + description: current phase of the deployer + enum: + - Pending + - Ready + - Updating + - ScalingUp + - ScalingDown + - Terminating + - Error + type: string + initialized: + description: true if the search head cluster has finished initialization + type: boolean + maintenanceMode: + description: true if the search head cluster is in maintenance mode + type: boolean + members: + description: status of each search head cluster member + items: + description: SearchHeadClusterMemberStatus is used to track the status + of each search head cluster member + properties: + active_historical_search_count: + description: Number of currently running historical searches. + type: integer + active_realtime_search_count: + description: Number of currently running realtime searches. + type: integer + adhoc_searchhead: + description: Flag that indicates if this member can run scheduled + searches. + type: boolean + is_registered: + description: Indicates if this member is registered with the searchhead + cluster captain. + type: boolean + name: + description: Name of the search head cluster member + type: string + status: + description: Indicates the status of the member. + type: string + type: object + type: array + minPeersJoined: + description: true if the minimum number of search head cluster members + have joined + type: boolean + namespace_scoped_secret_resource_version: + description: Indicates resource version of namespace scoped secret + type: string + phase: + description: current phase of the search head cluster + enum: + - Pending + - Ready + - Updating + - ScalingUp + - ScalingDown + - Terminating + - Error + type: string + readyReplicas: + description: current number of ready search head cluster members + format: int32 + type: integer + replicas: + description: desired number of search head cluster members + format: int32 + type: integer + selector: + description: selector for pods, used by HorizontalPodAutoscaler + type: string + shcSecretChangedFlag: + description: Indicates when the shc_secret has been changed for a peer + items: + type: boolean + type: array + type: object + type: object + version: v1beta1 + versions: + - name: v1beta1 + served: true + storage: true + - name: v1alpha3 + served: true + storage: false + - name: v1alpha2 + served: true + storage: false diff --git a/deploy/olm-catalog/splunk/0.2.2/enterprise.splunk.com_sparks_crd.yaml b/deploy/olm-catalog/splunk/0.2.2/enterprise.splunk.com_sparks_crd.yaml new file mode 100644 index 000000000..063d16f26 --- /dev/null +++ b/deploy/olm-catalog/splunk/0.2.2/enterprise.splunk.com_sparks_crd.yaml @@ -0,0 +1,996 @@ +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: sparks.enterprise.splunk.com +spec: + additionalPrinterColumns: + - JSONPath: .status.phase + description: Status of Spark workers + name: Phase + type: string + - JSONPath: .status.masterPhase + description: Status of Spark master + name: Master + type: string + - JSONPath: .status.replicas + description: Number of desired Spark workers + name: Desired + type: integer + - JSONPath: .status.readyReplicas + description: Current number of ready Spark workers + name: Ready + type: integer + - JSONPath: .metadata.creationTimestamp + description: Age of Spark cluster + name: Age + type: date + group: enterprise.splunk.com + names: + kind: Spark + listKind: SparkList + plural: sparks + singular: spark + scope: Namespaced + subresources: + scale: + labelSelectorPath: .status.selector + specReplicasPath: .spec.replicas + statusReplicasPath: .status.replicas + status: {} + validation: + openAPIV3Schema: + description: Spark is the Schema for a Spark cluster + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: SparkSpec defines the desired state of a Spark cluster + properties: + affinity: + description: Kubernetes Affinity rules that control how pods are assigned + to particular nodes. + properties: + nodeAffinity: + description: Describes node affinity scheduling rules for the pod. + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: The scheduler will prefer to schedule pods to nodes + that satisfy the affinity expressions specified by this field, + but it may choose a node that violates one or more of the + expressions. The node that is most preferred is the one with + the greatest sum of weights, i.e. for each node that meets + all of the scheduling requirements (resource request, requiredDuringScheduling + affinity expressions, etc.), compute a sum by iterating through + the elements of this field and adding "weight" to the sum + if the node matches the corresponding matchExpressions; the + node(s) with the highest sum are the most preferred. + items: + description: An empty preferred scheduling term matches all + objects with implicit weight 0 (i.e. it's a no-op). A null + preferred scheduling term matches no objects (i.e. is also + a no-op). + properties: + preference: + description: A node selector term, associated with the + corresponding weight. + properties: + matchExpressions: + description: A list of node selector requirements + by node's labels. + items: + description: A node selector requirement is a selector + that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: Represents a key's relationship + to a set of values. Valid operators are In, + NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: An array of string values. If the + operator is In or NotIn, the values array + must be non-empty. If the operator is Exists + or DoesNotExist, the values array must be + empty. If the operator is Gt or Lt, the values + array must have a single element, which will + be interpreted as an integer. This array is + replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchFields: + description: A list of node selector requirements + by node's fields. + items: + description: A node selector requirement is a selector + that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: Represents a key's relationship + to a set of values. Valid operators are In, + NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: An array of string values. If the + operator is In or NotIn, the values array + must be non-empty. If the operator is Exists + or DoesNotExist, the values array must be + empty. If the operator is Gt or Lt, the values + array must have a single element, which will + be interpreted as an integer. This array is + replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + type: object + weight: + description: Weight associated with matching the corresponding + nodeSelectorTerm, in the range 1-100. + format: int32 + type: integer + required: + - preference + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + description: If the affinity requirements specified by this + field are not met at scheduling time, the pod will not be + scheduled onto the node. If the affinity requirements specified + by this field cease to be met at some point during pod execution + (e.g. due to an update), the system may or may not try to + eventually evict the pod from its node. + properties: + nodeSelectorTerms: + description: Required. A list of node selector terms. The + terms are ORed. + items: + description: A null or empty node selector term matches + no objects. The requirements of them are ANDed. The + TopologySelectorTerm type implements a subset of the + NodeSelectorTerm. + properties: + matchExpressions: + description: A list of node selector requirements + by node's labels. + items: + description: A node selector requirement is a selector + that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: Represents a key's relationship + to a set of values. Valid operators are In, + NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: An array of string values. If the + operator is In or NotIn, the values array + must be non-empty. If the operator is Exists + or DoesNotExist, the values array must be + empty. If the operator is Gt or Lt, the values + array must have a single element, which will + be interpreted as an integer. This array is + replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchFields: + description: A list of node selector requirements + by node's fields. + items: + description: A node selector requirement is a selector + that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: Represents a key's relationship + to a set of values. Valid operators are In, + NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: An array of string values. If the + operator is In or NotIn, the values array + must be non-empty. If the operator is Exists + or DoesNotExist, the values array must be + empty. If the operator is Gt or Lt, the values + array must have a single element, which will + be interpreted as an integer. This array is + replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + type: object + type: array + required: + - nodeSelectorTerms + type: object + type: object + podAffinity: + description: Describes pod affinity scheduling rules (e.g. co-locate + this pod in the same node, zone, etc. as some other pod(s)). + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: The scheduler will prefer to schedule pods to nodes + that satisfy the affinity expressions specified by this field, + but it may choose a node that violates one or more of the + expressions. The node that is most preferred is the one with + the greatest sum of weights, i.e. for each node that meets + all of the scheduling requirements (resource request, requiredDuringScheduling + affinity expressions, etc.), compute a sum by iterating through + the elements of this field and adding "weight" to the sum + if the node has pods which matches the corresponding podAffinityTerm; + the node(s) with the highest sum are the most preferred. + items: + description: The weights of all of the matched WeightedPodAffinityTerm + fields are added per-node to find the most preferred node(s) + properties: + podAffinityTerm: + description: Required. A pod affinity term, associated + with the corresponding weight. + properties: + labelSelector: + description: A label query over a set of resources, + in this case pods. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: A label selector requirement is + a selector that contains values, a key, and + an operator that relates the key and values. + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: operator represents a key's + relationship to a set of values. Valid + operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: values is an array of string + values. If the operator is In or NotIn, + the values array must be non-empty. If + the operator is Exists or DoesNotExist, + the values array must be empty. This array + is replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, + whose key field is "key", the operator is "In", + and the values array contains only "value". + The requirements are ANDed. + type: object + type: object + namespaces: + description: namespaces specifies which namespaces + the labelSelector applies to (matches against); + null or empty list means "this pod's namespace" + items: + type: string + type: array + topologyKey: + description: This pod should be co-located (affinity) + or not co-located (anti-affinity) with the pods + matching the labelSelector in the specified namespaces, + where co-located is defined as running on a node + whose value of the label with key topologyKey matches + that of any node on which any of the selected pods + is running. Empty topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + weight: + description: weight associated with matching the corresponding + podAffinityTerm, in the range 1-100. + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + description: If the affinity requirements specified by this + field are not met at scheduling time, the pod will not be + scheduled onto the node. If the affinity requirements specified + by this field cease to be met at some point during pod execution + (e.g. due to a pod label update), the system may or may not + try to eventually evict the pod from its node. When there + are multiple elements, the lists of nodes corresponding to + each podAffinityTerm are intersected, i.e. all terms must + be satisfied. + items: + description: Defines a set of pods (namely those matching + the labelSelector relative to the given namespace(s)) that + this pod should be co-located (affinity) or not co-located + (anti-affinity) with, where co-located is defined as running + on a node whose value of the label with key + matches that of any node on which a pod of the set of pods + is running + properties: + labelSelector: + description: A label query over a set of resources, in + this case pods. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: A label selector requirement is a selector + that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: operator represents a key's relationship + to a set of values. Valid operators are In, + NotIn, Exists and DoesNotExist. + type: string + values: + description: values is an array of string values. + If the operator is In or NotIn, the values + array must be non-empty. If the operator is + Exists or DoesNotExist, the values array must + be empty. This array is replaced during a + strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} pairs. + A single {key,value} in the matchLabels map is equivalent + to an element of matchExpressions, whose key field + is "key", the operator is "In", and the values array + contains only "value". The requirements are ANDed. + type: object + type: object + namespaces: + description: namespaces specifies which namespaces the + labelSelector applies to (matches against); null or + empty list means "this pod's namespace" + items: + type: string + type: array + topologyKey: + description: This pod should be co-located (affinity) + or not co-located (anti-affinity) with the pods matching + the labelSelector in the specified namespaces, where + co-located is defined as running on a node whose value + of the label with key topologyKey matches that of any + node on which any of the selected pods is running. Empty + topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + type: array + type: object + podAntiAffinity: + description: Describes pod anti-affinity scheduling rules (e.g. + avoid putting this pod in the same node, zone, etc. as some other + pod(s)). + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: The scheduler will prefer to schedule pods to nodes + that satisfy the anti-affinity expressions specified by this + field, but it may choose a node that violates one or more + of the expressions. The node that is most preferred is the + one with the greatest sum of weights, i.e. for each node that + meets all of the scheduling requirements (resource request, + requiredDuringScheduling anti-affinity expressions, etc.), + compute a sum by iterating through the elements of this field + and adding "weight" to the sum if the node has pods which + matches the corresponding podAffinityTerm; the node(s) with + the highest sum are the most preferred. + items: + description: The weights of all of the matched WeightedPodAffinityTerm + fields are added per-node to find the most preferred node(s) + properties: + podAffinityTerm: + description: Required. A pod affinity term, associated + with the corresponding weight. + properties: + labelSelector: + description: A label query over a set of resources, + in this case pods. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: A label selector requirement is + a selector that contains values, a key, and + an operator that relates the key and values. + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: operator represents a key's + relationship to a set of values. Valid + operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: values is an array of string + values. If the operator is In or NotIn, + the values array must be non-empty. If + the operator is Exists or DoesNotExist, + the values array must be empty. This array + is replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, + whose key field is "key", the operator is "In", + and the values array contains only "value". + The requirements are ANDed. + type: object + type: object + namespaces: + description: namespaces specifies which namespaces + the labelSelector applies to (matches against); + null or empty list means "this pod's namespace" + items: + type: string + type: array + topologyKey: + description: This pod should be co-located (affinity) + or not co-located (anti-affinity) with the pods + matching the labelSelector in the specified namespaces, + where co-located is defined as running on a node + whose value of the label with key topologyKey matches + that of any node on which any of the selected pods + is running. Empty topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + weight: + description: weight associated with matching the corresponding + podAffinityTerm, in the range 1-100. + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + description: If the anti-affinity requirements specified by + this field are not met at scheduling time, the pod will not + be scheduled onto the node. If the anti-affinity requirements + specified by this field cease to be met at some point during + pod execution (e.g. due to a pod label update), the system + may or may not try to eventually evict the pod from its node. + When there are multiple elements, the lists of nodes corresponding + to each podAffinityTerm are intersected, i.e. all terms must + be satisfied. + items: + description: Defines a set of pods (namely those matching + the labelSelector relative to the given namespace(s)) that + this pod should be co-located (affinity) or not co-located + (anti-affinity) with, where co-located is defined as running + on a node whose value of the label with key + matches that of any node on which a pod of the set of pods + is running + properties: + labelSelector: + description: A label query over a set of resources, in + this case pods. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: A label selector requirement is a selector + that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: operator represents a key's relationship + to a set of values. Valid operators are In, + NotIn, Exists and DoesNotExist. + type: string + values: + description: values is an array of string values. + If the operator is In or NotIn, the values + array must be non-empty. If the operator is + Exists or DoesNotExist, the values array must + be empty. This array is replaced during a + strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} pairs. + A single {key,value} in the matchLabels map is equivalent + to an element of matchExpressions, whose key field + is "key", the operator is "In", and the values array + contains only "value". The requirements are ANDed. + type: object + type: object + namespaces: + description: namespaces specifies which namespaces the + labelSelector applies to (matches against); null or + empty list means "this pod's namespace" + items: + type: string + type: array + topologyKey: + description: This pod should be co-located (affinity) + or not co-located (anti-affinity) with the pods matching + the labelSelector in the specified namespaces, where + co-located is defined as running on a node whose value + of the label with key topologyKey matches that of any + node on which any of the selected pods is running. Empty + topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + type: array + type: object + type: object + image: + description: Image to use for Splunk pod containers (overrides RELATED_IMAGE_SPLUNK_ENTERPRISE + environment variables) + type: string + imagePullPolicy: + description: 'Sets pull policy for all images (either “Always” or the + default: “IfNotPresent”)' + enum: + - Always + - IfNotPresent + type: string + replicas: + description: Number of spark worker pods + format: int32 + type: integer + resources: + description: resource requirements for the pod containers + properties: + limits: + additionalProperties: + type: string + description: 'Limits describes the maximum amount of compute resources + allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/' + type: object + requests: + additionalProperties: + type: string + description: 'Requests describes the minimum amount of compute resources + required. If Requests is omitted for a container, it defaults + to Limits if that is explicitly specified, otherwise to an implementation-defined + value. More info: https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/' + type: object + type: object + schedulerName: + description: Name of Scheduler to use for pod placement (defaults to + “default-scheduler”) + type: string + serviceTemplate: + description: ServiceTemplate is a template used to create Kubernetes + services + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the + latest internal value, and may reject unrecognized values. More + info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource + this object represents. Servers may infer this from the endpoint + the client submits requests to. Cannot be updated. In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + description: 'Standard object''s metadata. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata' + type: object + spec: + description: Spec defines the behavior of a service. https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status + properties: + clusterIP: + description: 'clusterIP is the IP address of the service and + is usually assigned randomly by the master. If an address + is specified manually and is not in use by others, it will + be allocated to the service; otherwise, creation of the service + will fail. This field can not be changed through updates. + Valid values are "None", empty string (""), or a valid IP + address. "None" can be specified for headless services when + proxying is not required. Only applies to types ClusterIP, + NodePort, and LoadBalancer. Ignored if type is ExternalName. + More info: https://kubernetes.io/docs/concepts/services-networking/service/#virtual-ips-and-service-proxies' + type: string + externalIPs: + description: externalIPs is a list of IP addresses for which + nodes in the cluster will also accept traffic for this service. These + IPs are not managed by Kubernetes. The user is responsible + for ensuring that traffic arrives at a node with this IP. A + common example is external load-balancers that are not part + of the Kubernetes system. + items: + type: string + type: array + externalName: + description: externalName is the external reference that kubedns + or equivalent will return as a CNAME record for this service. + No proxying will be involved. Must be a valid RFC-1123 hostname + (https://tools.ietf.org/html/rfc1123) and requires Type to + be ExternalName. + type: string + externalTrafficPolicy: + description: externalTrafficPolicy denotes if this Service desires + to route external traffic to node-local or cluster-wide endpoints. + "Local" preserves the client source IP and avoids a second + hop for LoadBalancer and Nodeport type services, but risks + potentially imbalanced traffic spreading. "Cluster" obscures + the client source IP and may cause a second hop to another + node, but should have good overall load-spreading. + type: string + healthCheckNodePort: + description: healthCheckNodePort specifies the healthcheck nodePort + for the service. If not specified, HealthCheckNodePort is + created by the service api backend with the allocated nodePort. + Will use user-specified nodePort value if specified by the + client. Only effects when Type is set to LoadBalancer and + ExternalTrafficPolicy is set to Local. + format: int32 + type: integer + ipFamily: + description: ipFamily specifies whether this Service has a preference + for a particular IP family (e.g. IPv4 vs. IPv6). If a specific + IP family is requested, the clusterIP field will be allocated + from that family, if it is available in the cluster. If no + IP family is requested, the cluster's primary IP family will + be used. Other IP fields (loadBalancerIP, loadBalancerSourceRanges, + externalIPs) and controllers which allocate external load-balancers + should use the same IP family. Endpoints for this Service + will be of this family. This field is immutable after creation. + Assigning a ServiceIPFamily not available in the cluster (e.g. + IPv6 in IPv4 only cluster) is an error condition and will + fail during clusterIP assignment. + type: string + loadBalancerIP: + description: 'Only applies to Service Type: LoadBalancer LoadBalancer + will get created with the IP specified in this field. This + feature depends on whether the underlying cloud-provider supports + specifying the loadBalancerIP when a load balancer is created. + This field will be ignored if the cloud-provider does not + support the feature.' + type: string + loadBalancerSourceRanges: + description: 'If specified and supported by the platform, this + will restrict traffic through the cloud-provider load-balancer + will be restricted to the specified client IPs. This field + will be ignored if the cloud-provider does not support the + feature." More info: https://kubernetes.io/docs/tasks/access-application-cluster/configure-cloud-provider-firewall/' + items: + type: string + type: array + ports: + description: 'The list of ports that are exposed by this service. + More info: https://kubernetes.io/docs/concepts/services-networking/service/#virtual-ips-and-service-proxies' + items: + description: ServicePort contains information on service's + port. + properties: + name: + description: The name of this port within the service. + This must be a DNS_LABEL. All ports within a ServiceSpec + must have unique names. When considering the endpoints + for a Service, this must match the 'name' field in the + EndpointPort. Optional if only one ServicePort is defined + on this service. + type: string + nodePort: + description: 'The port on each node on which this service + is exposed when type=NodePort or LoadBalancer. Usually + assigned by the system. If specified, it will be allocated + to the service if unused or else creation of the service + will fail. Default is to auto-allocate a port if the + ServiceType of this Service requires one. More info: + https://kubernetes.io/docs/concepts/services-networking/service/#type-nodeport' + format: int32 + type: integer + port: + description: The port that will be exposed by this service. + format: int32 + type: integer + protocol: + description: The IP protocol for this port. Supports "TCP", + "UDP", and "SCTP". Default is TCP. + type: string + targetPort: + anyOf: + - type: integer + - type: string + description: 'Number or name of the port to access on + the pods targeted by the service. Number must be in + the range 1 to 65535. Name must be an IANA_SVC_NAME. + If this is a string, it will be looked up as a named + port in the target Pod''s container ports. If this is + not specified, the value of the ''port'' field is used + (an identity map). This field is ignored for services + with clusterIP=None, and should be omitted or set equal + to the ''port'' field. More info: https://kubernetes.io/docs/concepts/services-networking/service/#defining-a-service' + x-kubernetes-int-or-string: true + required: + - port + type: object + type: array + publishNotReadyAddresses: + description: publishNotReadyAddresses, when set to true, indicates + that DNS implementations must publish the notReadyAddresses + of subsets for the Endpoints associated with the Service. + The default value is false. The primary use case for setting + this field is to use a StatefulSet's Headless Service to propagate + SRV records for its Pods without respect to their readiness + for purpose of peer discovery. + type: boolean + selector: + additionalProperties: + type: string + description: 'Route service traffic to pods with label keys + and values matching this selector. If empty or not present, + the service is assumed to have an external process managing + its endpoints, which Kubernetes will not modify. Only applies + to types ClusterIP, NodePort, and LoadBalancer. Ignored if + type is ExternalName. More info: https://kubernetes.io/docs/concepts/services-networking/service/' + type: object + sessionAffinity: + description: 'Supports "ClientIP" and "None". Used to maintain + session affinity. Enable client IP based session affinity. + Must be ClientIP or None. Defaults to None. More info: https://kubernetes.io/docs/concepts/services-networking/service/#virtual-ips-and-service-proxies' + type: string + sessionAffinityConfig: + description: sessionAffinityConfig contains the configurations + of session affinity. + properties: + clientIP: + description: clientIP contains the configurations of Client + IP based session affinity. + properties: + timeoutSeconds: + description: timeoutSeconds specifies the seconds of + ClientIP type session sticky time. The value must + be >0 && <=86400(for 1 day) if ServiceAffinity == + "ClientIP". Default value is 10800(for 3 hours). + format: int32 + type: integer + type: object + type: object + type: + description: 'type determines how the Service is exposed. Defaults + to ClusterIP. Valid options are ExternalName, ClusterIP, NodePort, + and LoadBalancer. "ExternalName" maps to the specified externalName. + "ClusterIP" allocates a cluster-internal IP address for load-balancing + to endpoints. Endpoints are determined by the selector or + if that is not specified, by manual construction of an Endpoints + object. If clusterIP is "None", no virtual IP is allocated + and the endpoints are published as a set of endpoints rather + than a stable IP. "NodePort" builds on ClusterIP and allocates + a port on every node which routes to the clusterIP. "LoadBalancer" + builds on NodePort and creates an external load-balancer (if + supported in the current cloud) which routes to the clusterIP. + More info: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types' + type: string + type: object + status: + description: 'Most recently observed status of the service. Populated + by the system. Read-only. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status' + properties: + loadBalancer: + description: LoadBalancer contains the current status of the + load-balancer, if one is present. + properties: + ingress: + description: Ingress is a list containing ingress points + for the load-balancer. Traffic intended for the service + should be sent to these ingress points. + items: + description: 'LoadBalancerIngress represents the status + of a load-balancer ingress point: traffic intended for + the service should be sent to an ingress point.' + properties: + hostname: + description: Hostname is set for load-balancer ingress + points that are DNS based (typically AWS load-balancers) + type: string + ip: + description: IP is set for load-balancer ingress points + that are IP based (typically GCE or OpenStack load-balancers) + type: string + type: object + type: array + type: object + type: object + type: object + tolerations: + description: Pod's tolerations for Kubernetes node's taint + items: + description: The pod this Toleration is attached to tolerates any + taint that matches the triple using the matching + operator . + properties: + effect: + description: Effect indicates the taint effect to match. Empty + means match all taint effects. When specified, allowed values + are NoSchedule, PreferNoSchedule and NoExecute. + type: string + key: + description: Key is the taint key that the toleration applies + to. Empty means match all taint keys. If the key is empty, operator + must be Exists; this combination means to match all values and + all keys. + type: string + operator: + description: Operator represents a key's relationship to the value. + Valid operators are Exists and Equal. Defaults to Equal. Exists + is equivalent to wildcard for value, so that a pod can tolerate + all taints of a particular category. + type: string + tolerationSeconds: + description: TolerationSeconds represents the period of time the + toleration (which must be of effect NoExecute, otherwise this + field is ignored) tolerates the taint. By default, it is not + set, which means tolerate the taint forever (do not evict). + Zero and negative values will be treated as 0 (evict immediately) + by the system. + format: int64 + type: integer + value: + description: Value is the taint value the toleration matches to. + If the operator is Exists, the value should be empty, otherwise + just a regular string. + type: string + type: object + type: array + type: object + status: + description: SparkStatus defines the observed state of a Spark cluster + properties: + masterPhase: + description: current phase of the spark master + enum: + - Pending + - Ready + - Updating + - ScalingUp + - ScalingDown + - Terminating + - Error + type: string + phase: + description: current phase of the spark workers + enum: + - Pending + - Ready + - Updating + - ScalingUp + - ScalingDown + - Terminating + - Error + type: string + readyReplicas: + description: current number of ready spark workers + format: int32 + type: integer + replicas: + description: number of desired spark workers + format: int32 + type: integer + selector: + description: selector for pods, used by HorizontalPodAutoscaler + type: string + type: object + type: object + version: v1beta1 + versions: + - name: v1beta1 + served: true + storage: true + - name: v1alpha3 + served: true + storage: false + - name: v1alpha2 + served: true + storage: false diff --git a/deploy/olm-catalog/splunk/0.2.2/enterprise.splunk.com_standalones_crd.yaml b/deploy/olm-catalog/splunk/0.2.2/enterprise.splunk.com_standalones_crd.yaml new file mode 100644 index 000000000..ec058bd40 --- /dev/null +++ b/deploy/olm-catalog/splunk/0.2.2/enterprise.splunk.com_standalones_crd.yaml @@ -0,0 +1,2544 @@ +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: standalones.enterprise.splunk.com +spec: + additionalPrinterColumns: + - JSONPath: .status.phase + description: Status of standalone instances + name: Phase + type: string + - JSONPath: .status.replicas + description: Number of desired standalone instances + name: Desired + type: integer + - JSONPath: .status.readyReplicas + description: Current number of ready standalone instances + name: Ready + type: integer + - JSONPath: .metadata.creationTimestamp + description: Age of standalone resource + name: Age + type: date + group: enterprise.splunk.com + names: + kind: Standalone + listKind: StandaloneList + plural: standalones + shortNames: + - stdaln + singular: standalone + scope: Namespaced + subresources: + scale: + labelSelectorPath: .status.selector + specReplicasPath: .spec.replicas + statusReplicasPath: .status.replicas + status: {} + validation: + openAPIV3Schema: + description: Standalone is the Schema for a Splunk Enterprise standalone instances. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: StandaloneSpec defines the desired state of a Splunk Enterprise + standalone instances. + properties: + Mock: + description: Mock to differentiate between UTs and actual reconcile + type: boolean + affinity: + description: Kubernetes Affinity rules that control how pods are assigned + to particular nodes. + properties: + nodeAffinity: + description: Describes node affinity scheduling rules for the pod. + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: The scheduler will prefer to schedule pods to nodes + that satisfy the affinity expressions specified by this field, + but it may choose a node that violates one or more of the + expressions. The node that is most preferred is the one with + the greatest sum of weights, i.e. for each node that meets + all of the scheduling requirements (resource request, requiredDuringScheduling + affinity expressions, etc.), compute a sum by iterating through + the elements of this field and adding "weight" to the sum + if the node matches the corresponding matchExpressions; the + node(s) with the highest sum are the most preferred. + items: + description: An empty preferred scheduling term matches all + objects with implicit weight 0 (i.e. it's a no-op). A null + preferred scheduling term matches no objects (i.e. is also + a no-op). + properties: + preference: + description: A node selector term, associated with the + corresponding weight. + properties: + matchExpressions: + description: A list of node selector requirements + by node's labels. + items: + description: A node selector requirement is a selector + that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: Represents a key's relationship + to a set of values. Valid operators are In, + NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: An array of string values. If the + operator is In or NotIn, the values array + must be non-empty. If the operator is Exists + or DoesNotExist, the values array must be + empty. If the operator is Gt or Lt, the values + array must have a single element, which will + be interpreted as an integer. This array is + replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchFields: + description: A list of node selector requirements + by node's fields. + items: + description: A node selector requirement is a selector + that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: Represents a key's relationship + to a set of values. Valid operators are In, + NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: An array of string values. If the + operator is In or NotIn, the values array + must be non-empty. If the operator is Exists + or DoesNotExist, the values array must be + empty. If the operator is Gt or Lt, the values + array must have a single element, which will + be interpreted as an integer. This array is + replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + type: object + weight: + description: Weight associated with matching the corresponding + nodeSelectorTerm, in the range 1-100. + format: int32 + type: integer + required: + - preference + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + description: If the affinity requirements specified by this + field are not met at scheduling time, the pod will not be + scheduled onto the node. If the affinity requirements specified + by this field cease to be met at some point during pod execution + (e.g. due to an update), the system may or may not try to + eventually evict the pod from its node. + properties: + nodeSelectorTerms: + description: Required. A list of node selector terms. The + terms are ORed. + items: + description: A null or empty node selector term matches + no objects. The requirements of them are ANDed. The + TopologySelectorTerm type implements a subset of the + NodeSelectorTerm. + properties: + matchExpressions: + description: A list of node selector requirements + by node's labels. + items: + description: A node selector requirement is a selector + that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: Represents a key's relationship + to a set of values. Valid operators are In, + NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: An array of string values. If the + operator is In or NotIn, the values array + must be non-empty. If the operator is Exists + or DoesNotExist, the values array must be + empty. If the operator is Gt or Lt, the values + array must have a single element, which will + be interpreted as an integer. This array is + replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchFields: + description: A list of node selector requirements + by node's fields. + items: + description: A node selector requirement is a selector + that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: The label key that the selector + applies to. + type: string + operator: + description: Represents a key's relationship + to a set of values. Valid operators are In, + NotIn, Exists, DoesNotExist. Gt, and Lt. + type: string + values: + description: An array of string values. If the + operator is In or NotIn, the values array + must be non-empty. If the operator is Exists + or DoesNotExist, the values array must be + empty. If the operator is Gt or Lt, the values + array must have a single element, which will + be interpreted as an integer. This array is + replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + type: object + type: array + required: + - nodeSelectorTerms + type: object + type: object + podAffinity: + description: Describes pod affinity scheduling rules (e.g. co-locate + this pod in the same node, zone, etc. as some other pod(s)). + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: The scheduler will prefer to schedule pods to nodes + that satisfy the affinity expressions specified by this field, + but it may choose a node that violates one or more of the + expressions. The node that is most preferred is the one with + the greatest sum of weights, i.e. for each node that meets + all of the scheduling requirements (resource request, requiredDuringScheduling + affinity expressions, etc.), compute a sum by iterating through + the elements of this field and adding "weight" to the sum + if the node has pods which matches the corresponding podAffinityTerm; + the node(s) with the highest sum are the most preferred. + items: + description: The weights of all of the matched WeightedPodAffinityTerm + fields are added per-node to find the most preferred node(s) + properties: + podAffinityTerm: + description: Required. A pod affinity term, associated + with the corresponding weight. + properties: + labelSelector: + description: A label query over a set of resources, + in this case pods. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: A label selector requirement is + a selector that contains values, a key, and + an operator that relates the key and values. + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: operator represents a key's + relationship to a set of values. Valid + operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: values is an array of string + values. If the operator is In or NotIn, + the values array must be non-empty. If + the operator is Exists or DoesNotExist, + the values array must be empty. This array + is replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, + whose key field is "key", the operator is "In", + and the values array contains only "value". + The requirements are ANDed. + type: object + type: object + namespaces: + description: namespaces specifies which namespaces + the labelSelector applies to (matches against); + null or empty list means "this pod's namespace" + items: + type: string + type: array + topologyKey: + description: This pod should be co-located (affinity) + or not co-located (anti-affinity) with the pods + matching the labelSelector in the specified namespaces, + where co-located is defined as running on a node + whose value of the label with key topologyKey matches + that of any node on which any of the selected pods + is running. Empty topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + weight: + description: weight associated with matching the corresponding + podAffinityTerm, in the range 1-100. + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + description: If the affinity requirements specified by this + field are not met at scheduling time, the pod will not be + scheduled onto the node. If the affinity requirements specified + by this field cease to be met at some point during pod execution + (e.g. due to a pod label update), the system may or may not + try to eventually evict the pod from its node. When there + are multiple elements, the lists of nodes corresponding to + each podAffinityTerm are intersected, i.e. all terms must + be satisfied. + items: + description: Defines a set of pods (namely those matching + the labelSelector relative to the given namespace(s)) that + this pod should be co-located (affinity) or not co-located + (anti-affinity) with, where co-located is defined as running + on a node whose value of the label with key + matches that of any node on which a pod of the set of pods + is running + properties: + labelSelector: + description: A label query over a set of resources, in + this case pods. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: A label selector requirement is a selector + that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: operator represents a key's relationship + to a set of values. Valid operators are In, + NotIn, Exists and DoesNotExist. + type: string + values: + description: values is an array of string values. + If the operator is In or NotIn, the values + array must be non-empty. If the operator is + Exists or DoesNotExist, the values array must + be empty. This array is replaced during a + strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} pairs. + A single {key,value} in the matchLabels map is equivalent + to an element of matchExpressions, whose key field + is "key", the operator is "In", and the values array + contains only "value". The requirements are ANDed. + type: object + type: object + namespaces: + description: namespaces specifies which namespaces the + labelSelector applies to (matches against); null or + empty list means "this pod's namespace" + items: + type: string + type: array + topologyKey: + description: This pod should be co-located (affinity) + or not co-located (anti-affinity) with the pods matching + the labelSelector in the specified namespaces, where + co-located is defined as running on a node whose value + of the label with key topologyKey matches that of any + node on which any of the selected pods is running. Empty + topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + type: array + type: object + podAntiAffinity: + description: Describes pod anti-affinity scheduling rules (e.g. + avoid putting this pod in the same node, zone, etc. as some other + pod(s)). + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: The scheduler will prefer to schedule pods to nodes + that satisfy the anti-affinity expressions specified by this + field, but it may choose a node that violates one or more + of the expressions. The node that is most preferred is the + one with the greatest sum of weights, i.e. for each node that + meets all of the scheduling requirements (resource request, + requiredDuringScheduling anti-affinity expressions, etc.), + compute a sum by iterating through the elements of this field + and adding "weight" to the sum if the node has pods which + matches the corresponding podAffinityTerm; the node(s) with + the highest sum are the most preferred. + items: + description: The weights of all of the matched WeightedPodAffinityTerm + fields are added per-node to find the most preferred node(s) + properties: + podAffinityTerm: + description: Required. A pod affinity term, associated + with the corresponding weight. + properties: + labelSelector: + description: A label query over a set of resources, + in this case pods. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are + ANDed. + items: + description: A label selector requirement is + a selector that contains values, a key, and + an operator that relates the key and values. + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: operator represents a key's + relationship to a set of values. Valid + operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: values is an array of string + values. If the operator is In or NotIn, + the values array must be non-empty. If + the operator is Exists or DoesNotExist, + the values array must be empty. This array + is replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, + whose key field is "key", the operator is "In", + and the values array contains only "value". + The requirements are ANDed. + type: object + type: object + namespaces: + description: namespaces specifies which namespaces + the labelSelector applies to (matches against); + null or empty list means "this pod's namespace" + items: + type: string + type: array + topologyKey: + description: This pod should be co-located (affinity) + or not co-located (anti-affinity) with the pods + matching the labelSelector in the specified namespaces, + where co-located is defined as running on a node + whose value of the label with key topologyKey matches + that of any node on which any of the selected pods + is running. Empty topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + weight: + description: weight associated with matching the corresponding + podAffinityTerm, in the range 1-100. + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + description: If the anti-affinity requirements specified by + this field are not met at scheduling time, the pod will not + be scheduled onto the node. If the anti-affinity requirements + specified by this field cease to be met at some point during + pod execution (e.g. due to a pod label update), the system + may or may not try to eventually evict the pod from its node. + When there are multiple elements, the lists of nodes corresponding + to each podAffinityTerm are intersected, i.e. all terms must + be satisfied. + items: + description: Defines a set of pods (namely those matching + the labelSelector relative to the given namespace(s)) that + this pod should be co-located (affinity) or not co-located + (anti-affinity) with, where co-located is defined as running + on a node whose value of the label with key + matches that of any node on which a pod of the set of pods + is running + properties: + labelSelector: + description: A label query over a set of resources, in + this case pods. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: A label selector requirement is a selector + that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: operator represents a key's relationship + to a set of values. Valid operators are In, + NotIn, Exists and DoesNotExist. + type: string + values: + description: values is an array of string values. + If the operator is In or NotIn, the values + array must be non-empty. If the operator is + Exists or DoesNotExist, the values array must + be empty. This array is replaced during a + strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} pairs. + A single {key,value} in the matchLabels map is equivalent + to an element of matchExpressions, whose key field + is "key", the operator is "In", and the values array + contains only "value". The requirements are ANDed. + type: object + type: object + namespaces: + description: namespaces specifies which namespaces the + labelSelector applies to (matches against); null or + empty list means "this pod's namespace" + items: + type: string + type: array + topologyKey: + description: This pod should be co-located (affinity) + or not co-located (anti-affinity) with the pods matching + the labelSelector in the specified namespaces, where + co-located is defined as running on a node whose value + of the label with key topologyKey matches that of any + node on which any of the selected pods is running. Empty + topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + type: array + type: object + type: object + clusterMasterRef: + description: ClusterMasterRef refers to a Splunk Enterprise indexer + cluster managed by the operator within Kubernetes + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: 'If referring to a piece of an object instead of an + entire object, this string should contain a valid JSON/Go field + access statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to a container within + a pod, this would take on a value like: "spec.containers{name}" + (where "name" refers to the name of the container that triggered + the event) or if no container name is specified "spec.containers[2]" + (container with index 2 in this pod). This syntax is chosen only + to have some well-defined way of referencing a part of an object. + TODO: this design is not final and this field is subject to change + in the future.' + type: string + kind: + description: 'Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + namespace: + description: 'Namespace of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/' + type: string + resourceVersion: + description: 'Specific resourceVersion to which this reference is + made, if any. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency' + type: string + uid: + description: 'UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids' + type: string + type: object + defaults: + description: Inline map of default.yml overrides used to initialize + the environment + type: string + defaultsUrl: + description: Full path or URL for one or more default.yml files, separated + by commas + type: string + defaultsUrlApps: + description: Full path or URL for one or more defaults.yml files specific + to App install, separated by commas. The defaults listed here will + be installed on the CM, standalone, search head deployer or license + master instance. + type: string + etcVolumeStorageConfig: + description: Storage configuration for /opt/splunk/etc volume + properties: + ephemeralStorage: + description: If true, ephemeral (emptyDir) storage will be used + type: boolean + storageCapacity: + description: Storage capacity to request persistent volume claims + (default=”10Gi” for etc and "100Gi" for var) + type: string + storageClassName: + description: Name of StorageClass to use for persistent volume claims + type: string + type: object + image: + description: Image to use for Splunk pod containers (overrides RELATED_IMAGE_SPLUNK_ENTERPRISE + environment variables) + type: string + imagePullPolicy: + description: 'Sets pull policy for all images (either “Always” or the + default: “IfNotPresent”)' + enum: + - Always + - IfNotPresent + type: string + licenseMasterRef: + description: LicenseMasterRef refers to a Splunk Enterprise license + master managed by the operator within Kubernetes + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: 'If referring to a piece of an object instead of an + entire object, this string should contain a valid JSON/Go field + access statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to a container within + a pod, this would take on a value like: "spec.containers{name}" + (where "name" refers to the name of the container that triggered + the event) or if no container name is specified "spec.containers[2]" + (container with index 2 in this pod). This syntax is chosen only + to have some well-defined way of referencing a part of an object. + TODO: this design is not final and this field is subject to change + in the future.' + type: string + kind: + description: 'Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + namespace: + description: 'Namespace of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/' + type: string + resourceVersion: + description: 'Specific resourceVersion to which this reference is + made, if any. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency' + type: string + uid: + description: 'UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids' + type: string + type: object + licenseUrl: + description: Full path or URL for a Splunk Enterprise license file + type: string + replicas: + description: Number of standalone pods + format: int32 + type: integer + resources: + description: resource requirements for the pod containers + properties: + limits: + additionalProperties: + type: string + description: 'Limits describes the maximum amount of compute resources + allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/' + type: object + requests: + additionalProperties: + type: string + description: 'Requests describes the minimum amount of compute resources + required. If Requests is omitted for a container, it defaults + to Limits if that is explicitly specified, otherwise to an implementation-defined + value. More info: https://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/' + type: object + type: object + schedulerName: + description: Name of Scheduler to use for pod placement (defaults to + “default-scheduler”) + type: string + serviceAccount: + description: ServiceAccount is the service account used by the pods + deployed by the CRD. If not specified uses the default serviceAccount + for the namespace as per https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/#use-the-default-service-account-to-access-the-api-server + type: string + serviceTemplate: + description: ServiceTemplate is a template used to create Kubernetes + services + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the + latest internal value, and may reject unrecognized values. More + info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource + this object represents. Servers may infer this from the endpoint + the client submits requests to. Cannot be updated. In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + description: 'Standard object''s metadata. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata' + type: object + spec: + description: Spec defines the behavior of a service. https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status + properties: + clusterIP: + description: 'clusterIP is the IP address of the service and + is usually assigned randomly by the master. If an address + is specified manually and is not in use by others, it will + be allocated to the service; otherwise, creation of the service + will fail. This field can not be changed through updates. + Valid values are "None", empty string (""), or a valid IP + address. "None" can be specified for headless services when + proxying is not required. Only applies to types ClusterIP, + NodePort, and LoadBalancer. Ignored if type is ExternalName. + More info: https://kubernetes.io/docs/concepts/services-networking/service/#virtual-ips-and-service-proxies' + type: string + externalIPs: + description: externalIPs is a list of IP addresses for which + nodes in the cluster will also accept traffic for this service. These + IPs are not managed by Kubernetes. The user is responsible + for ensuring that traffic arrives at a node with this IP. A + common example is external load-balancers that are not part + of the Kubernetes system. + items: + type: string + type: array + externalName: + description: externalName is the external reference that kubedns + or equivalent will return as a CNAME record for this service. + No proxying will be involved. Must be a valid RFC-1123 hostname + (https://tools.ietf.org/html/rfc1123) and requires Type to + be ExternalName. + type: string + externalTrafficPolicy: + description: externalTrafficPolicy denotes if this Service desires + to route external traffic to node-local or cluster-wide endpoints. + "Local" preserves the client source IP and avoids a second + hop for LoadBalancer and Nodeport type services, but risks + potentially imbalanced traffic spreading. "Cluster" obscures + the client source IP and may cause a second hop to another + node, but should have good overall load-spreading. + type: string + healthCheckNodePort: + description: healthCheckNodePort specifies the healthcheck nodePort + for the service. If not specified, HealthCheckNodePort is + created by the service api backend with the allocated nodePort. + Will use user-specified nodePort value if specified by the + client. Only effects when Type is set to LoadBalancer and + ExternalTrafficPolicy is set to Local. + format: int32 + type: integer + ipFamily: + description: ipFamily specifies whether this Service has a preference + for a particular IP family (e.g. IPv4 vs. IPv6). If a specific + IP family is requested, the clusterIP field will be allocated + from that family, if it is available in the cluster. If no + IP family is requested, the cluster's primary IP family will + be used. Other IP fields (loadBalancerIP, loadBalancerSourceRanges, + externalIPs) and controllers which allocate external load-balancers + should use the same IP family. Endpoints for this Service + will be of this family. This field is immutable after creation. + Assigning a ServiceIPFamily not available in the cluster (e.g. + IPv6 in IPv4 only cluster) is an error condition and will + fail during clusterIP assignment. + type: string + loadBalancerIP: + description: 'Only applies to Service Type: LoadBalancer LoadBalancer + will get created with the IP specified in this field. This + feature depends on whether the underlying cloud-provider supports + specifying the loadBalancerIP when a load balancer is created. + This field will be ignored if the cloud-provider does not + support the feature.' + type: string + loadBalancerSourceRanges: + description: 'If specified and supported by the platform, this + will restrict traffic through the cloud-provider load-balancer + will be restricted to the specified client IPs. This field + will be ignored if the cloud-provider does not support the + feature." More info: https://kubernetes.io/docs/tasks/access-application-cluster/configure-cloud-provider-firewall/' + items: + type: string + type: array + ports: + description: 'The list of ports that are exposed by this service. + More info: https://kubernetes.io/docs/concepts/services-networking/service/#virtual-ips-and-service-proxies' + items: + description: ServicePort contains information on service's + port. + properties: + name: + description: The name of this port within the service. + This must be a DNS_LABEL. All ports within a ServiceSpec + must have unique names. When considering the endpoints + for a Service, this must match the 'name' field in the + EndpointPort. Optional if only one ServicePort is defined + on this service. + type: string + nodePort: + description: 'The port on each node on which this service + is exposed when type=NodePort or LoadBalancer. Usually + assigned by the system. If specified, it will be allocated + to the service if unused or else creation of the service + will fail. Default is to auto-allocate a port if the + ServiceType of this Service requires one. More info: + https://kubernetes.io/docs/concepts/services-networking/service/#type-nodeport' + format: int32 + type: integer + port: + description: The port that will be exposed by this service. + format: int32 + type: integer + protocol: + description: The IP protocol for this port. Supports "TCP", + "UDP", and "SCTP". Default is TCP. + type: string + targetPort: + anyOf: + - type: integer + - type: string + description: 'Number or name of the port to access on + the pods targeted by the service. Number must be in + the range 1 to 65535. Name must be an IANA_SVC_NAME. + If this is a string, it will be looked up as a named + port in the target Pod''s container ports. If this is + not specified, the value of the ''port'' field is used + (an identity map). This field is ignored for services + with clusterIP=None, and should be omitted or set equal + to the ''port'' field. More info: https://kubernetes.io/docs/concepts/services-networking/service/#defining-a-service' + x-kubernetes-int-or-string: true + required: + - port + type: object + type: array + publishNotReadyAddresses: + description: publishNotReadyAddresses, when set to true, indicates + that DNS implementations must publish the notReadyAddresses + of subsets for the Endpoints associated with the Service. + The default value is false. The primary use case for setting + this field is to use a StatefulSet's Headless Service to propagate + SRV records for its Pods without respect to their readiness + for purpose of peer discovery. + type: boolean + selector: + additionalProperties: + type: string + description: 'Route service traffic to pods with label keys + and values matching this selector. If empty or not present, + the service is assumed to have an external process managing + its endpoints, which Kubernetes will not modify. Only applies + to types ClusterIP, NodePort, and LoadBalancer. Ignored if + type is ExternalName. More info: https://kubernetes.io/docs/concepts/services-networking/service/' + type: object + sessionAffinity: + description: 'Supports "ClientIP" and "None". Used to maintain + session affinity. Enable client IP based session affinity. + Must be ClientIP or None. Defaults to None. More info: https://kubernetes.io/docs/concepts/services-networking/service/#virtual-ips-and-service-proxies' + type: string + sessionAffinityConfig: + description: sessionAffinityConfig contains the configurations + of session affinity. + properties: + clientIP: + description: clientIP contains the configurations of Client + IP based session affinity. + properties: + timeoutSeconds: + description: timeoutSeconds specifies the seconds of + ClientIP type session sticky time. The value must + be >0 && <=86400(for 1 day) if ServiceAffinity == + "ClientIP". Default value is 10800(for 3 hours). + format: int32 + type: integer + type: object + type: object + type: + description: 'type determines how the Service is exposed. Defaults + to ClusterIP. Valid options are ExternalName, ClusterIP, NodePort, + and LoadBalancer. "ExternalName" maps to the specified externalName. + "ClusterIP" allocates a cluster-internal IP address for load-balancing + to endpoints. Endpoints are determined by the selector or + if that is not specified, by manual construction of an Endpoints + object. If clusterIP is "None", no virtual IP is allocated + and the endpoints are published as a set of endpoints rather + than a stable IP. "NodePort" builds on ClusterIP and allocates + a port on every node which routes to the clusterIP. "LoadBalancer" + builds on NodePort and creates an external load-balancer (if + supported in the current cloud) which routes to the clusterIP. + More info: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types' + type: string + type: object + status: + description: 'Most recently observed status of the service. Populated + by the system. Read-only. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status' + properties: + loadBalancer: + description: LoadBalancer contains the current status of the + load-balancer, if one is present. + properties: + ingress: + description: Ingress is a list containing ingress points + for the load-balancer. Traffic intended for the service + should be sent to these ingress points. + items: + description: 'LoadBalancerIngress represents the status + of a load-balancer ingress point: traffic intended for + the service should be sent to an ingress point.' + properties: + hostname: + description: Hostname is set for load-balancer ingress + points that are DNS based (typically AWS load-balancers) + type: string + ip: + description: IP is set for load-balancer ingress points + that are IP based (typically GCE or OpenStack load-balancers) + type: string + type: object + type: array + type: object + type: object + type: object + smartstore: + description: Splunk Smartstore configuration. Refer to indexes.conf.spec + and server.conf.spec on docs.splunk.com + properties: + cacheManager: + description: Defines Cache manager settings + properties: + evictionPadding: + description: Additional size beyond 'minFreeSize' before eviction + kicks in + type: integer + evictionPolicy: + description: Eviction policy to use + type: string + hotlistBloomFilterRecencyHours: + description: Time period relative to the bucket's age, during + which the bloom filter file is protected from cache eviction + type: integer + hotlistRecencySecs: + description: Time period relative to the bucket's age, during + which the bucket is protected from cache eviction + type: integer + maxCacheSize: + description: Max cache size per partition + type: integer + maxConcurrentDownloads: + description: Maximum number of buckets that can be downloaded + from remote storage in parallel + type: integer + maxConcurrentUploads: + description: Maximum number of buckets that can be uploaded + to remote storage in parallel + type: integer + type: object + defaults: + description: Default configuration for indexes + properties: + maxGlobalDataSizeMB: + description: MaxGlobalDataSizeMB defines the maximum amount + of space for warm and cold buckets of an index + type: integer + maxGlobalRawDataSizeMB: + description: MaxGlobalDataSizeMB defines the maximum amount + of cumulative space for warm and cold buckets of an index + type: integer + volumeName: + description: Remote Volume name + type: string + type: object + indexes: + description: List of Splunk indexes + items: + description: IndexSpec defines Splunk index name and storage path + properties: + hotlistBloomFilterRecencyHours: + description: Time period relative to the bucket's age, during + which the bloom filter file is protected from cache eviction + type: integer + hotlistRecencySecs: + description: Time period relative to the bucket's age, during + which the bucket is protected from cache eviction + type: integer + maxGlobalDataSizeMB: + description: MaxGlobalDataSizeMB defines the maximum amount + of space for warm and cold buckets of an index + type: integer + maxGlobalRawDataSizeMB: + description: MaxGlobalDataSizeMB defines the maximum amount + of cumulative space for warm and cold buckets of an index + type: integer + name: + description: Splunk index name + type: string + remotePath: + description: Index location relative to the remote volume + path + type: string + volumeName: + description: Remote Volume name + type: string + type: object + type: array + volumes: + description: List of remote storage volumes + items: + description: VolumeSpec defines remote volume name and remote + volume URI + properties: + endpoint: + description: Remote volume URI + type: string + name: + description: Remote volume name + type: string + path: + description: Remote volume path + type: string + secretRef: + description: Secret object name + type: string + type: object + type: array + type: object + sparkImage: + description: Image to use for Spark pod containers (overrides RELATED_IMAGE_SPLUNK_SPARK + environment variables) + type: string + sparkRef: + description: SparkRef refers to a Spark cluster managed by the operator + within Kubernetes When defined, Data Fabric Search (DFS) will be enabled + and configured to use the Spark cluster. + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: 'If referring to a piece of an object instead of an + entire object, this string should contain a valid JSON/Go field + access statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to a container within + a pod, this would take on a value like: "spec.containers{name}" + (where "name" refers to the name of the container that triggered + the event) or if no container name is specified "spec.containers[2]" + (container with index 2 in this pod). This syntax is chosen only + to have some well-defined way of referencing a part of an object. + TODO: this design is not final and this field is subject to change + in the future.' + type: string + kind: + description: 'Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + namespace: + description: 'Namespace of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/' + type: string + resourceVersion: + description: 'Specific resourceVersion to which this reference is + made, if any. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency' + type: string + uid: + description: 'UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids' + type: string + type: object + tolerations: + description: Pod's tolerations for Kubernetes node's taint + items: + description: The pod this Toleration is attached to tolerates any + taint that matches the triple using the matching + operator . + properties: + effect: + description: Effect indicates the taint effect to match. Empty + means match all taint effects. When specified, allowed values + are NoSchedule, PreferNoSchedule and NoExecute. + type: string + key: + description: Key is the taint key that the toleration applies + to. Empty means match all taint keys. If the key is empty, operator + must be Exists; this combination means to match all values and + all keys. + type: string + operator: + description: Operator represents a key's relationship to the value. + Valid operators are Exists and Equal. Defaults to Equal. Exists + is equivalent to wildcard for value, so that a pod can tolerate + all taints of a particular category. + type: string + tolerationSeconds: + description: TolerationSeconds represents the period of time the + toleration (which must be of effect NoExecute, otherwise this + field is ignored) tolerates the taint. By default, it is not + set, which means tolerate the taint forever (do not evict). + Zero and negative values will be treated as 0 (evict immediately) + by the system. + format: int64 + type: integer + value: + description: Value is the taint value the toleration matches to. + If the operator is Exists, the value should be empty, otherwise + just a regular string. + type: string + type: object + type: array + varVolumeStorageConfig: + description: Storage configuration for /opt/splunk/var volume + properties: + ephemeralStorage: + description: If true, ephemeral (emptyDir) storage will be used + type: boolean + storageCapacity: + description: Storage capacity to request persistent volume claims + (default=”10Gi” for etc and "100Gi" for var) + type: string + storageClassName: + description: Name of StorageClass to use for persistent volume claims + type: string + type: object + volumes: + description: List of one or more Kubernetes volumes. These will be mounted + in all pod containers as as /mnt/ + items: + description: Volume represents a named volume in a pod that may be + accessed by any container in the pod. + properties: + awsElasticBlockStore: + description: 'AWSElasticBlockStore represents an AWS Disk resource + that is attached to a kubelet''s host machine and then exposed + to the pod. More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore' + properties: + fsType: + description: 'Filesystem type of the volume that you want + to mount. Tip: Ensure that the filesystem type is supported + by the host operating system. Examples: "ext4", "xfs", "ntfs". + Implicitly inferred to be "ext4" if unspecified. More info: + https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore + TODO: how do we prevent errors in the filesystem from compromising + the machine' + type: string + partition: + description: 'The partition in the volume that you want to + mount. If omitted, the default is to mount by volume name. + Examples: For volume /dev/sda1, you specify the partition + as "1". Similarly, the volume partition for /dev/sda is + "0" (or you can leave the property empty).' + format: int32 + type: integer + readOnly: + description: 'Specify "true" to force and set the ReadOnly + property in VolumeMounts to "true". If omitted, the default + is "false". More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore' + type: boolean + volumeID: + description: 'Unique ID of the persistent disk resource in + AWS (Amazon EBS volume). More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore' + type: string + required: + - volumeID + type: object + azureDisk: + description: AzureDisk represents an Azure Data Disk mount on + the host and bind mount to the pod. + properties: + cachingMode: + description: 'Host Caching mode: None, Read Only, Read Write.' + type: string + diskName: + description: The Name of the data disk in the blob storage + type: string + diskURI: + description: The URI the data disk in the blob storage + type: string + fsType: + description: Filesystem type to mount. Must be a filesystem + type supported by the host operating system. Ex. "ext4", + "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + type: string + kind: + description: 'Expected values Shared: multiple blob disks + per storage account Dedicated: single blob disk per storage + account Managed: azure managed data disk (only in managed + availability set). defaults to shared' + type: string + readOnly: + description: Defaults to false (read/write). ReadOnly here + will force the ReadOnly setting in VolumeMounts. + type: boolean + required: + - diskName + - diskURI + type: object + azureFile: + description: AzureFile represents an Azure File Service mount + on the host and bind mount to the pod. + properties: + readOnly: + description: Defaults to false (read/write). ReadOnly here + will force the ReadOnly setting in VolumeMounts. + type: boolean + secretName: + description: the name of secret that contains Azure Storage + Account Name and Key + type: string + shareName: + description: Share Name + type: string + required: + - secretName + - shareName + type: object + cephfs: + description: CephFS represents a Ceph FS mount on the host that + shares a pod's lifetime + properties: + monitors: + description: 'Required: Monitors is a collection of Ceph monitors + More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it' + items: + type: string + type: array + path: + description: 'Optional: Used as the mounted root, rather than + the full Ceph tree, default is /' + type: string + readOnly: + description: 'Optional: Defaults to false (read/write). ReadOnly + here will force the ReadOnly setting in VolumeMounts. More + info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it' + type: boolean + secretFile: + description: 'Optional: SecretFile is the path to key ring + for User, default is /etc/ceph/user.secret More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it' + type: string + secretRef: + description: 'Optional: SecretRef is reference to the authentication + secret for User, default is empty. More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it' + properties: + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + type: object + user: + description: 'Optional: User is the rados user name, default + is admin More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it' + type: string + required: + - monitors + type: object + cinder: + description: 'Cinder represents a cinder volume attached and mounted + on kubelets host machine. More info: https://examples.k8s.io/mysql-cinder-pd/README.md' + properties: + fsType: + description: 'Filesystem type to mount. Must be a filesystem + type supported by the host operating system. Examples: "ext4", + "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + More info: https://examples.k8s.io/mysql-cinder-pd/README.md' + type: string + readOnly: + description: 'Optional: Defaults to false (read/write). ReadOnly + here will force the ReadOnly setting in VolumeMounts. More + info: https://examples.k8s.io/mysql-cinder-pd/README.md' + type: boolean + secretRef: + description: 'Optional: points to a secret object containing + parameters used to connect to OpenStack.' + properties: + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + type: object + volumeID: + description: 'volume id used to identify the volume in cinder. + More info: https://examples.k8s.io/mysql-cinder-pd/README.md' + type: string + required: + - volumeID + type: object + configMap: + description: ConfigMap represents a configMap that should populate + this volume + properties: + defaultMode: + description: 'Optional: mode bits to use on created files + by default. Must be a value between 0 and 0777. Defaults + to 0644. Directories within the path are not affected by + this setting. This might be in conflict with other options + that affect the file mode, like fsGroup, and the result + can be other mode bits set.' + format: int32 + type: integer + items: + description: If unspecified, each key-value pair in the Data + field of the referenced ConfigMap will be projected into + the volume as a file whose name is the key and content is + the value. If specified, the listed keys will be projected + into the specified paths, and unlisted keys will not be + present. If a key is specified which is not present in the + ConfigMap, the volume setup will error unless it is marked + optional. Paths must be relative and may not contain the + '..' path or start with '..'. + items: + description: Maps a string key to a path within a volume. + properties: + key: + description: The key to project. + type: string + mode: + description: 'Optional: mode bits to use on this file, + must be a value between 0 and 0777. If not specified, + the volume defaultMode will be used. This might be + in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode + bits set.' + format: int32 + type: integer + path: + description: The relative path of the file to map the + key to. May not be an absolute path. May not contain + the path element '..'. May not start with the string + '..'. + type: string + required: + - key + - path + type: object + type: array + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + optional: + description: Specify whether the ConfigMap or its keys must + be defined + type: boolean + type: object + csi: + description: CSI (Container Storage Interface) represents storage + that is handled by an external CSI driver (Alpha feature). + properties: + driver: + description: Driver is the name of the CSI driver that handles + this volume. Consult with your admin for the correct name + as registered in the cluster. + type: string + fsType: + description: Filesystem type to mount. Ex. "ext4", "xfs", + "ntfs". If not provided, the empty value is passed to the + associated CSI driver which will determine the default filesystem + to apply. + type: string + nodePublishSecretRef: + description: NodePublishSecretRef is a reference to the secret + object containing sensitive information to pass to the CSI + driver to complete the CSI NodePublishVolume and NodeUnpublishVolume + calls. This field is optional, and may be empty if no secret + is required. If the secret object contains more than one + secret, all secret references are passed. + properties: + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + type: object + readOnly: + description: Specifies a read-only configuration for the volume. + Defaults to false (read/write). + type: boolean + volumeAttributes: + additionalProperties: + type: string + description: VolumeAttributes stores driver-specific properties + that are passed to the CSI driver. Consult your driver's + documentation for supported values. + type: object + required: + - driver + type: object + downwardAPI: + description: DownwardAPI represents downward API about the pod + that should populate this volume + properties: + defaultMode: + description: 'Optional: mode bits to use on created files + by default. Must be a value between 0 and 0777. Defaults + to 0644. Directories within the path are not affected by + this setting. This might be in conflict with other options + that affect the file mode, like fsGroup, and the result + can be other mode bits set.' + format: int32 + type: integer + items: + description: Items is a list of downward API volume file + items: + description: DownwardAPIVolumeFile represents information + to create the file containing the pod field + properties: + fieldRef: + description: 'Required: Selects a field of the pod: + only annotations, labels, name and namespace are supported.' + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in the + specified API version. + type: string + required: + - fieldPath + type: object + mode: + description: 'Optional: mode bits to use on this file, + must be a value between 0 and 0777. If not specified, + the volume defaultMode will be used. This might be + in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode + bits set.' + format: int32 + type: integer + path: + description: 'Required: Path is the relative path name + of the file to be created. Must not be absolute or + contain the ''..'' path. Must be utf-8 encoded. The + first item of the relative path must not start with + ''..''' + type: string + resourceFieldRef: + description: 'Selects a resource of the container: only + resources limits and requests (limits.cpu, limits.memory, + requests.cpu and requests.memory) are currently supported.' + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + description: Specifies the output format of the + exposed resources, defaults to "1" + type: string + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + required: + - path + type: object + type: array + type: object + emptyDir: + description: 'EmptyDir represents a temporary directory that shares + a pod''s lifetime. More info: https://kubernetes.io/docs/concepts/storage/volumes#emptydir' + properties: + medium: + description: 'What type of storage medium should back this + directory. The default is "" which means to use the node''s + default medium. Must be an empty string (default) or Memory. + More info: https://kubernetes.io/docs/concepts/storage/volumes#emptydir' + type: string + sizeLimit: + description: 'Total amount of local storage required for this + EmptyDir volume. The size limit is also applicable for memory + medium. The maximum usage on memory medium EmptyDir would + be the minimum value between the SizeLimit specified here + and the sum of memory limits of all containers in a pod. + The default is nil which means that the limit is undefined. + More info: http://kubernetes.io/docs/user-guide/volumes#emptydir' + type: string + type: object + fc: + description: FC represents a Fibre Channel resource that is attached + to a kubelet's host machine and then exposed to the pod. + properties: + fsType: + description: 'Filesystem type to mount. Must be a filesystem + type supported by the host operating system. Ex. "ext4", + "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + TODO: how do we prevent errors in the filesystem from compromising + the machine' + type: string + lun: + description: 'Optional: FC target lun number' + format: int32 + type: integer + readOnly: + description: 'Optional: Defaults to false (read/write). ReadOnly + here will force the ReadOnly setting in VolumeMounts.' + type: boolean + targetWWNs: + description: 'Optional: FC target worldwide names (WWNs)' + items: + type: string + type: array + wwids: + description: 'Optional: FC volume world wide identifiers (wwids) + Either wwids or combination of targetWWNs and lun must be + set, but not both simultaneously.' + items: + type: string + type: array + type: object + flexVolume: + description: FlexVolume represents a generic volume resource that + is provisioned/attached using an exec based plugin. + properties: + driver: + description: Driver is the name of the driver to use for this + volume. + type: string + fsType: + description: Filesystem type to mount. Must be a filesystem + type supported by the host operating system. Ex. "ext4", + "xfs", "ntfs". The default filesystem depends on FlexVolume + script. + type: string + options: + additionalProperties: + type: string + description: 'Optional: Extra command options if any.' + type: object + readOnly: + description: 'Optional: Defaults to false (read/write). ReadOnly + here will force the ReadOnly setting in VolumeMounts.' + type: boolean + secretRef: + description: 'Optional: SecretRef is reference to the secret + object containing sensitive information to pass to the plugin + scripts. This may be empty if no secret object is specified. + If the secret object contains more than one secret, all + secrets are passed to the plugin scripts.' + properties: + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + type: object + required: + - driver + type: object + flocker: + description: Flocker represents a Flocker volume attached to a + kubelet's host machine. This depends on the Flocker control + service being running + properties: + datasetName: + description: Name of the dataset stored as metadata -> name + on the dataset for Flocker should be considered as deprecated + type: string + datasetUUID: + description: UUID of the dataset. This is unique identifier + of a Flocker dataset + type: string + type: object + gcePersistentDisk: + description: 'GCEPersistentDisk represents a GCE Disk resource + that is attached to a kubelet''s host machine and then exposed + to the pod. More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk' + properties: + fsType: + description: 'Filesystem type of the volume that you want + to mount. Tip: Ensure that the filesystem type is supported + by the host operating system. Examples: "ext4", "xfs", "ntfs". + Implicitly inferred to be "ext4" if unspecified. More info: + https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk + TODO: how do we prevent errors in the filesystem from compromising + the machine' + type: string + partition: + description: 'The partition in the volume that you want to + mount. If omitted, the default is to mount by volume name. + Examples: For volume /dev/sda1, you specify the partition + as "1". Similarly, the volume partition for /dev/sda is + "0" (or you can leave the property empty). More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk' + format: int32 + type: integer + pdName: + description: 'Unique name of the PD resource in GCE. Used + to identify the disk in GCE. More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk' + type: string + readOnly: + description: 'ReadOnly here will force the ReadOnly setting + in VolumeMounts. Defaults to false. More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk' + type: boolean + required: + - pdName + type: object + gitRepo: + description: 'GitRepo represents a git repository at a particular + revision. DEPRECATED: GitRepo is deprecated. To provision a + container with a git repo, mount an EmptyDir into an InitContainer + that clones the repo using git, then mount the EmptyDir into + the Pod''s container.' + properties: + directory: + description: Target directory name. Must not contain or start + with '..'. If '.' is supplied, the volume directory will + be the git repository. Otherwise, if specified, the volume + will contain the git repository in the subdirectory with + the given name. + type: string + repository: + description: Repository URL + type: string + revision: + description: Commit hash for the specified revision. + type: string + required: + - repository + type: object + glusterfs: + description: 'Glusterfs represents a Glusterfs mount on the host + that shares a pod''s lifetime. More info: https://examples.k8s.io/volumes/glusterfs/README.md' + properties: + endpoints: + description: 'EndpointsName is the endpoint name that details + Glusterfs topology. More info: https://examples.k8s.io/volumes/glusterfs/README.md#create-a-pod' + type: string + path: + description: 'Path is the Glusterfs volume path. More info: + https://examples.k8s.io/volumes/glusterfs/README.md#create-a-pod' + type: string + readOnly: + description: 'ReadOnly here will force the Glusterfs volume + to be mounted with read-only permissions. Defaults to false. + More info: https://examples.k8s.io/volumes/glusterfs/README.md#create-a-pod' + type: boolean + required: + - endpoints + - path + type: object + hostPath: + description: 'HostPath represents a pre-existing file or directory + on the host machine that is directly exposed to the container. + This is generally used for system agents or other privileged + things that are allowed to see the host machine. Most containers + will NOT need this. More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath + --- TODO(jonesdl) We need to restrict who can use host directory + mounts and who can/can not mount host directories as read/write.' + properties: + path: + description: 'Path of the directory on the host. If the path + is a symlink, it will follow the link to the real path. + More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath' + type: string + type: + description: 'Type for HostPath Volume Defaults to "" More + info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath' + type: string + required: + - path + type: object + iscsi: + description: 'ISCSI represents an ISCSI Disk resource that is + attached to a kubelet''s host machine and then exposed to the + pod. More info: https://examples.k8s.io/volumes/iscsi/README.md' + properties: + chapAuthDiscovery: + description: whether support iSCSI Discovery CHAP authentication + type: boolean + chapAuthSession: + description: whether support iSCSI Session CHAP authentication + type: boolean + fsType: + description: 'Filesystem type of the volume that you want + to mount. Tip: Ensure that the filesystem type is supported + by the host operating system. Examples: "ext4", "xfs", "ntfs". + Implicitly inferred to be "ext4" if unspecified. More info: + https://kubernetes.io/docs/concepts/storage/volumes#iscsi + TODO: how do we prevent errors in the filesystem from compromising + the machine' + type: string + initiatorName: + description: Custom iSCSI Initiator Name. If initiatorName + is specified with iscsiInterface simultaneously, new iSCSI + interface : will be created + for the connection. + type: string + iqn: + description: Target iSCSI Qualified Name. + type: string + iscsiInterface: + description: iSCSI Interface Name that uses an iSCSI transport. + Defaults to 'default' (tcp). + type: string + lun: + description: iSCSI Target Lun number. + format: int32 + type: integer + portals: + description: iSCSI Target Portal List. The portal is either + an IP or ip_addr:port if the port is other than default + (typically TCP ports 860 and 3260). + items: + type: string + type: array + readOnly: + description: ReadOnly here will force the ReadOnly setting + in VolumeMounts. Defaults to false. + type: boolean + secretRef: + description: CHAP Secret for iSCSI target and initiator authentication + properties: + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + type: object + targetPortal: + description: iSCSI Target Portal. The Portal is either an + IP or ip_addr:port if the port is other than default (typically + TCP ports 860 and 3260). + type: string + required: + - iqn + - lun + - targetPortal + type: object + name: + description: 'Volume''s name. Must be a DNS_LABEL and unique within + the pod. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + nfs: + description: 'NFS represents an NFS mount on the host that shares + a pod''s lifetime More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs' + properties: + path: + description: 'Path that is exported by the NFS server. More + info: https://kubernetes.io/docs/concepts/storage/volumes#nfs' + type: string + readOnly: + description: 'ReadOnly here will force the NFS export to be + mounted with read-only permissions. Defaults to false. More + info: https://kubernetes.io/docs/concepts/storage/volumes#nfs' + type: boolean + server: + description: 'Server is the hostname or IP address of the + NFS server. More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs' + type: string + required: + - path + - server + type: object + persistentVolumeClaim: + description: 'PersistentVolumeClaimVolumeSource represents a reference + to a PersistentVolumeClaim in the same namespace. More info: + https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims' + properties: + claimName: + description: 'ClaimName is the name of a PersistentVolumeClaim + in the same namespace as the pod using this volume. More + info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims' + type: string + readOnly: + description: Will force the ReadOnly setting in VolumeMounts. + Default false. + type: boolean + required: + - claimName + type: object + photonPersistentDisk: + description: PhotonPersistentDisk represents a PhotonController + persistent disk attached and mounted on kubelets host machine + properties: + fsType: + description: Filesystem type to mount. Must be a filesystem + type supported by the host operating system. Ex. "ext4", + "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + type: string + pdID: + description: ID that identifies Photon Controller persistent + disk + type: string + required: + - pdID + type: object + portworxVolume: + description: PortworxVolume represents a portworx volume attached + and mounted on kubelets host machine + properties: + fsType: + description: FSType represents the filesystem type to mount + Must be a filesystem type supported by the host operating + system. Ex. "ext4", "xfs". Implicitly inferred to be "ext4" + if unspecified. + type: string + readOnly: + description: Defaults to false (read/write). ReadOnly here + will force the ReadOnly setting in VolumeMounts. + type: boolean + volumeID: + description: VolumeID uniquely identifies a Portworx volume + type: string + required: + - volumeID + type: object + projected: + description: Items for all in one resources secrets, configmaps, + and downward API + properties: + defaultMode: + description: Mode bits to use on created files by default. + Must be a value between 0 and 0777. Directories within the + path are not affected by this setting. This might be in + conflict with other options that affect the file mode, like + fsGroup, and the result can be other mode bits set. + format: int32 + type: integer + sources: + description: list of volume projections + items: + description: Projection that may be projected along with + other supported volume types + properties: + configMap: + description: information about the configMap data to + project + properties: + items: + description: If unspecified, each key-value pair + in the Data field of the referenced ConfigMap + will be projected into the volume as a file whose + name is the key and content is the value. If specified, + the listed keys will be projected into the specified + paths, and unlisted keys will not be present. + If a key is specified which is not present in + the ConfigMap, the volume setup will error unless + it is marked optional. Paths must be relative + and may not contain the '..' path or start with + '..'. + items: + description: Maps a string key to a path within + a volume. + properties: + key: + description: The key to project. + type: string + mode: + description: 'Optional: mode bits to use on + this file, must be a value between 0 and + 0777. If not specified, the volume defaultMode + will be used. This might be in conflict + with other options that affect the file + mode, like fsGroup, and the result can be + other mode bits set.' + format: int32 + type: integer + path: + description: The relative path of the file + to map the key to. May not be an absolute + path. May not contain the path element '..'. + May not start with the string '..'. + type: string + required: + - key + - path + type: object + type: array + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, + uid?' + type: string + optional: + description: Specify whether the ConfigMap or its + keys must be defined + type: boolean + type: object + downwardAPI: + description: information about the downwardAPI data + to project + properties: + items: + description: Items is a list of DownwardAPIVolume + file + items: + description: DownwardAPIVolumeFile represents + information to create the file containing the + pod field + properties: + fieldRef: + description: 'Required: Selects a field of + the pod: only annotations, labels, name + and namespace are supported.' + properties: + apiVersion: + description: Version of the schema the + FieldPath is written in terms of, defaults + to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + mode: + description: 'Optional: mode bits to use on + this file, must be a value between 0 and + 0777. If not specified, the volume defaultMode + will be used. This might be in conflict + with other options that affect the file + mode, like fsGroup, and the result can be + other mode bits set.' + format: int32 + type: integer + path: + description: 'Required: Path is the relative + path name of the file to be created. Must + not be absolute or contain the ''..'' path. + Must be utf-8 encoded. The first item of + the relative path must not start with ''..''' + type: string + resourceFieldRef: + description: 'Selects a resource of the container: + only resources limits and requests (limits.cpu, + limits.memory, requests.cpu and requests.memory) + are currently supported.' + properties: + containerName: + description: 'Container name: required + for volumes, optional for env vars' + type: string + divisor: + description: Specifies the output format + of the exposed resources, defaults to + "1" + type: string + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + required: + - path + type: object + type: array + type: object + secret: + description: information about the secret data to project + properties: + items: + description: If unspecified, each key-value pair + in the Data field of the referenced Secret will + be projected into the volume as a file whose name + is the key and content is the value. If specified, + the listed keys will be projected into the specified + paths, and unlisted keys will not be present. + If a key is specified which is not present in + the Secret, the volume setup will error unless + it is marked optional. Paths must be relative + and may not contain the '..' path or start with + '..'. + items: + description: Maps a string key to a path within + a volume. + properties: + key: + description: The key to project. + type: string + mode: + description: 'Optional: mode bits to use on + this file, must be a value between 0 and + 0777. If not specified, the volume defaultMode + will be used. This might be in conflict + with other options that affect the file + mode, like fsGroup, and the result can be + other mode bits set.' + format: int32 + type: integer + path: + description: The relative path of the file + to map the key to. May not be an absolute + path. May not contain the path element '..'. + May not start with the string '..'. + type: string + required: + - key + - path + type: object + type: array + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, + uid?' + type: string + optional: + description: Specify whether the Secret or its key + must be defined + type: boolean + type: object + serviceAccountToken: + description: information about the serviceAccountToken + data to project + properties: + audience: + description: Audience is the intended audience of + the token. A recipient of a token must identify + itself with an identifier specified in the audience + of the token, and otherwise should reject the + token. The audience defaults to the identifier + of the apiserver. + type: string + expirationSeconds: + description: ExpirationSeconds is the requested + duration of validity of the service account token. + As the token approaches expiration, the kubelet + volume plugin will proactively rotate the service + account token. The kubelet will start trying to + rotate the token if the token is older than 80 + percent of its time to live or if the token is + older than 24 hours.Defaults to 1 hour and must + be at least 10 minutes. + format: int64 + type: integer + path: + description: Path is the path relative to the mount + point of the file to project the token into. + type: string + required: + - path + type: object + type: object + type: array + required: + - sources + type: object + quobyte: + description: Quobyte represents a Quobyte mount on the host that + shares a pod's lifetime + properties: + group: + description: Group to map volume access to Default is no group + type: string + readOnly: + description: ReadOnly here will force the Quobyte volume to + be mounted with read-only permissions. Defaults to false. + type: boolean + registry: + description: Registry represents a single or multiple Quobyte + Registry services specified as a string as host:port pair + (multiple entries are separated with commas) which acts + as the central registry for volumes + type: string + tenant: + description: Tenant owning the given Quobyte volume in the + Backend Used with dynamically provisioned Quobyte volumes, + value is set by the plugin + type: string + user: + description: User to map volume access to Defaults to serivceaccount + user + type: string + volume: + description: Volume is a string that references an already + created Quobyte volume by name. + type: string + required: + - registry + - volume + type: object + rbd: + description: 'RBD represents a Rados Block Device mount on the + host that shares a pod''s lifetime. More info: https://examples.k8s.io/volumes/rbd/README.md' + properties: + fsType: + description: 'Filesystem type of the volume that you want + to mount. Tip: Ensure that the filesystem type is supported + by the host operating system. Examples: "ext4", "xfs", "ntfs". + Implicitly inferred to be "ext4" if unspecified. More info: + https://kubernetes.io/docs/concepts/storage/volumes#rbd + TODO: how do we prevent errors in the filesystem from compromising + the machine' + type: string + image: + description: 'The rados image name. More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it' + type: string + keyring: + description: 'Keyring is the path to key ring for RBDUser. + Default is /etc/ceph/keyring. More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it' + type: string + monitors: + description: 'A collection of Ceph monitors. More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it' + items: + type: string + type: array + pool: + description: 'The rados pool name. Default is rbd. More info: + https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it' + type: string + readOnly: + description: 'ReadOnly here will force the ReadOnly setting + in VolumeMounts. Defaults to false. More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it' + type: boolean + secretRef: + description: 'SecretRef is name of the authentication secret + for RBDUser. If provided overrides keyring. Default is nil. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it' + properties: + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + type: object + user: + description: 'The rados user name. Default is admin. More + info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it' + type: string + required: + - image + - monitors + type: object + scaleIO: + description: ScaleIO represents a ScaleIO persistent volume attached + and mounted on Kubernetes nodes. + properties: + fsType: + description: Filesystem type to mount. Must be a filesystem + type supported by the host operating system. Ex. "ext4", + "xfs", "ntfs". Default is "xfs". + type: string + gateway: + description: The host address of the ScaleIO API Gateway. + type: string + protectionDomain: + description: The name of the ScaleIO Protection Domain for + the configured storage. + type: string + readOnly: + description: Defaults to false (read/write). ReadOnly here + will force the ReadOnly setting in VolumeMounts. + type: boolean + secretRef: + description: SecretRef references to the secret for ScaleIO + user and other sensitive information. If this is not provided, + Login operation will fail. + properties: + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + type: object + sslEnabled: + description: Flag to enable/disable SSL communication with + Gateway, default false + type: boolean + storageMode: + description: Indicates whether the storage for a volume should + be ThickProvisioned or ThinProvisioned. Default is ThinProvisioned. + type: string + storagePool: + description: The ScaleIO Storage Pool associated with the + protection domain. + type: string + system: + description: The name of the storage system as configured + in ScaleIO. + type: string + volumeName: + description: The name of a volume already created in the ScaleIO + system that is associated with this volume source. + type: string + required: + - gateway + - secretRef + - system + type: object + secret: + description: 'Secret represents a secret that should populate + this volume. More info: https://kubernetes.io/docs/concepts/storage/volumes#secret' + properties: + defaultMode: + description: 'Optional: mode bits to use on created files + by default. Must be a value between 0 and 0777. Defaults + to 0644. Directories within the path are not affected by + this setting. This might be in conflict with other options + that affect the file mode, like fsGroup, and the result + can be other mode bits set.' + format: int32 + type: integer + items: + description: If unspecified, each key-value pair in the Data + field of the referenced Secret will be projected into the + volume as a file whose name is the key and content is the + value. If specified, the listed keys will be projected into + the specified paths, and unlisted keys will not be present. + If a key is specified which is not present in the Secret, + the volume setup will error unless it is marked optional. + Paths must be relative and may not contain the '..' path + or start with '..'. + items: + description: Maps a string key to a path within a volume. + properties: + key: + description: The key to project. + type: string + mode: + description: 'Optional: mode bits to use on this file, + must be a value between 0 and 0777. If not specified, + the volume defaultMode will be used. This might be + in conflict with other options that affect the file + mode, like fsGroup, and the result can be other mode + bits set.' + format: int32 + type: integer + path: + description: The relative path of the file to map the + key to. May not be an absolute path. May not contain + the path element '..'. May not start with the string + '..'. + type: string + required: + - key + - path + type: object + type: array + optional: + description: Specify whether the Secret or its keys must be + defined + type: boolean + secretName: + description: 'Name of the secret in the pod''s namespace to + use. More info: https://kubernetes.io/docs/concepts/storage/volumes#secret' + type: string + type: object + storageos: + description: StorageOS represents a StorageOS volume attached + and mounted on Kubernetes nodes. + properties: + fsType: + description: Filesystem type to mount. Must be a filesystem + type supported by the host operating system. Ex. "ext4", + "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + type: string + readOnly: + description: Defaults to false (read/write). ReadOnly here + will force the ReadOnly setting in VolumeMounts. + type: boolean + secretRef: + description: SecretRef specifies the secret to use for obtaining + the StorageOS API credentials. If not specified, default + values will be attempted. + properties: + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + type: object + volumeName: + description: VolumeName is the human-readable name of the + StorageOS volume. Volume names are only unique within a + namespace. + type: string + volumeNamespace: + description: VolumeNamespace specifies the scope of the volume + within StorageOS. If no namespace is specified then the + Pod's namespace will be used. This allows the Kubernetes + name scoping to be mirrored within StorageOS for tighter + integration. Set VolumeName to any name to override the + default behaviour. Set to "default" if you are not using + namespaces within StorageOS. Namespaces that do not pre-exist + within StorageOS will be created. + type: string + type: object + vsphereVolume: + description: VsphereVolume represents a vSphere volume attached + and mounted on kubelets host machine + properties: + fsType: + description: Filesystem type to mount. Must be a filesystem + type supported by the host operating system. Ex. "ext4", + "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. + type: string + storagePolicyID: + description: Storage Policy Based Management (SPBM) profile + ID associated with the StoragePolicyName. + type: string + storagePolicyName: + description: Storage Policy Based Management (SPBM) profile + name. + type: string + volumePath: + description: Path that identifies vSphere volume vmdk + type: string + required: + - volumePath + type: object + required: + - name + type: object + type: array + type: object + status: + description: StandaloneStatus defines the observed state of a Splunk Enterprise + standalone instances. + properties: + phase: + description: current phase of the standalone instances + enum: + - Pending + - Ready + - Updating + - ScalingUp + - ScalingDown + - Terminating + - Error + type: string + readyReplicas: + description: current number of ready standalone instances + format: int32 + type: integer + replicas: + description: number of desired standalone instances + format: int32 + type: integer + resourceRevMap: + additionalProperties: + type: string + description: Resource Revision tracker + type: object + selector: + description: selector for pods, used by HorizontalPodAutoscaler + type: string + smartstore: + description: Splunk Smartstore configuration. Refer to indexes.conf.spec + and server.conf.spec on docs.splunk.com + properties: + cacheManager: + description: Defines Cache manager settings + properties: + evictionPadding: + description: Additional size beyond 'minFreeSize' before eviction + kicks in + type: integer + evictionPolicy: + description: Eviction policy to use + type: string + hotlistBloomFilterRecencyHours: + description: Time period relative to the bucket's age, during + which the bloom filter file is protected from cache eviction + type: integer + hotlistRecencySecs: + description: Time period relative to the bucket's age, during + which the bucket is protected from cache eviction + type: integer + maxCacheSize: + description: Max cache size per partition + type: integer + maxConcurrentDownloads: + description: Maximum number of buckets that can be downloaded + from remote storage in parallel + type: integer + maxConcurrentUploads: + description: Maximum number of buckets that can be uploaded + to remote storage in parallel + type: integer + type: object + defaults: + description: Default configuration for indexes + properties: + maxGlobalDataSizeMB: + description: MaxGlobalDataSizeMB defines the maximum amount + of space for warm and cold buckets of an index + type: integer + maxGlobalRawDataSizeMB: + description: MaxGlobalDataSizeMB defines the maximum amount + of cumulative space for warm and cold buckets of an index + type: integer + volumeName: + description: Remote Volume name + type: string + type: object + indexes: + description: List of Splunk indexes + items: + description: IndexSpec defines Splunk index name and storage path + properties: + hotlistBloomFilterRecencyHours: + description: Time period relative to the bucket's age, during + which the bloom filter file is protected from cache eviction + type: integer + hotlistRecencySecs: + description: Time period relative to the bucket's age, during + which the bucket is protected from cache eviction + type: integer + maxGlobalDataSizeMB: + description: MaxGlobalDataSizeMB defines the maximum amount + of space for warm and cold buckets of an index + type: integer + maxGlobalRawDataSizeMB: + description: MaxGlobalDataSizeMB defines the maximum amount + of cumulative space for warm and cold buckets of an index + type: integer + name: + description: Splunk index name + type: string + remotePath: + description: Index location relative to the remote volume + path + type: string + volumeName: + description: Remote Volume name + type: string + type: object + type: array + volumes: + description: List of remote storage volumes + items: + description: VolumeSpec defines remote volume name and remote + volume URI + properties: + endpoint: + description: Remote volume URI + type: string + name: + description: Remote volume name + type: string + path: + description: Remote volume path + type: string + secretRef: + description: Secret object name + type: string + type: object + type: array + type: object + type: object + type: object + version: v1beta1 + versions: + - name: v1beta1 + served: true + storage: true + - name: v1alpha3 + served: true + storage: false + - name: v1alpha2 + served: true + storage: false diff --git a/deploy/olm-catalog/splunk/0.2.2/splunk.v0.2.2.clusterserviceversion.yaml b/deploy/olm-catalog/splunk/0.2.2/splunk.v0.2.2.clusterserviceversion.yaml new file mode 100644 index 000000000..34846f666 --- /dev/null +++ b/deploy/olm-catalog/splunk/0.2.2/splunk.v0.2.2.clusterserviceversion.yaml @@ -0,0 +1,262 @@ +apiVersion: operators.coreos.com/v1alpha1 +kind: ClusterServiceVersion +metadata: + annotations: + alm-examples: |- + [{ + "apiVersion": "enterprise.splunk.com/v1beta1", + "kind": "IndexerCluster", + "metadata": { + "name": "example", + "finalizers": [ "enterprise.splunk.com/delete-pvc" ] + }, + "spec": { + "replicas": 1 + } + }, + { + "apiVersion": "enterprise.splunk.com/v1beta1", + "kind": "LicenseMaster", + "metadata": { + "name": "example", + "finalizers": [ "enterprise.splunk.com/delete-pvc" ] + }, + "spec": {} + }, + { + "apiVersion": "enterprise.splunk.com/v1beta1", + "kind": "SearchHeadCluster", + "metadata": { + "name": "example", + "finalizers": [ "enterprise.splunk.com/delete-pvc" ] + }, + "spec": { + "replicas": 1 + } + }, + { + "apiVersion": "enterprise.splunk.com/v1beta1", + "kind": "Spark", + "metadata": { + "name": "example" + }, + "spec": { + "replicas": 1 + } + }, + { + "apiVersion": "enterprise.splunk.com/v1beta1", + "kind": "Standalone", + "metadata": { + "name": "example", + "finalizers": [ "enterprise.splunk.com/delete-pvc" ] + }, + "spec": {} + }] + capabilities: Basic Install + name: splunk.v0.2.2 + namespace: placeholder +spec: + apiservicedefinitions: {} + customresourcedefinitions: + owned: + - description: ClusterMaster is the Schema for the clustermasters API + kind: ClusterMaster + name: clustermasters.enterprise.splunk.com + version: v1beta1 + resources: + - kind: StatefulSets + version: apps/v1 + - kind: Deployments + version: apps/v1 + - kind: Pods + version: v1 + - kind: Services + version: v1 + - kind: ConfigMaps + version: v1 + - kind: Secrets + version: v1 + displayName: IndexerCluster + - description: IndexerCluster is the Schema for a Splunk Enterprise indexer cluster + kind: IndexerCluster + name: indexerclusters.enterprise.splunk.com + version: v1beta1 + resources: + - kind: StatefulSets + version: apps/v1 + - kind: Deployments + version: apps/v1 + - kind: Pods + version: v1 + - kind: Services + version: v1 + - kind: ConfigMaps + version: v1 + - kind: Secrets + version: v1 + displayName: LicenseMaster + - description: LicenseMaster is the Schema for a Splunk Enterprise license master. + kind: LicenseMaster + name: licensemasters.enterprise.splunk.com + version: v1beta1 + resources: + - kind: StatefulSets + version: apps/v1 + - kind: Deployments + version: apps/v1 + - kind: Pods + version: v1 + - kind: Services + version: v1 + - kind: ConfigMaps + version: v1 + - kind: Secrets + version: v1 + displayName: SearchHeadCluster + - description: SearchHeadCluster is the Schema for a Splunk Enterprise search + head cluster + kind: SearchHeadCluster + name: searchheadclusters.enterprise.splunk.com + version: v1beta1 + resources: + - kind: StatefulSets + version: apps/v1 + - kind: Deployments + version: apps/v1 + - kind: Pods + version: v1 + - kind: Services + version: v1 + - kind: ConfigMaps + version: v1 + - kind: Secrets + version: v1 + displayName: Spark + - description: Spark is the Schema for a Spark cluster + kind: Spark + name: sparks.enterprise.splunk.com + version: v1beta1 + resources: + - kind: StatefulSets + version: apps/v1 + - kind: Deployments + version: apps/v1 + - kind: Pods + version: v1 + - kind: Services + version: v1 + - kind: ConfigMaps + version: v1 + - kind: Secrets + version: v1 + displayName: Standalone + - description: Standalone is the Schema for a Splunk Enterprise standalone instances. + kind: Standalone + name: standalones.enterprise.splunk.com + version: v1beta1 + description: Placeholder description + displayName: Splunk + install: + spec: + deployments: + - name: splunk-operator + spec: + replicas: 1 + selector: + matchLabels: + name: splunk-operator + strategy: {} + template: + metadata: + labels: + name: splunk-operator + spec: + containers: + - env: + - name: WATCH_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.annotations['olm.targetNamespaces'] + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: OPERATOR_NAME + value: splunk-operator + - name: RELATED_IMAGE_SPLUNK_ENTERPRISE + value: docker.io/splunk/splunk:8.1.2 + - name: RELATED_IMAGE_SPLUNK_SPARK + value: docker.io/splunk/spark:0.0.2 + image: docker.io/splunk/splunk-operator:0.2.2 + imagePullPolicy: IfNotPresent + name: splunk-operator + resources: {} + serviceAccountName: splunk-operator + permissions: + - rules: + - apiGroups: + - "" + resources: + - services + - endpoints + - persistentvolumeclaims + - configmaps + - secrets + - pods + - pods/exec + - serviceaccounts + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch + - apiGroups: + - "" + resources: + - events + verbs: + - get + - list + - watch + - apiGroups: + - apps + resources: + - deployments + - daemonsets + - replicasets + - statefulsets + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch + - apiGroups: + - enterprise.splunk.com + resources: + - '*' + verbs: + - '*' + serviceAccountName: splunk-operator + strategy: deployment + installModes: + - supported: true + type: OwnNamespace + - supported: true + type: SingleNamespace + - supported: false + type: MultiNamespace + - supported: true + type: AllNamespaces + maturity: alpha + provider: {} + replaces: splunk.v0.0.0 + version: 0.2.2 diff --git a/deploy/operator.yaml b/deploy/operator.yaml index 701e6ba53..99f490d04 100644 --- a/deploy/operator.yaml +++ b/deploy/operator.yaml @@ -30,6 +30,6 @@ spec: - name: OPERATOR_NAME value: "splunk-operator" - name: RELATED_IMAGE_SPLUNK_ENTERPRISE - value: "docker.io/splunk/splunk:8.1.1" + value: "docker.io/splunk/splunk:8.1.2" - name: RELATED_IMAGE_SPLUNK_SPARK value: "docker.io/splunk/spark:0.0.2" diff --git a/docs/ChangeLog.md b/docs/ChangeLog.md index 47fba97e6..b44cb5b13 100644 --- a/docs/ChangeLog.md +++ b/docs/ChangeLog.md @@ -1,5 +1,28 @@ # Splunk Operator for Kubernetes Change Log +## 0.2.2 Beta(2021-02-09) +* This release depends upon changes made concurrently in the Splunk Enterprise container images. You should use the splunk/splunk:8.1.2 image with it, or alternatively any release version 8.1.0 or later + +* This release updates the CRDs which may require updates to the custom resources being used in existing deployments + +* CSPL-526 - Enhanced ingress documentation with guidelines on ingesting data into the K8S cluster using ingress controllers(istio, nginx) + +* CSPL-564 - Changed the way licenseMasterRef is configured on the ClusterMaster and IndexerCluster CRDs + +* CSPL-609 - Added a shortname stdaln for the Standalone CRD + +* CSPL-637 - Updated Splunk port names to conform with Istio ingress controllers convention + +* CSPL-660 - Separated storage class specifications for etc and var volumes + +* CSPL-663 - Optimize deployment of Splunk apps on SHC using new parameter defaultsUrlApps + +* CSPL-694 - Avoid unnecessary pod resets + +* CSPL-720 - Added support to configure a custom service account per Splunk Enterprise CRD + +* CSPL-721 - Mounted etc and var as emptyDirs volumes on the monitoring console + ## 0.2.1 Beta(2020-12-15) * This release depends upon changes made concurrently in the Splunk Enterprise container images. You must use the latest splunk/splunk:edge nightly image with it, or alternatively any release version 8.1.0 or later diff --git a/docs/Install.md b/docs/Install.md index f3469c4b7..c32740bdc 100644 --- a/docs/Install.md +++ b/docs/Install.md @@ -11,7 +11,7 @@ Splunk Operator (as described below), please download a local copy of the installation YAML and open it in your favorite editor. ``` -wget -O splunk-operator.yaml https://github.com/splunk/splunk-operator/releases/download/0.2.1/splunk-operator-install.yaml +wget -O splunk-operator.yaml https://github.com/splunk/splunk-operator/releases/download/0.2.2/splunk-operator-install.yaml ``` @@ -23,14 +23,14 @@ installed by regular users within their own namespaces. If you are not an administrator, you can have someone else create these objects for you by running ``` -kubectl apply -f https://github.com/splunk/splunk-operator/releases/download/0.2.1/splunk-operator-crds.yaml +kubectl apply -f https://github.com/splunk/splunk-operator/releases/download/0.2.2/splunk-operator-crds.yaml ``` You should then be able download and use the following YAML to install the operator within your own namespace: ``` -wget -O splunk-operator.yaml https://github.com/splunk/splunk-operator/releases/download/0.2.1/splunk-operator-noadmin.yaml +wget -O splunk-operator.yaml https://github.com/splunk/splunk-operator/releases/download/0.2.2/splunk-operator-noadmin.yaml kubectl config set-context --current --namespace= kubectl apply -f splunk-operator.yaml ``` @@ -43,7 +43,7 @@ objects for all the namespaces of your cluster, you can use the alternative cluster scope installation YAML: ``` -wget -O splunk-operator.yaml https://github.com/splunk/splunk-operator/releases/download/0.2.1/splunk-operator-cluster.yaml +wget -O splunk-operator.yaml https://github.com/splunk/splunk-operator/releases/download/0.2.2/splunk-operator-cluster.yaml ``` When running at cluster scope, you will need to bind the diff --git a/docs/README.md b/docs/README.md index 61ad4df70..cc7fd03cd 100644 --- a/docs/README.md +++ b/docs/README.md @@ -86,7 +86,7 @@ information. Most users can install and start the Splunk Operator by just running ``` -kubectl apply -f https://github.com/splunk/splunk-operator/releases/download/0.2.1/splunk-operator-install.yaml +kubectl apply -f https://github.com/splunk/splunk-operator/releases/download/0.2.2/splunk-operator-install.yaml ``` Users of Red Hat OpenShift should read the additional @@ -120,7 +120,7 @@ kubectl delete searchheadclusters --all kubectl delete clustermasters --all kubectl delete indexerclusters --all kubectl delete spark --all -kubectl delete -f https://github.com/splunk/splunk-operator/releases/download/0.2.1/splunk-operator-install.yaml +kubectl delete -f https://github.com/splunk/splunk-operator/releases/download/0.2.2/splunk-operator-install.yaml ``` diff --git a/version/version.go b/version/version.go index cd725763c..c5439690e 100644 --- a/version/version.go +++ b/version/version.go @@ -16,5 +16,5 @@ package version var ( // Version of splunk-operator - Version = "0.2.1" + Version = "0.2.2" )