From e71818195b846ea93fb2d5ece882e2ce83692982 Mon Sep 17 00:00:00 2001 From: Mike Dickey Date: Tue, 3 Mar 2020 16:12:41 -0800 Subject: [PATCH 1/7] Implemented logic for the operator to take a more active role in managing updates and scaling for all custom resources. The status fields for all CRDs are now actively managed by the operator, and have been added as print lines so that more information is shown when you run for example "kubectl get searchhead": NAME PHASE DEPLOYER DESIRED READY AGE s1 Ready Ready 3 3 1d The kubectl scale command should not work with all CRs (except for LicenseMaster, since there is only 1) --- deploy/all-in-one-cluster.yaml | 268 +++++++++++++----- deploy/all-in-one-scoped.yaml | 268 +++++++++++++----- deploy/cluster_operator.yaml | 2 +- deploy/crds/combined.yaml | 266 ++++++++++++----- .../enterprise.splunk.com_indexers_crd.yaml | 67 +++-- ...erprise.splunk.com_licensemasters_crd.yaml | 18 +- ...enterprise.splunk.com_searchheads_crd.yaml | 64 ++++- .../enterprise.splunk.com_sparks_crd.yaml | 68 ++++- ...enterprise.splunk.com_standalones_crd.yaml | 49 +++- deploy/operator.yaml | 2 +- pkg/apis/enterprise/v1alpha2/common_types.go | 27 ++ pkg/apis/enterprise/v1alpha2/indexer_types.go | 27 +- .../v1alpha2/licensemaster_types.go | 6 +- .../enterprise/v1alpha2/searchhead_types.go | 26 +- pkg/apis/enterprise/v1alpha2/spark_types.go | 28 +- .../enterprise/v1alpha2/standalone_types.go | 22 +- pkg/splunk/enterprise/configuration.go | 14 +- pkg/splunk/enterprise/configuration_test.go | 28 +- pkg/splunk/enterprise/names.go | 8 +- pkg/splunk/enterprise/names_test.go | 2 +- pkg/splunk/reconcile/deployment.go | 83 +++--- pkg/splunk/reconcile/deployment_test.go | 80 +++++- pkg/splunk/reconcile/indexer.go | 36 ++- pkg/splunk/reconcile/licensemaster.go | 25 +- pkg/splunk/reconcile/searchhead.go | 35 ++- pkg/splunk/reconcile/spark.go | 24 +- pkg/splunk/reconcile/standalone.go | 29 +- pkg/splunk/reconcile/statefulset.go | 144 +++++++--- pkg/splunk/reconcile/statefulset_test.go | 5 +- pkg/splunk/reconcile/util.go | 111 +++++--- pkg/splunk/reconcile/util_test.go | 23 +- 31 files changed, 1372 insertions(+), 483 deletions(-) diff --git a/deploy/all-in-one-cluster.yaml b/deploy/all-in-one-cluster.yaml index 7525dc2b6..34b2e76ba 100644 --- a/deploy/all-in-one-cluster.yaml +++ b/deploy/all-in-one-cluster.yaml @@ -5,14 +5,22 @@ metadata: name: standalones.enterprise.splunk.com spec: additionalPrinterColumns: - - JSONPath: .spec.status.phase + - JSONPath: .status.phase description: Status of standalone instances name: Phase - type: integer - - JSONPath: .spec.status.instances - description: Number of standalone instances - name: Instances 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 @@ -21,6 +29,10 @@ spec: singular: standalone scope: Namespaced subresources: + scale: + labelSelectorPath: .status.selector + specReplicasPath: .spec.replicas + statusReplicasPath: .status.replicas status: {} validation: openAPIV3Schema: @@ -717,6 +729,7 @@ spec: type: string replicas: description: Number of standalone pods + format: int32 type: integer resources: description: resource requirements for the pod containers @@ -2211,17 +2224,27 @@ spec: description: StandaloneStatus defines the observed state of a Splunk Enterprise standalone instances. properties: - instances: - description: current number of standalone instances - type: integer phase: description: current phase of the standalone instances enum: - - pending - - ready - - scaleup - - scaledown - - updating + - 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 + selector: + description: selector for pods, used by HorizontalPodAutoscaler type: string type: object type: object @@ -2237,10 +2260,14 @@ metadata: name: licensemasters.enterprise.splunk.com spec: additionalPrinterColumns: - - JSONPath: .spec.status.phase + - JSONPath: .status.phase description: Status of license master name: Phase - type: integer + type: string + - JSONPath: .metadata.creationTimestamp + description: Age of license master + name: Age + type: date group: enterprise.splunk.com names: kind: LicenseMaster @@ -4399,9 +4426,13 @@ spec: phase: description: current phase of the license master enum: - - pending - - ready - - updating + - Pending + - Ready + - Updating + - ScalingUp + - ScalingDown + - Terminating + - Error type: string type: object type: object @@ -4417,14 +4448,26 @@ metadata: name: searchheads.enterprise.splunk.com spec: additionalPrinterColumns: - - JSONPath: .spec.status.phase + - JSONPath: .status.phase description: Status of search head cluster name: Phase - type: integer - - JSONPath: .spec.status.instances - description: Number of search heads - name: Instances type: string + - JSONPath: .status.deployerPhase + description: Status of the deployer + name: Deployer + type: string + - JSONPath: .status.replicas + description: Number of desired search head replicas + name: Desired + type: integer + - JSONPath: .status.readyReplicas + description: Current number of ready search head replicas + name: Ready + type: integer + - JSONPath: .metadata.creationTimestamp + description: Age of search head cluster + name: Age + type: date group: enterprise.splunk.com names: kind: SearchHead @@ -4436,6 +4479,10 @@ spec: singular: searchhead scope: Namespaced subresources: + scale: + labelSelectorPath: .status.selector + specReplicasPath: .spec.replicas + statusReplicasPath: .status.replicas status: {} validation: openAPIV3Schema: @@ -5134,6 +5181,7 @@ spec: 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 @@ -6628,17 +6676,38 @@ spec: description: SearchHeadStatus defines the observed state of a Splunk Enterprise standalone search head or cluster of search heads properties: - instances: - description: current number of search head instances - type: integer + deployerPhase: + description: current phase of the deployer + enum: + - Pending + - Ready + - Updating + - ScalingUp + - ScalingDown + - Terminating + - Error + type: string phase: description: current phase of the search head cluster enum: - - pending - - ready - - scaleup - - scaledown - - updating + - Pending + - Ready + - Updating + - ScalingUp + - ScalingDown + - Terminating + - Error + type: string + readyReplicas: + description: current number of ready search head replicas + format: int32 + type: integer + replicas: + description: number of desired search head replicas + format: int32 + type: integer + selector: + description: selector for pods, used by HorizontalPodAutoscaler type: string type: object type: object @@ -6654,18 +6723,26 @@ metadata: name: indexers.enterprise.splunk.com spec: additionalPrinterColumns: - - JSONPath: .spec.status.phase + - JSONPath: .status.phase description: Status of indexer cluster name: Phase - type: integer - - JSONPath: .spec.status.clusterMasterPhase - description: Status of cluster master - name: CM type: string - - JSONPath: .spec.status.instances - description: Number of indexers - name: Instances + - JSONPath: .status.clusterMasterPhase + description: Status of cluster master + name: Master type: string + - JSONPath: .status.replicas + description: Number of desired 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: Indexer @@ -6676,6 +6753,10 @@ spec: singular: indexer scope: Namespaced subresources: + scale: + labelSelectorPath: .status.selector + specReplicasPath: .spec.replicas + statusReplicasPath: .status.replicas status: {} validation: openAPIV3Schema: @@ -7374,6 +7455,7 @@ spec: 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 @@ -8829,23 +8911,35 @@ spec: clusterMasterPhase: description: current phase of the cluster master enum: - - pending - - ready - - scaleup - - scaledown - - updating + - Pending + - Ready + - Updating + - ScalingUp + - ScalingDown + - Terminating + - Error type: string - instances: - description: current number of indexer instances - type: integer phase: description: current phase of the indexer cluster enum: - - pending - - ready - - scaleup - - scaledown - - updating + - Pending + - Ready + - Updating + - ScalingUp + - ScalingDown + - Terminating + - Error + type: string + readyReplicas: + description: current number of ready indexer peers + format: int32 + type: integer + replicas: + description: number of desired indexer peers + format: int32 + type: integer + selector: + description: selector for pods, used by HorizontalPodAutoscaler type: string type: object type: object @@ -8861,14 +8955,26 @@ metadata: name: sparks.enterprise.splunk.com spec: additionalPrinterColumns: - - JSONPath: .spec.status.phase - description: Status of Spark cluster + - JSONPath: .status.phase + description: Status of Spark workers name: Phase - type: integer - - JSONPath: .spec.status.instances - description: Number of Spark workers - name: Instances 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 @@ -8877,6 +8983,10 @@ spec: singular: spark scope: Namespaced subresources: + scale: + labelSelectorPath: .status.selector + specReplicasPath: .spec.replicas + statusReplicasPath: .status.replicas status: {} validation: openAPIV3Schema: @@ -9483,6 +9593,7 @@ spec: type: string replicas: description: Number of spark worker pods + format: int32 type: integer resources: description: resource requirements for the pod containers @@ -9748,17 +9859,38 @@ spec: status: description: SparkStatus defines the observed state of a Spark cluster properties: - instances: - description: current number of spark worker instances - type: integer + 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 cluster + description: current phase of the spark workers enum: - - pending - - ready - - scaleup - - scaledown - - updating + - 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 @@ -9904,6 +10036,6 @@ spec: - name: OPERATOR_NAME value: "splunk-operator" - name: SPLUNK_IMAGE - value: "splunk/splunk:8.0" + value: "splunk/splunk:edge" - name: SPARK_IMAGE value: "splunk/spark" diff --git a/deploy/all-in-one-scoped.yaml b/deploy/all-in-one-scoped.yaml index 78e166b3d..b8357047e 100644 --- a/deploy/all-in-one-scoped.yaml +++ b/deploy/all-in-one-scoped.yaml @@ -5,14 +5,22 @@ metadata: name: standalones.enterprise.splunk.com spec: additionalPrinterColumns: - - JSONPath: .spec.status.phase + - JSONPath: .status.phase description: Status of standalone instances name: Phase - type: integer - - JSONPath: .spec.status.instances - description: Number of standalone instances - name: Instances 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 @@ -21,6 +29,10 @@ spec: singular: standalone scope: Namespaced subresources: + scale: + labelSelectorPath: .status.selector + specReplicasPath: .spec.replicas + statusReplicasPath: .status.replicas status: {} validation: openAPIV3Schema: @@ -717,6 +729,7 @@ spec: type: string replicas: description: Number of standalone pods + format: int32 type: integer resources: description: resource requirements for the pod containers @@ -2211,17 +2224,27 @@ spec: description: StandaloneStatus defines the observed state of a Splunk Enterprise standalone instances. properties: - instances: - description: current number of standalone instances - type: integer phase: description: current phase of the standalone instances enum: - - pending - - ready - - scaleup - - scaledown - - updating + - 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 + selector: + description: selector for pods, used by HorizontalPodAutoscaler type: string type: object type: object @@ -2237,10 +2260,14 @@ metadata: name: licensemasters.enterprise.splunk.com spec: additionalPrinterColumns: - - JSONPath: .spec.status.phase + - JSONPath: .status.phase description: Status of license master name: Phase - type: integer + type: string + - JSONPath: .metadata.creationTimestamp + description: Age of license master + name: Age + type: date group: enterprise.splunk.com names: kind: LicenseMaster @@ -4399,9 +4426,13 @@ spec: phase: description: current phase of the license master enum: - - pending - - ready - - updating + - Pending + - Ready + - Updating + - ScalingUp + - ScalingDown + - Terminating + - Error type: string type: object type: object @@ -4417,14 +4448,26 @@ metadata: name: searchheads.enterprise.splunk.com spec: additionalPrinterColumns: - - JSONPath: .spec.status.phase + - JSONPath: .status.phase description: Status of search head cluster name: Phase - type: integer - - JSONPath: .spec.status.instances - description: Number of search heads - name: Instances type: string + - JSONPath: .status.deployerPhase + description: Status of the deployer + name: Deployer + type: string + - JSONPath: .status.replicas + description: Number of desired search head replicas + name: Desired + type: integer + - JSONPath: .status.readyReplicas + description: Current number of ready search head replicas + name: Ready + type: integer + - JSONPath: .metadata.creationTimestamp + description: Age of search head cluster + name: Age + type: date group: enterprise.splunk.com names: kind: SearchHead @@ -4436,6 +4479,10 @@ spec: singular: searchhead scope: Namespaced subresources: + scale: + labelSelectorPath: .status.selector + specReplicasPath: .spec.replicas + statusReplicasPath: .status.replicas status: {} validation: openAPIV3Schema: @@ -5134,6 +5181,7 @@ spec: 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 @@ -6628,17 +6676,38 @@ spec: description: SearchHeadStatus defines the observed state of a Splunk Enterprise standalone search head or cluster of search heads properties: - instances: - description: current number of search head instances - type: integer + deployerPhase: + description: current phase of the deployer + enum: + - Pending + - Ready + - Updating + - ScalingUp + - ScalingDown + - Terminating + - Error + type: string phase: description: current phase of the search head cluster enum: - - pending - - ready - - scaleup - - scaledown - - updating + - Pending + - Ready + - Updating + - ScalingUp + - ScalingDown + - Terminating + - Error + type: string + readyReplicas: + description: current number of ready search head replicas + format: int32 + type: integer + replicas: + description: number of desired search head replicas + format: int32 + type: integer + selector: + description: selector for pods, used by HorizontalPodAutoscaler type: string type: object type: object @@ -6654,18 +6723,26 @@ metadata: name: indexers.enterprise.splunk.com spec: additionalPrinterColumns: - - JSONPath: .spec.status.phase + - JSONPath: .status.phase description: Status of indexer cluster name: Phase - type: integer - - JSONPath: .spec.status.clusterMasterPhase - description: Status of cluster master - name: CM type: string - - JSONPath: .spec.status.instances - description: Number of indexers - name: Instances + - JSONPath: .status.clusterMasterPhase + description: Status of cluster master + name: Master type: string + - JSONPath: .status.replicas + description: Number of desired 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: Indexer @@ -6676,6 +6753,10 @@ spec: singular: indexer scope: Namespaced subresources: + scale: + labelSelectorPath: .status.selector + specReplicasPath: .spec.replicas + statusReplicasPath: .status.replicas status: {} validation: openAPIV3Schema: @@ -7374,6 +7455,7 @@ spec: 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 @@ -8829,23 +8911,35 @@ spec: clusterMasterPhase: description: current phase of the cluster master enum: - - pending - - ready - - scaleup - - scaledown - - updating + - Pending + - Ready + - Updating + - ScalingUp + - ScalingDown + - Terminating + - Error type: string - instances: - description: current number of indexer instances - type: integer phase: description: current phase of the indexer cluster enum: - - pending - - ready - - scaleup - - scaledown - - updating + - Pending + - Ready + - Updating + - ScalingUp + - ScalingDown + - Terminating + - Error + type: string + readyReplicas: + description: current number of ready indexer peers + format: int32 + type: integer + replicas: + description: number of desired indexer peers + format: int32 + type: integer + selector: + description: selector for pods, used by HorizontalPodAutoscaler type: string type: object type: object @@ -8861,14 +8955,26 @@ metadata: name: sparks.enterprise.splunk.com spec: additionalPrinterColumns: - - JSONPath: .spec.status.phase - description: Status of Spark cluster + - JSONPath: .status.phase + description: Status of Spark workers name: Phase - type: integer - - JSONPath: .spec.status.instances - description: Number of Spark workers - name: Instances 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 @@ -8877,6 +8983,10 @@ spec: singular: spark scope: Namespaced subresources: + scale: + labelSelectorPath: .status.selector + specReplicasPath: .spec.replicas + statusReplicasPath: .status.replicas status: {} validation: openAPIV3Schema: @@ -9483,6 +9593,7 @@ spec: type: string replicas: description: Number of spark worker pods + format: int32 type: integer resources: description: resource requirements for the pod containers @@ -9748,17 +9859,38 @@ spec: status: description: SparkStatus defines the observed state of a Spark cluster properties: - instances: - description: current number of spark worker instances - type: integer + 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 cluster + description: current phase of the spark workers enum: - - pending - - ready - - scaleup - - scaledown - - updating + - 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 @@ -9884,6 +10016,6 @@ spec: - name: OPERATOR_NAME value: "splunk-operator" - name: SPLUNK_IMAGE - value: "splunk/splunk:8.0" + value: "splunk/splunk:edge" - name: SPARK_IMAGE value: "splunk/spark" diff --git a/deploy/cluster_operator.yaml b/deploy/cluster_operator.yaml index 2938a7dfb..a6317bb96 100644 --- a/deploy/cluster_operator.yaml +++ b/deploy/cluster_operator.yaml @@ -67,6 +67,6 @@ spec: - name: OPERATOR_NAME value: "splunk-operator" - name: SPLUNK_IMAGE - value: "splunk/splunk:8.0" + value: "splunk/splunk:edge" - name: SPARK_IMAGE value: "splunk/spark" diff --git a/deploy/crds/combined.yaml b/deploy/crds/combined.yaml index 641cfc0d3..47d078388 100644 --- a/deploy/crds/combined.yaml +++ b/deploy/crds/combined.yaml @@ -5,14 +5,22 @@ metadata: name: standalones.enterprise.splunk.com spec: additionalPrinterColumns: - - JSONPath: .spec.status.phase + - JSONPath: .status.phase description: Status of standalone instances name: Phase - type: integer - - JSONPath: .spec.status.instances - description: Number of standalone instances - name: Instances 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 @@ -21,6 +29,10 @@ spec: singular: standalone scope: Namespaced subresources: + scale: + labelSelectorPath: .status.selector + specReplicasPath: .spec.replicas + statusReplicasPath: .status.replicas status: {} validation: openAPIV3Schema: @@ -717,6 +729,7 @@ spec: type: string replicas: description: Number of standalone pods + format: int32 type: integer resources: description: resource requirements for the pod containers @@ -2211,17 +2224,27 @@ spec: description: StandaloneStatus defines the observed state of a Splunk Enterprise standalone instances. properties: - instances: - description: current number of standalone instances - type: integer phase: description: current phase of the standalone instances enum: - - pending - - ready - - scaleup - - scaledown - - updating + - 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 + selector: + description: selector for pods, used by HorizontalPodAutoscaler type: string type: object type: object @@ -2237,10 +2260,14 @@ metadata: name: licensemasters.enterprise.splunk.com spec: additionalPrinterColumns: - - JSONPath: .spec.status.phase + - JSONPath: .status.phase description: Status of license master name: Phase - type: integer + type: string + - JSONPath: .metadata.creationTimestamp + description: Age of license master + name: Age + type: date group: enterprise.splunk.com names: kind: LicenseMaster @@ -4399,9 +4426,13 @@ spec: phase: description: current phase of the license master enum: - - pending - - ready - - updating + - Pending + - Ready + - Updating + - ScalingUp + - ScalingDown + - Terminating + - Error type: string type: object type: object @@ -4417,14 +4448,26 @@ metadata: name: searchheads.enterprise.splunk.com spec: additionalPrinterColumns: - - JSONPath: .spec.status.phase + - JSONPath: .status.phase description: Status of search head cluster name: Phase - type: integer - - JSONPath: .spec.status.instances - description: Number of search heads - name: Instances type: string + - JSONPath: .status.deployerPhase + description: Status of the deployer + name: Deployer + type: string + - JSONPath: .status.replicas + description: Number of desired search head replicas + name: Desired + type: integer + - JSONPath: .status.readyReplicas + description: Current number of ready search head replicas + name: Ready + type: integer + - JSONPath: .metadata.creationTimestamp + description: Age of search head cluster + name: Age + type: date group: enterprise.splunk.com names: kind: SearchHead @@ -4436,6 +4479,10 @@ spec: singular: searchhead scope: Namespaced subresources: + scale: + labelSelectorPath: .status.selector + specReplicasPath: .spec.replicas + statusReplicasPath: .status.replicas status: {} validation: openAPIV3Schema: @@ -5134,6 +5181,7 @@ spec: 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 @@ -6628,17 +6676,38 @@ spec: description: SearchHeadStatus defines the observed state of a Splunk Enterprise standalone search head or cluster of search heads properties: - instances: - description: current number of search head instances - type: integer + deployerPhase: + description: current phase of the deployer + enum: + - Pending + - Ready + - Updating + - ScalingUp + - ScalingDown + - Terminating + - Error + type: string phase: description: current phase of the search head cluster enum: - - pending - - ready - - scaleup - - scaledown - - updating + - Pending + - Ready + - Updating + - ScalingUp + - ScalingDown + - Terminating + - Error + type: string + readyReplicas: + description: current number of ready search head replicas + format: int32 + type: integer + replicas: + description: number of desired search head replicas + format: int32 + type: integer + selector: + description: selector for pods, used by HorizontalPodAutoscaler type: string type: object type: object @@ -6654,18 +6723,26 @@ metadata: name: indexers.enterprise.splunk.com spec: additionalPrinterColumns: - - JSONPath: .spec.status.phase + - JSONPath: .status.phase description: Status of indexer cluster name: Phase - type: integer - - JSONPath: .spec.status.clusterMasterPhase - description: Status of cluster master - name: CM type: string - - JSONPath: .spec.status.instances - description: Number of indexers - name: Instances + - JSONPath: .status.clusterMasterPhase + description: Status of cluster master + name: Master type: string + - JSONPath: .status.replicas + description: Number of desired 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: Indexer @@ -6676,6 +6753,10 @@ spec: singular: indexer scope: Namespaced subresources: + scale: + labelSelectorPath: .status.selector + specReplicasPath: .spec.replicas + statusReplicasPath: .status.replicas status: {} validation: openAPIV3Schema: @@ -7374,6 +7455,7 @@ spec: 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 @@ -8829,23 +8911,35 @@ spec: clusterMasterPhase: description: current phase of the cluster master enum: - - pending - - ready - - scaleup - - scaledown - - updating + - Pending + - Ready + - Updating + - ScalingUp + - ScalingDown + - Terminating + - Error type: string - instances: - description: current number of indexer instances - type: integer phase: description: current phase of the indexer cluster enum: - - pending - - ready - - scaleup - - scaledown - - updating + - Pending + - Ready + - Updating + - ScalingUp + - ScalingDown + - Terminating + - Error + type: string + readyReplicas: + description: current number of ready indexer peers + format: int32 + type: integer + replicas: + description: number of desired indexer peers + format: int32 + type: integer + selector: + description: selector for pods, used by HorizontalPodAutoscaler type: string type: object type: object @@ -8861,14 +8955,26 @@ metadata: name: sparks.enterprise.splunk.com spec: additionalPrinterColumns: - - JSONPath: .spec.status.phase - description: Status of Spark cluster + - JSONPath: .status.phase + description: Status of Spark workers name: Phase - type: integer - - JSONPath: .spec.status.instances - description: Number of Spark workers - name: Instances 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 @@ -8877,6 +8983,10 @@ spec: singular: spark scope: Namespaced subresources: + scale: + labelSelectorPath: .status.selector + specReplicasPath: .spec.replicas + statusReplicasPath: .status.replicas status: {} validation: openAPIV3Schema: @@ -9483,6 +9593,7 @@ spec: type: string replicas: description: Number of spark worker pods + format: int32 type: integer resources: description: resource requirements for the pod containers @@ -9748,17 +9859,38 @@ spec: status: description: SparkStatus defines the observed state of a Spark cluster properties: - instances: - description: current number of spark worker instances - type: integer + 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 cluster + description: current phase of the spark workers enum: - - pending - - ready - - scaleup - - scaledown - - updating + - 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 diff --git a/deploy/crds/enterprise.splunk.com_indexers_crd.yaml b/deploy/crds/enterprise.splunk.com_indexers_crd.yaml index 70b60c7a3..b0d9bd857 100644 --- a/deploy/crds/enterprise.splunk.com_indexers_crd.yaml +++ b/deploy/crds/enterprise.splunk.com_indexers_crd.yaml @@ -4,18 +4,26 @@ metadata: name: indexers.enterprise.splunk.com spec: additionalPrinterColumns: - - JSONPath: .spec.status.phase + - JSONPath: .status.phase description: Status of indexer cluster name: Phase - type: integer - - JSONPath: .spec.status.clusterMasterPhase - description: Status of cluster master - name: CM type: string - - JSONPath: .spec.status.instances - description: Number of indexers - name: Instances + - JSONPath: .status.clusterMasterPhase + description: Status of cluster master + name: Master type: string + - JSONPath: .status.replicas + description: Number of desired 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: Indexer @@ -26,6 +34,10 @@ spec: singular: indexer scope: Namespaced subresources: + scale: + labelSelectorPath: .status.selector + specReplicasPath: .spec.replicas + statusReplicasPath: .status.replicas status: {} validation: openAPIV3Schema: @@ -724,6 +736,7 @@ spec: 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 @@ -2179,23 +2192,35 @@ spec: clusterMasterPhase: description: current phase of the cluster master enum: - - pending - - ready - - scaleup - - scaledown - - updating + - Pending + - Ready + - Updating + - ScalingUp + - ScalingDown + - Terminating + - Error type: string - instances: - description: current number of indexer instances - type: integer phase: description: current phase of the indexer cluster enum: - - pending - - ready - - scaleup - - scaledown - - updating + - Pending + - Ready + - Updating + - ScalingUp + - ScalingDown + - Terminating + - Error + type: string + readyReplicas: + description: current number of ready indexer peers + format: int32 + type: integer + replicas: + description: number of desired indexer peers + format: int32 + type: integer + selector: + description: selector for pods, used by HorizontalPodAutoscaler type: string type: object type: object diff --git a/deploy/crds/enterprise.splunk.com_licensemasters_crd.yaml b/deploy/crds/enterprise.splunk.com_licensemasters_crd.yaml index f9ec6e20d..ffca31f4d 100644 --- a/deploy/crds/enterprise.splunk.com_licensemasters_crd.yaml +++ b/deploy/crds/enterprise.splunk.com_licensemasters_crd.yaml @@ -4,10 +4,14 @@ metadata: name: licensemasters.enterprise.splunk.com spec: additionalPrinterColumns: - - JSONPath: .spec.status.phase + - JSONPath: .status.phase description: Status of license master name: Phase - type: integer + type: string + - JSONPath: .metadata.creationTimestamp + description: Age of license master + name: Age + type: date group: enterprise.splunk.com names: kind: LicenseMaster @@ -2166,9 +2170,13 @@ spec: phase: description: current phase of the license master enum: - - pending - - ready - - updating + - Pending + - Ready + - Updating + - ScalingUp + - ScalingDown + - Terminating + - Error type: string type: object type: object diff --git a/deploy/crds/enterprise.splunk.com_searchheads_crd.yaml b/deploy/crds/enterprise.splunk.com_searchheads_crd.yaml index 6f2d8e9be..108df8d6a 100644 --- a/deploy/crds/enterprise.splunk.com_searchheads_crd.yaml +++ b/deploy/crds/enterprise.splunk.com_searchheads_crd.yaml @@ -4,14 +4,26 @@ metadata: name: searchheads.enterprise.splunk.com spec: additionalPrinterColumns: - - JSONPath: .spec.status.phase + - JSONPath: .status.phase description: Status of search head cluster name: Phase - type: integer - - JSONPath: .spec.status.instances - description: Number of search heads - name: Instances type: string + - JSONPath: .status.deployerPhase + description: Status of the deployer + name: Deployer + type: string + - JSONPath: .status.replicas + description: Number of desired search head replicas + name: Desired + type: integer + - JSONPath: .status.readyReplicas + description: Current number of ready search head replicas + name: Ready + type: integer + - JSONPath: .metadata.creationTimestamp + description: Age of search head cluster + name: Age + type: date group: enterprise.splunk.com names: kind: SearchHead @@ -23,6 +35,10 @@ spec: singular: searchhead scope: Namespaced subresources: + scale: + labelSelectorPath: .status.selector + specReplicasPath: .spec.replicas + statusReplicasPath: .status.replicas status: {} validation: openAPIV3Schema: @@ -721,6 +737,7 @@ spec: 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 @@ -2215,17 +2232,38 @@ spec: description: SearchHeadStatus defines the observed state of a Splunk Enterprise standalone search head or cluster of search heads properties: - instances: - description: current number of search head instances - type: integer + deployerPhase: + description: current phase of the deployer + enum: + - Pending + - Ready + - Updating + - ScalingUp + - ScalingDown + - Terminating + - Error + type: string phase: description: current phase of the search head cluster enum: - - pending - - ready - - scaleup - - scaledown - - updating + - Pending + - Ready + - Updating + - ScalingUp + - ScalingDown + - Terminating + - Error + type: string + readyReplicas: + description: current number of ready search head replicas + format: int32 + type: integer + replicas: + description: number of desired search head replicas + format: int32 + type: integer + selector: + description: selector for pods, used by HorizontalPodAutoscaler type: string type: object type: object diff --git a/deploy/crds/enterprise.splunk.com_sparks_crd.yaml b/deploy/crds/enterprise.splunk.com_sparks_crd.yaml index 5e363c170..20e99df96 100644 --- a/deploy/crds/enterprise.splunk.com_sparks_crd.yaml +++ b/deploy/crds/enterprise.splunk.com_sparks_crd.yaml @@ -4,14 +4,26 @@ metadata: name: sparks.enterprise.splunk.com spec: additionalPrinterColumns: - - JSONPath: .spec.status.phase - description: Status of Spark cluster + - JSONPath: .status.phase + description: Status of Spark workers name: Phase - type: integer - - JSONPath: .spec.status.instances - description: Number of Spark workers - name: Instances 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 @@ -20,6 +32,10 @@ spec: singular: spark scope: Namespaced subresources: + scale: + labelSelectorPath: .status.selector + specReplicasPath: .spec.replicas + statusReplicasPath: .status.replicas status: {} validation: openAPIV3Schema: @@ -626,6 +642,7 @@ spec: type: string replicas: description: Number of spark worker pods + format: int32 type: integer resources: description: resource requirements for the pod containers @@ -891,17 +908,38 @@ spec: status: description: SparkStatus defines the observed state of a Spark cluster properties: - instances: - description: current number of spark worker instances - type: integer + 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 cluster + description: current phase of the spark workers enum: - - pending - - ready - - scaleup - - scaledown - - updating + - 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 diff --git a/deploy/crds/enterprise.splunk.com_standalones_crd.yaml b/deploy/crds/enterprise.splunk.com_standalones_crd.yaml index afab066d0..2f601b7cd 100644 --- a/deploy/crds/enterprise.splunk.com_standalones_crd.yaml +++ b/deploy/crds/enterprise.splunk.com_standalones_crd.yaml @@ -4,14 +4,22 @@ metadata: name: standalones.enterprise.splunk.com spec: additionalPrinterColumns: - - JSONPath: .spec.status.phase + - JSONPath: .status.phase description: Status of standalone instances name: Phase - type: integer - - JSONPath: .spec.status.instances - description: Number of standalone instances - name: Instances 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 @@ -20,6 +28,10 @@ spec: singular: standalone scope: Namespaced subresources: + scale: + labelSelectorPath: .status.selector + specReplicasPath: .spec.replicas + statusReplicasPath: .status.replicas status: {} validation: openAPIV3Schema: @@ -716,6 +728,7 @@ spec: type: string replicas: description: Number of standalone pods + format: int32 type: integer resources: description: resource requirements for the pod containers @@ -2210,17 +2223,27 @@ spec: description: StandaloneStatus defines the observed state of a Splunk Enterprise standalone instances. properties: - instances: - description: current number of standalone instances - type: integer phase: description: current phase of the standalone instances enum: - - pending - - ready - - scaleup - - scaledown - - updating + - 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 + selector: + description: selector for pods, used by HorizontalPodAutoscaler type: string type: object type: object diff --git a/deploy/operator.yaml b/deploy/operator.yaml index 4e289230e..c8bb7c135 100644 --- a/deploy/operator.yaml +++ b/deploy/operator.yaml @@ -47,6 +47,6 @@ spec: - name: OPERATOR_NAME value: "splunk-operator" - name: SPLUNK_IMAGE - value: "splunk/splunk:8.0" + value: "splunk/splunk:edge" - name: SPARK_IMAGE value: "splunk/spark" diff --git a/pkg/apis/enterprise/v1alpha2/common_types.go b/pkg/apis/enterprise/v1alpha2/common_types.go index 2821705d9..b0ea496d4 100644 --- a/pkg/apis/enterprise/v1alpha2/common_types.go +++ b/pkg/apis/enterprise/v1alpha2/common_types.go @@ -21,6 +21,33 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" ) +// ResourcePhase is used to represent the current phase of a custom resource +// +kubebuilder:validation:Enum=Pending;Ready;Updating;ScalingUp;ScalingDown;Terminating;Error +type ResourcePhase string + +const ( + // PhasePending means a custom resource has just been created and is not yet ready + PhasePending ResourcePhase = "Pending" + + // PhaseReady means a custom resource is ready and up to date + PhaseReady ResourcePhase = "Ready" + + // PhaseUpdating means a custom resource is in the process of updating to a new desired state (spec) + PhaseUpdating ResourcePhase = "Updating" + + // PhaseScalingUp means a customer resource is in the process of scaling up + PhaseScalingUp ResourcePhase = "ScalingUp" + + // PhaseScalingDown means a customer resource is in the process of scaling down + PhaseScalingDown ResourcePhase = "ScalingDown" + + // PhaseTerminating means a customer resource is in the process of being removed + PhaseTerminating ResourcePhase = "Terminating" + + // PhaseError means an error occured with custom resource management + PhaseError ResourcePhase = "Error" +) + // default all fields to being optional // +kubebuilder:validation:Optional diff --git a/pkg/apis/enterprise/v1alpha2/indexer_types.go b/pkg/apis/enterprise/v1alpha2/indexer_types.go index bd4a5f715..82cc59897 100644 --- a/pkg/apis/enterprise/v1alpha2/indexer_types.go +++ b/pkg/apis/enterprise/v1alpha2/indexer_types.go @@ -31,31 +31,38 @@ type IndexerSpec struct { CommonSplunkSpec `json:",inline"` // Number of search head pods; a search head cluster will be created if > 1 - Replicas int `json:"replicas"` + Replicas int32 `json:"replicas"` } // IndexerStatus defines the observed state of a Splunk Enterprise standalone indexer or cluster of indexers type IndexerStatus struct { // current phase of the indexer cluster - // +kubebuilder:validation:Enum=pending;ready;scaleup;scaledown;updating - Phase string `json:"phase"` + Phase ResourcePhase `json:"phase"` // current phase of the cluster master - // +kubebuilder:validation:Enum=pending;ready;scaleup;scaledown;updating - ClusterMasterPhase string `json:"clusterMasterPhase"` + ClusterMasterPhase ResourcePhase `json:"clusterMasterPhase"` - // current number of indexer instances - Instances int `json:"instances"` + // number of desired indexer peers + Replicas int32 `json:"replicas"` + + // current number of ready indexer peers + ReadyReplicas int32 `json:"readyReplicas"` + + // selector for pods, used by HorizontalPodAutoscaler + Selector string `json:"selector"` } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // Indexer is the Schema for a Splunk Enterprise standalone indexer or cluster of indexers // +kubebuilder:subresource:status +// +kubebuilder:subresource:scale:specpath=.spec.replicas,statuspath=.status.replicas,selectorpath=.status.selector // +kubebuilder:resource:path=indexers,scope=Namespaced,shortName=idx -// +kubebuilder:printcolumn:name="Phase",type="integer",JSONPath=".spec.status.phase",description="Status of indexer cluster" -// +kubebuilder:printcolumn:name="CM",type="string",JSONPath=".spec.status.clusterMasterPhase",description="Status of cluster master" -// +kubebuilder:printcolumn:name="Instances",type="string",JSONPath=".spec.status.instances",description="Number of indexers" +// +kubebuilder:printcolumn:name="Phase",type="string",JSONPath=".status.phase",description="Status of indexer cluster" +// +kubebuilder:printcolumn:name="Master",type="string",JSONPath=".status.clusterMasterPhase",description="Status of cluster master" +// +kubebuilder:printcolumn:name="Desired",type="integer",JSONPath=".status.replicas",description="Number of desired indexer peers" +// +kubebuilder:printcolumn:name="Ready",type="integer",JSONPath=".status.readyReplicas",description="Current number of ready indexer peers" +// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="Age of indexer cluster" type Indexer struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` diff --git a/pkg/apis/enterprise/v1alpha2/licensemaster_types.go b/pkg/apis/enterprise/v1alpha2/licensemaster_types.go index 3b2e1680d..5fe78db16 100644 --- a/pkg/apis/enterprise/v1alpha2/licensemaster_types.go +++ b/pkg/apis/enterprise/v1alpha2/licensemaster_types.go @@ -34,8 +34,7 @@ type LicenseMasterSpec struct { // LicenseMasterStatus defines the observed state of a Splunk Enterprise license master. type LicenseMasterStatus struct { // current phase of the license master - // +kubebuilder:validation:Enum=pending;ready;updating - Phase string `json:"phase"` + Phase ResourcePhase `json:"phase"` } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object @@ -43,7 +42,8 @@ type LicenseMasterStatus struct { // LicenseMaster is the Schema for a Splunk Enterprise license master. // +kubebuilder:subresource:status // +kubebuilder:resource:path=licensemasters,scope=Namespaced,shortName=lm -// +kubebuilder:printcolumn:name="Phase",type="integer",JSONPath=".spec.status.phase",description="Status of license master" +// +kubebuilder:printcolumn:name="Phase",type="string",JSONPath=".status.phase",description="Status of license master" +// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="Age of license master" type LicenseMaster struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` diff --git a/pkg/apis/enterprise/v1alpha2/searchhead_types.go b/pkg/apis/enterprise/v1alpha2/searchhead_types.go index 024e5e965..11ca6702e 100644 --- a/pkg/apis/enterprise/v1alpha2/searchhead_types.go +++ b/pkg/apis/enterprise/v1alpha2/searchhead_types.go @@ -32,7 +32,7 @@ type SearchHeadSpec struct { CommonSplunkSpec `json:",inline"` // Number of search head pods; a search head cluster will be created if > 1 - Replicas int `json:"replicas"` + Replicas int32 `json:"replicas"` // 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. @@ -45,20 +45,32 @@ type SearchHeadSpec struct { // SearchHeadStatus defines the observed state of a Splunk Enterprise standalone search head or cluster of search heads type SearchHeadStatus struct { // current phase of the search head cluster - // +kubebuilder:validation:Enum=pending;ready;scaleup;scaledown;updating - Phase string `json:"phase"` + Phase ResourcePhase `json:"phase"` - // current number of search head instances - Instances int `json:"instances"` + // current phase of the deployer + DeployerPhase ResourcePhase `json:"deployerPhase"` + + // number of desired search head replicas + Replicas int32 `json:"replicas"` + + // current number of ready search head replicas + ReadyReplicas int32 `json:"readyReplicas"` + + // selector for pods, used by HorizontalPodAutoscaler + Selector string `json:"selector"` } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // SearchHead is the Schema for a Splunk Enterprise standalone search head or cluster of search heads // +kubebuilder:subresource:status +// +kubebuilder:subresource:scale:specpath=.spec.replicas,statuspath=.status.replicas,selectorpath=.status.selector // +kubebuilder:resource:path=searchheads,scope=Namespaced,shortName=search;sh -// +kubebuilder:printcolumn:name="Phase",type="integer",JSONPath=".spec.status.phase",description="Status of search head cluster" -// +kubebuilder:printcolumn:name="Instances",type="string",JSONPath=".spec.status.instances",description="Number of search heads" +// +kubebuilder:printcolumn:name="Phase",type="string",JSONPath=".status.phase",description="Status of search head cluster" +// +kubebuilder:printcolumn:name="Deployer",type="string",JSONPath=".status.deployerPhase",description="Status of the deployer" +// +kubebuilder:printcolumn:name="Desired",type="integer",JSONPath=".status.replicas",description="Number of desired search head replicas" +// +kubebuilder:printcolumn:name="Ready",type="integer",JSONPath=".status.readyReplicas",description="Current number of ready search head replicas" +// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="Age of search head cluster" type SearchHead struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` diff --git a/pkg/apis/enterprise/v1alpha2/spark_types.go b/pkg/apis/enterprise/v1alpha2/spark_types.go index e7667689d..abcf92b6c 100644 --- a/pkg/apis/enterprise/v1alpha2/spark_types.go +++ b/pkg/apis/enterprise/v1alpha2/spark_types.go @@ -31,26 +31,38 @@ type SparkSpec struct { CommonSpec `json:",inline"` // Number of spark worker pods - Replicas int `json:"replicas"` + Replicas int32 `json:"replicas"` } // SparkStatus defines the observed state of a Spark cluster type SparkStatus struct { - // current phase of the spark cluster - // +kubebuilder:validation:Enum=pending;ready;scaleup;scaledown;updating - Phase string `json:"phase"` + // current phase of the spark workers + Phase ResourcePhase `json:"phase"` - // current number of spark worker instances - Instances int `json:"instances"` + // current phase of the spark master + MasterPhase ResourcePhase `json:"masterPhase"` + + // number of desired spark workers + Replicas int32 `json:"replicas"` + + // current number of ready spark workers + ReadyReplicas int32 `json:"readyReplicas"` + + // selector for pods, used by HorizontalPodAutoscaler + Selector string `json:"selector"` } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // Spark is the Schema for a Spark cluster // +kubebuilder:subresource:status +// +kubebuilder:subresource:scale:specpath=.spec.replicas,statuspath=.status.replicas,selectorpath=.status.selector // +kubebuilder:resource:path=sparks,scope=Namespaced -// +kubebuilder:printcolumn:name="Phase",type="integer",JSONPath=".spec.status.phase",description="Status of Spark cluster" -// +kubebuilder:printcolumn:name="Instances",type="string",JSONPath=".spec.status.instances",description="Number of Spark workers" +// +kubebuilder:printcolumn:name="Phase",type="string",JSONPath=".status.phase",description="Status of Spark workers" +// +kubebuilder:printcolumn:name="Master",type="string",JSONPath=".status.masterPhase",description="Status of Spark master" +// +kubebuilder:printcolumn:name="Desired",type="integer",JSONPath=".status.replicas",description="Number of desired Spark workers" +// +kubebuilder:printcolumn:name="Ready",type="integer",JSONPath=".status.readyReplicas",description="Current number of ready Spark workers" +// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="Age of Spark cluster" type Spark struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` diff --git a/pkg/apis/enterprise/v1alpha2/standalone_types.go b/pkg/apis/enterprise/v1alpha2/standalone_types.go index de9be7523..a1564b001 100644 --- a/pkg/apis/enterprise/v1alpha2/standalone_types.go +++ b/pkg/apis/enterprise/v1alpha2/standalone_types.go @@ -32,7 +32,7 @@ type StandaloneSpec struct { CommonSplunkSpec `json:",inline"` // Number of standalone pods - Replicas int `json:"replicas"` + Replicas int32 `json:"replicas"` // 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. @@ -45,20 +45,28 @@ type StandaloneSpec struct { // StandaloneStatus defines the observed state of a Splunk Enterprise standalone instances. type StandaloneStatus struct { // current phase of the standalone instances - // +kubebuilder:validation:Enum=pending;ready;scaleup;scaledown;updating - Phase string `json:"phase"` + Phase ResourcePhase `json:"phase"` - // current number of standalone instances - Instances int `json:"instances"` + // number of desired standalone instances + Replicas int32 `json:"replicas"` + + // current number of ready standalone instances + ReadyReplicas int32 `json:"readyReplicas"` + + // selector for pods, used by HorizontalPodAutoscaler + Selector string `json:"selector"` } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // 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:printcolumn:name="Phase",type="integer",JSONPath=".spec.status.phase",description="Status of standalone instances" -// +kubebuilder:printcolumn:name="Instances",type="string",JSONPath=".spec.status.instances",description="Number of standalone instances" +// +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" +// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="Age of standalone resource" type Standalone struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` diff --git a/pkg/splunk/enterprise/configuration.go b/pkg/splunk/enterprise/configuration.go index df1d41cbb..d30159355 100644 --- a/pkg/splunk/enterprise/configuration.go +++ b/pkg/splunk/enterprise/configuration.go @@ -462,10 +462,9 @@ func addDFCToPodTemplate(podTemplateSpec *corev1.PodTemplateSpec, sparkRef corev } // getSplunkStatefulSet returns a Kubernetes StatefulSet object for Splunk instances configured for a Splunk Enterprise resource. -func getSplunkStatefulSet(cr enterprisev1.MetaObject, spec *enterprisev1.CommonSplunkSpec, instanceType InstanceType, replicas int, extraEnv []corev1.EnvVar) (*appsv1.StatefulSet, error) { +func getSplunkStatefulSet(cr enterprisev1.MetaObject, spec *enterprisev1.CommonSplunkSpec, instanceType InstanceType, replicas int32, extraEnv []corev1.EnvVar) (*appsv1.StatefulSet, error) { // prepare misc values - replicas32 := int32(replicas) ports := resources.SortContainerPorts(getSplunkContainerPorts(instanceType)) // note that port order is important for tests annotations := resources.GetIstioAnnotations(ports) selectLabels := getSplunkLabels(cr.GetIdentifier(), instanceType) @@ -501,8 +500,11 @@ func getSplunkStatefulSet(cr enterprisev1.MetaObject, spec *enterprisev1.CommonS MatchLabels: selectLabels, }, ServiceName: GetSplunkServiceName(instanceType, cr.GetIdentifier(), true), - Replicas: &replicas32, - PodManagementPolicy: "Parallel", + Replicas: &replicas, + PodManagementPolicy: appsv1.ParallelPodManagement, + UpdateStrategy: appsv1.StatefulSetUpdateStrategy{ + Type: appsv1.OnDeleteStatefulSetStrategyType, + }, Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Labels: labels, @@ -677,7 +679,7 @@ func updateSplunkPodTemplateWithConfig(podTemplateSpec *corev1.PodTemplateSpec, } // getSearchHeadExtraEnv returns extra environment variables used by search head clusters -func getSearchHeadExtraEnv(cr enterprisev1.MetaObject, replicas int) []corev1.EnvVar { +func getSearchHeadExtraEnv(cr enterprisev1.MetaObject, replicas int32) []corev1.EnvVar { return []corev1.EnvVar{ { Name: "SPLUNK_SEARCH_HEAD_URL", @@ -690,7 +692,7 @@ func getSearchHeadExtraEnv(cr enterprisev1.MetaObject, replicas int) []corev1.En } // getIndexerExtraEnv returns extra environment variables used by search head clusters -func getIndexerExtraEnv(cr enterprisev1.MetaObject, replicas int) []corev1.EnvVar { +func getIndexerExtraEnv(cr enterprisev1.MetaObject, replicas int32) []corev1.EnvVar { return []corev1.EnvVar{ { Name: "SPLUNK_INDEXER_URL", diff --git a/pkg/splunk/enterprise/configuration_test.go b/pkg/splunk/enterprise/configuration_test.go index d55429ab2..2d95b0846 100644 --- a/pkg/splunk/enterprise/configuration_test.go +++ b/pkg/splunk/enterprise/configuration_test.go @@ -58,7 +58,7 @@ func TestGetIndexerStatefulSet(t *testing.T) { configTester(t, "GetIndexerStatefulSet()", f, want) } - 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-stack1-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-stack1-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-secrets"}}],"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_INDEXER_URL","value":"splunk-stack1-indexer-0.splunk-stack1-indexer-headless.test.svc.cluster.local"},{"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-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-stack1-indexer"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"1Gi"}}},"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-stack1-indexer"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"200Gi"}}},"status":{}}],"serviceName":"splunk-stack1-indexer-headless","podManagementPolicy":"Parallel","updateStrategy":{}},"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-stack1-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-stack1-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-secrets"}}],"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_INDEXER_URL","value":"splunk-stack1-indexer-0.splunk-stack1-indexer-headless.test.svc.cluster.local"},{"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-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-stack1-indexer"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"1Gi"}}},"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-stack1-indexer"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"200Gi"}}},"status":{}}],"serviceName":"splunk-stack1-indexer-headless","podManagementPolicy":"Parallel","updateStrategy":{"type":"OnDelete"}},"status":{"replicas":0}}`) } func TestGetSearchHeadStatefulSet(t *testing.T) { @@ -80,19 +80,19 @@ 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-secrets"}}],"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":"datarecieve","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_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":"1Gi"}}},"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":"200Gi"}}},"status":{}}],"serviceName":"splunk-stack1-search-head-headless","podManagementPolicy":"Parallel","updateStrategy":{}},"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-secrets"}}],"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":"datarecieve","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_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":"1Gi"}}},"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":"200Gi"}}},"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-secrets"}}],"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":"datarecieve","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_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":"1Gi"}}},"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":"200Gi"}}},"status":{}}],"serviceName":"splunk-stack1-search-head-headless","podManagementPolicy":"Parallel","updateStrategy":{}},"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-secrets"}}],"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":"datarecieve","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_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":"1Gi"}}},"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":"200Gi"}}},"status":{}}],"serviceName":"splunk-stack1-search-head-headless","podManagementPolicy":"Parallel","updateStrategy":{"type":"OnDelete"}},"status":{"replicas":0}}`) cr.Spec.Replicas = 5 cr.Spec.IndexerRef.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-secrets"}}],"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":"datarecieve","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_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":"1Gi"}}},"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":"200Gi"}}},"status":{}}],"serviceName":"splunk-stack1-search-head-headless","podManagementPolicy":"Parallel","updateStrategy":{}},"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-secrets"}}],"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":"datarecieve","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_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":"1Gi"}}},"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":"200Gi"}}},"status":{}}],"serviceName":"splunk-stack1-search-head-headless","podManagementPolicy":"Parallel","updateStrategy":{"type":"OnDelete"}},"status":{"replicas":0}}`) cr.Spec.Replicas = 6 cr.Spec.SparkRef.Name = cr.GetIdentifier() cr.Spec.IndexerRef.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-secrets"}},{"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":"datarecieve","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_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":"1Gi"}}},"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":"200Gi"}}},"status":{}}],"serviceName":"splunk-stack1-search-head-headless","podManagementPolicy":"Parallel","updateStrategy":{}},"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-secrets"}},{"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":"datarecieve","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_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":"1Gi"}}},"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":"200Gi"}}},"status":{}}],"serviceName":"splunk-stack1-search-head-headless","podManagementPolicy":"Parallel","updateStrategy":{"type":"OnDelete"}},"status":{"replicas":0}}`) } func TestGetStandaloneStatefulSet(t *testing.T) { @@ -113,10 +113,10 @@ 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-secrets"}}],"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":"datarecieve","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"}],"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":"1Gi"}}},"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":"200Gi"}}},"status":{}}],"serviceName":"splunk-stack1-standalone-headless","podManagementPolicy":"Parallel","updateStrategy":{}},"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-secrets"}}],"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":"datarecieve","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"}],"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":"1Gi"}}},"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":"200Gi"}}},"status":{}}],"serviceName":"splunk-stack1-standalone-headless","podManagementPolicy":"Parallel","updateStrategy":{"type":"OnDelete"}},"status":{"replicas":0}}`) cr.Spec.SparkRef.Name = cr.GetIdentifier() - 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-secrets"}},{"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":"datarecieve","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_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":"1Gi"}}},"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":"200Gi"}}},"status":{}}],"serviceName":"splunk-stack1-standalone-headless","podManagementPolicy":"Parallel","updateStrategy":{}},"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-secrets"}},{"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":"datarecieve","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_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":"1Gi"}}},"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":"200Gi"}}},"status":{}}],"serviceName":"splunk-stack1-standalone-headless","podManagementPolicy":"Parallel","updateStrategy":{"type":"OnDelete"}},"status":{"replicas":0}}`) cr.Spec.IndexerRef.Name = "stack2" cr.Spec.StorageClassName = "gp2" @@ -126,7 +126,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-secrets"}},{"name":"mnt-splunk-defaults","configMap":{"name":"splunk-stack1-standalone-defaults"}},{"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":"datarecieve","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,/mnt/defaults/defaults.yml,/mnt/splunk-defaults/default.yml"},{"name":"SPLUNK_HOME_OWNERSHIP_ENFORCEMENT","value":"false"},{"name":"SPLUNK_ROLE","value":"splunk_standalone"},{"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":"1Gi"}},"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":"200Gi"}},"storageClassName":"gp2"},"status":{}}],"serviceName":"splunk-stack1-standalone-headless","podManagementPolicy":"Parallel","updateStrategy":{}},"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-secrets"}},{"name":"mnt-splunk-defaults","configMap":{"name":"splunk-stack1-standalone-defaults"}},{"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":"datarecieve","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,/mnt/defaults/defaults.yml,/mnt/splunk-defaults/default.yml"},{"name":"SPLUNK_HOME_OWNERSHIP_ENFORCEMENT","value":"false"},{"name":"SPLUNK_ROLE","value":"splunk_standalone"},{"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":"1Gi"}},"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":"200Gi"}},"storageClassName":"gp2"},"status":{}}],"serviceName":"splunk-stack1-standalone-headless","podManagementPolicy":"Parallel","updateStrategy":{"type":"OnDelete"}},"status":{"replicas":0}}`) } func TestGetLicenseMasterStatefulSet(t *testing.T) { @@ -147,10 +147,10 @@ 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-secrets"}}],"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"}],"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":"1Gi"}}},"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":"200Gi"}}},"status":{}}],"serviceName":"splunk-stack1-license-master-headless","podManagementPolicy":"Parallel","updateStrategy":{}},"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-secrets"}}],"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"}],"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":"1Gi"}}},"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":"200Gi"}}},"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-secrets"}}],"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_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":"1Gi"}}},"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":"200Gi"}}},"status":{}}],"serviceName":"splunk-stack1-license-master-headless","podManagementPolicy":"Parallel","updateStrategy":{}},"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-secrets"}}],"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_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":"1Gi"}}},"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":"200Gi"}}},"status":{}}],"serviceName":"splunk-stack1-license-master-headless","podManagementPolicy":"Parallel","updateStrategy":{"type":"OnDelete"}},"status":{"replicas":0}}`) } func TestGetClusterMasterStatefulSet(t *testing.T) { @@ -172,17 +172,17 @@ func TestGetClusterMasterStatefulSet(t *testing.T) { } cr.Spec.Replicas = 1 - 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-indexer-secrets"}}],"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_INDEXER_URL","value":"splunk-stack1-indexer-0.splunk-stack1-indexer-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-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":"1Gi"}}},"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":"200Gi"}}},"status":{}}],"serviceName":"splunk-stack1-cluster-master-headless","podManagementPolicy":"Parallel","updateStrategy":{}},"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-indexer-secrets"}}],"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_INDEXER_URL","value":"splunk-stack1-indexer-0.splunk-stack1-indexer-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-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":"1Gi"}}},"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":"200Gi"}}},"status":{}}],"serviceName":"splunk-stack1-cluster-master-headless","podManagementPolicy":"Parallel","updateStrategy":{"type":"OnDelete"}},"status":{"replicas":0}}`) cr.Spec.Replicas = 2 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-indexer-secrets"}}],"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_LICENSE_MASTER_URL","value":"splunk-stack1-license-master-service.test.svc.cluster.local"},{"name":"SPLUNK_INDEXER_URL","value":"splunk-stack1-indexer-0.splunk-stack1-indexer-headless.test.svc.cluster.local,splunk-stack1-indexer-1.splunk-stack1-indexer-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-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":"1Gi"}}},"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":"200Gi"}}},"status":{}}],"serviceName":"splunk-stack1-cluster-master-headless","podManagementPolicy":"Parallel","updateStrategy":{}},"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-indexer-secrets"}}],"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_LICENSE_MASTER_URL","value":"splunk-stack1-license-master-service.test.svc.cluster.local"},{"name":"SPLUNK_INDEXER_URL","value":"splunk-stack1-indexer-0.splunk-stack1-indexer-headless.test.svc.cluster.local,splunk-stack1-indexer-1.splunk-stack1-indexer-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-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":"1Gi"}}},"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":"200Gi"}}},"status":{}}],"serviceName":"splunk-stack1-cluster-master-headless","podManagementPolicy":"Parallel","updateStrategy":{"type":"OnDelete"}},"status":{"replicas":0}}`) cr.Spec.Replicas = 3 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-indexer-secrets"}}],"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_LICENSE_URI","value":"/mnt/splunk.lic"},{"name":"SPLUNK_INDEXER_URL","value":"splunk-stack1-indexer-0.splunk-stack1-indexer-headless.test.svc.cluster.local,splunk-stack1-indexer-1.splunk-stack1-indexer-headless.test.svc.cluster.local,splunk-stack1-indexer-2.splunk-stack1-indexer-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-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":"1Gi"}}},"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":"200Gi"}}},"status":{}}],"serviceName":"splunk-stack1-cluster-master-headless","podManagementPolicy":"Parallel","updateStrategy":{}},"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-indexer-secrets"}}],"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_LICENSE_URI","value":"/mnt/splunk.lic"},{"name":"SPLUNK_INDEXER_URL","value":"splunk-stack1-indexer-0.splunk-stack1-indexer-headless.test.svc.cluster.local,splunk-stack1-indexer-1.splunk-stack1-indexer-headless.test.svc.cluster.local,splunk-stack1-indexer-2.splunk-stack1-indexer-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-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":"1Gi"}}},"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":"200Gi"}}},"status":{}}],"serviceName":"splunk-stack1-cluster-master-headless","podManagementPolicy":"Parallel","updateStrategy":{"type":"OnDelete"}},"status":{"replicas":0}}`) } func TestGetDeployerStatefulSet(t *testing.T) { @@ -204,7 +204,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-search-head-secrets"}}],"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_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":"1Gi"}}},"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":"200Gi"}}},"status":{}}],"serviceName":"splunk-stack1-deployer-headless","podManagementPolicy":"Parallel","updateStrategy":{}},"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-search-head-secrets"}}],"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_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":"1Gi"}}},"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":"200Gi"}}},"status":{}}],"serviceName":"splunk-stack1-deployer-headless","podManagementPolicy":"Parallel","updateStrategy":{"type":"OnDelete"}},"status":{"replicas":0}}`) } func TestGetSplunkService(t *testing.T) { diff --git a/pkg/splunk/enterprise/names.go b/pkg/splunk/enterprise/names.go index 4e809ea8d..a3831705e 100644 --- a/pkg/splunk/enterprise/names.go +++ b/pkg/splunk/enterprise/names.go @@ -62,7 +62,7 @@ func GetSplunkStatefulsetName(instanceType InstanceType, identifier string) stri } // GetSplunkStatefulsetPodName uses a template to name a specific pod within a Kubernetes StatefulSet for Splunk instances. -func GetSplunkStatefulsetPodName(instanceType InstanceType, identifier string, index int) string { +func GetSplunkStatefulsetPodName(instanceType InstanceType, identifier string, index int32) string { return fmt.Sprintf(statefulSetPodTemplateStr, identifier, instanceType, index) } @@ -90,16 +90,16 @@ func GetSplunkDefaultsName(identifier string, instanceType InstanceType) string } // GetSplunkStatefulsetUrls returns a list of fully qualified domain names for all pods within a Splunk StatefulSet. -func GetSplunkStatefulsetUrls(namespace string, instanceType InstanceType, identifier string, replicas int, hostnameOnly bool) string { +func GetSplunkStatefulsetUrls(namespace string, instanceType InstanceType, identifier string, replicas int32, hostnameOnly bool) string { urls := make([]string, replicas) - for i := 0; i < replicas; i++ { + for i := int32(0); i < replicas; i++ { urls[i] = GetSplunkStatefulsetURL(namespace, instanceType, identifier, i, hostnameOnly) } return strings.Join(urls, ",") } // GetSplunkStatefulsetURL returns a fully qualified domain name for a specific pod within a Kubernetes StatefulSet Splunk instances. -func GetSplunkStatefulsetURL(namespace string, instanceType InstanceType, identifier string, index int, hostnameOnly bool) string { +func GetSplunkStatefulsetURL(namespace string, instanceType InstanceType, identifier string, index int32, hostnameOnly bool) string { podName := GetSplunkStatefulsetPodName(instanceType, identifier, index) if hostnameOnly { diff --git a/pkg/splunk/enterprise/names_test.go b/pkg/splunk/enterprise/names_test.go index 6614d84ce..b6d810590 100644 --- a/pkg/splunk/enterprise/names_test.go +++ b/pkg/splunk/enterprise/names_test.go @@ -73,7 +73,7 @@ func TestGetSplunkDefaultsName(t *testing.T) { } func TestGetSplunkStatefulsetUrls(t *testing.T) { - test := func(want string, namespace string, instanceType InstanceType, identifier string, replicas int, hostnameOnly bool) { + test := func(want string, namespace string, instanceType InstanceType, identifier string, replicas int32, hostnameOnly bool) { got := GetSplunkStatefulsetUrls(namespace, instanceType, identifier, replicas, hostnameOnly) if got != want { t.Errorf("GetSplunkStatefulsetUrls(\"%s\",\"%s\",\"%s\",%d,%t) = %s; want %s", diff --git a/pkg/splunk/reconcile/deployment.go b/pkg/splunk/reconcile/deployment.go index b3667a42e..afff7541b 100644 --- a/pkg/splunk/reconcile/deployment.go +++ b/pkg/splunk/reconcile/deployment.go @@ -16,59 +16,68 @@ package deploy import ( "context" + "fmt" + enterprisev1 "github.com/splunk/splunk-operator/pkg/apis/enterprise/v1alpha2" appsv1 "k8s.io/api/apps/v1" "k8s.io/apimachinery/pkg/types" ) // ApplyDeployment creates or updates a Kubernetes Deployment -func ApplyDeployment(client ControllerClient, deployment *appsv1.Deployment) error { +func ApplyDeployment(c ControllerClient, revised *appsv1.Deployment) (enterprisev1.ResourcePhase, error) { scopedLog := log.WithName("ApplyDeployment").WithValues( - "name", deployment.GetObjectMeta().GetName(), - "namespace", deployment.GetObjectMeta().GetNamespace()) + "name", revised.GetObjectMeta().GetName(), + "namespace", revised.GetObjectMeta().GetNamespace()) - namespacedName := types.NamespacedName{Namespace: deployment.GetNamespace(), Name: deployment.GetName()} + namespacedName := types.NamespacedName{Namespace: revised.GetNamespace(), Name: revised.GetName()} var current appsv1.Deployment - err := client.Get(context.TODO(), namespacedName, ¤t) - if err == nil { - // found existing Deployment - if MergeDeploymentUpdates(¤t, deployment) { - // only update if there are material differences, as determined by comparison function - err = UpdateResource(client, ¤t) - } else { - scopedLog.Info("No changes for Deployment") - } - } else { - err = CreateResource(client, deployment) + err := c.Get(context.TODO(), namespacedName, ¤t) + if err != nil { + return enterprisev1.PhasePending, CreateResource(c, revised) } - return err -} + // found an existing Deployment -// MergeDeploymentUpdates looks for material differences between a -// Deployment's current config and a revised config. It merges material -// changes from revised to current. This enables us to minimize updates. -// It returns true if there are material differences between them, or false otherwise. -func MergeDeploymentUpdates(current *appsv1.Deployment, revised *appsv1.Deployment) bool { - scopedLog := log.WithName("MergeDeploymentUpdates").WithValues( - "name", current.GetObjectMeta().GetName(), - "namespace", current.GetObjectMeta().GetNamespace()) - result := false + // check for changes in Pod template + hasUpdates := MergePodUpdates(¤t.Spec.Template, &revised.Spec.Template, current.GetObjectMeta().GetName()) + desiredReplicas := *revised.Spec.Replicas + *revised = current // caller expects that object passed represents latest state - // check for change in Replicas count - if current.Spec.Replicas != nil && revised.Spec.Replicas != nil && *current.Spec.Replicas != *revised.Spec.Replicas { - scopedLog.Info("Deployment Replicas differ", - "current", *current.Spec.Replicas, - "revised", *revised.Spec.Replicas) - current.Spec.Replicas = revised.Spec.Replicas - result = true + // check for scaling + if revised.Spec.Replicas != nil { + if *revised.Spec.Replicas < desiredReplicas { + scopedLog.Info(fmt.Sprintf("Scaling replicas up to %d", desiredReplicas)) + *revised.Spec.Replicas = desiredReplicas + return enterprisev1.PhaseScalingUp, UpdateResource(c, revised) + } else if *revised.Spec.Replicas > desiredReplicas { + scopedLog.Info(fmt.Sprintf("Scaling replicas down to %d", desiredReplicas)) + *revised.Spec.Replicas = desiredReplicas + return enterprisev1.PhaseScalingDown, UpdateResource(c, revised) + } } - // check for changes in Pod template - if MergePodUpdates(¤t.Spec.Template, &revised.Spec.Template, current.GetObjectMeta().GetName()) { - result = true + // only update if there are material differences, as determined by comparison function + if hasUpdates { + return enterprisev1.PhaseUpdating, UpdateResource(c, revised) + } + + // check if updates are in progress + if revised.Status.UpdatedReplicas < revised.Status.Replicas { + scopedLog.Info("Waiting for updates to complete") + return enterprisev1.PhaseUpdating, nil + } + + // check if replicas are not yet ready + if revised.Status.ReadyReplicas < desiredReplicas { + scopedLog.Info("Waiting for pods to become ready") + if revised.Status.ReadyReplicas > 0 { + return enterprisev1.PhaseScalingUp, nil + } + return enterprisev1.PhasePending, nil } - return result + // all is good! + scopedLog.Info("All pods are ready") + return enterprisev1.PhaseReady, nil } diff --git a/pkg/splunk/reconcile/deployment_test.go b/pkg/splunk/reconcile/deployment_test.go index 93febbb0b..456912a2e 100644 --- a/pkg/splunk/reconcile/deployment_test.go +++ b/pkg/splunk/reconcile/deployment_test.go @@ -17,6 +17,7 @@ package deploy import ( "testing" + enterprisev1 "github.com/splunk/splunk-operator/pkg/apis/enterprise/v1alpha2" appsv1 "k8s.io/api/apps/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -34,11 +35,86 @@ func TestApplyDeployment(t *testing.T) { Spec: appsv1.DeploymentSpec{ Replicas: &replicas, }, + Status: appsv1.DeploymentStatus{ + Replicas: 1, + ReadyReplicas: 1, + UpdatedReplicas: 1, + }, + } + wantPhases := []enterprisev1.ResourcePhase{ + enterprisev1.PhasePending, + enterprisev1.PhaseReady, + enterprisev1.PhaseUpdating, } + wantPhaseNum := 0 + reconcile := func(c *mockClient, cr interface{}) error { + gotPhase, err := ApplyDeployment(c, cr.(*appsv1.Deployment)) + if gotPhase != wantPhases[wantPhaseNum] { + t.Errorf("TestApplyDeployment() got phase[%d] = %s; want %s", wantPhaseNum, gotPhase, wantPhases[wantPhaseNum]) + } + wantPhaseNum++ + return err + } + + // test update revised := current.DeepCopy() + revised.Spec.Template.ObjectMeta.Labels = map[string]string{"one": "two"} + //reconcileTester(t, "TestApplyDeployment", ¤t, revised, createCalls, updateCalls, reconcile) + + // test scale up + revised = current.DeepCopy() *revised.Spec.Replicas = 3 - reconcile := func(c *mockClient, cr interface{}) error { - return ApplyDeployment(c, cr.(*appsv1.Deployment)) + wantPhases = []enterprisev1.ResourcePhase{ + enterprisev1.PhasePending, + enterprisev1.PhaseReady, + enterprisev1.PhaseScalingUp, } + wantPhaseNum = 0 reconcileTester(t, "TestApplyDeployment", ¤t, revised, createCalls, updateCalls, reconcile) + + // test scale down + *current.Spec.Replicas = 5 + current.Status.Replicas = 5 + current.Status.ReadyReplicas = 5 + current.Status.UpdatedReplicas = 5 + revised = current.DeepCopy() + *revised.Spec.Replicas = 3 + wantPhases = []enterprisev1.ResourcePhase{ + enterprisev1.PhasePending, + enterprisev1.PhaseReady, + enterprisev1.PhaseScalingDown, + } + wantPhaseNum = 0 + reconcileTester(t, "TestApplyDeployment", ¤t, revised, createCalls, updateCalls, reconcile) + + // check for no updates, except pending pod updates (in progress) + c := newMockClient() + c.state[getStateKey(¤t)] = ¤t + current.Status.Replicas = 5 + current.Status.ReadyReplicas = 5 + current.Status.UpdatedReplicas = 3 + wantPhase := enterprisev1.PhaseUpdating + gotPhase, err := ApplyDeployment(c, ¤t) + if gotPhase != wantPhase { + t.Errorf("TestApplyDeployment() got phase = %s; want %s", gotPhase, wantPhase) + } + if err != nil { + t.Errorf("TestApplyDeployment() returned error = %v; want nil", err) + } + + // check for no updates, except waiting for pods to become ready + c = newMockClient() + c.state[getStateKey(¤t)] = ¤t + *current.Spec.Replicas = 5 + current.Status.Replicas = 5 + current.Status.ReadyReplicas = 3 + current.Status.UpdatedReplicas = 5 + wantPhase = enterprisev1.PhaseScalingUp + gotPhase, err = ApplyDeployment(c, ¤t) + if gotPhase != wantPhase { + t.Errorf("TestApplyDeployment() got phase = %s; want %s", gotPhase, wantPhase) + } + if err != nil { + t.Errorf("TestApplyDeployment() returned error = %v; want nil", err) + } } diff --git a/pkg/splunk/reconcile/indexer.go b/pkg/splunk/reconcile/indexer.go index c35a6e2cc..2a0c18f6e 100644 --- a/pkg/splunk/reconcile/indexer.go +++ b/pkg/splunk/reconcile/indexer.go @@ -15,6 +15,9 @@ package deploy import ( + "context" + "fmt" + enterprisev1 "github.com/splunk/splunk-operator/pkg/apis/enterprise/v1alpha2" "github.com/splunk/splunk-operator/pkg/splunk/enterprise" ) @@ -28,9 +31,22 @@ func ReconcileIndexer(client ControllerClient, cr *enterprisev1.Indexer) error { return err } + // updates status after function completes + cr.Status.Phase = enterprisev1.PhaseError + cr.Status.Replicas = cr.Spec.Replicas + cr.Status.Selector = fmt.Sprintf("app.kubernetes.io/instance=splunk-%s-indexer", cr.GetIdentifier()) + defer func() { + client.Status().Update(context.TODO(), cr) + }() + // check if deletion has been requested if cr.ObjectMeta.DeletionTimestamp != nil { - _, err := CheckSplunkDeletion(cr, client) + terminating, err := CheckSplunkDeletion(cr, client) + if terminating && err != nil { // don't bother if no error, since it will just be removed immmediately after + cr.Status.Phase = enterprisev1.PhaseTerminating + cr.Status.ClusterMasterPhase = enterprisev1.PhaseTerminating + client.Status().Update(context.TODO(), cr) + } return err } @@ -63,8 +79,12 @@ func ReconcileIndexer(client ControllerClient, cr *enterprisev1.Indexer) error { if err != nil { return err } - err = ApplyStatefulSet(client, statefulSet) + cr.Status.ClusterMasterPhase, err = ApplyStatefulSet(client, statefulSet) + if err == nil && cr.Status.Phase == enterprisev1.PhaseReady { + cr.Status.ClusterMasterPhase, err = ReconcileStatefulSetPods(client, statefulSet, statefulSet.Status.ReadyReplicas, 1, nil) + } if err != nil { + cr.Status.ClusterMasterPhase = enterprisev1.PhaseError return err } @@ -73,5 +93,15 @@ func ReconcileIndexer(client ControllerClient, cr *enterprisev1.Indexer) error { if err != nil { return err } - return ApplyStatefulSet(client, statefulSet) + cr.Status.Phase, err = ApplyStatefulSet(client, statefulSet) + cr.Status.ReadyReplicas = statefulSet.Status.ReadyReplicas + if err == nil && cr.Status.Phase == enterprisev1.PhaseReady { + cr.Status.Phase, err = ReconcileStatefulSetPods(client, statefulSet, cr.Status.ReadyReplicas, cr.Spec.Replicas, nil) + } + if err != nil { + cr.Status.Phase = enterprisev1.PhaseError + return err + } + + return nil } diff --git a/pkg/splunk/reconcile/licensemaster.go b/pkg/splunk/reconcile/licensemaster.go index 5a21d6d45..1516f5406 100644 --- a/pkg/splunk/reconcile/licensemaster.go +++ b/pkg/splunk/reconcile/licensemaster.go @@ -15,6 +15,8 @@ package deploy import ( + "context" + enterprisev1 "github.com/splunk/splunk-operator/pkg/apis/enterprise/v1alpha2" "github.com/splunk/splunk-operator/pkg/splunk/enterprise" ) @@ -28,9 +30,19 @@ func ReconcileLicenseMaster(client ControllerClient, cr *enterprisev1.LicenseMas return err } + // updates status after function completes + cr.Status.Phase = enterprisev1.PhaseError + defer func() { + client.Status().Update(context.TODO(), cr) + }() + // check if deletion has been requested if cr.ObjectMeta.DeletionTimestamp != nil { - _, err := CheckSplunkDeletion(cr, client) + terminating, err := CheckSplunkDeletion(cr, client) + if terminating && err != nil { // don't bother if no error, since it will just be removed immmediately after + cr.Status.Phase = enterprisev1.PhaseTerminating + client.Status().Update(context.TODO(), cr) + } return err } @@ -51,5 +63,14 @@ func ReconcileLicenseMaster(client ControllerClient, cr *enterprisev1.LicenseMas if err != nil { return err } - return ApplyStatefulSet(client, statefulSet) + cr.Status.Phase, err = ApplyStatefulSet(client, statefulSet) + if err == nil && cr.Status.Phase == enterprisev1.PhaseReady { + cr.Status.Phase, err = ReconcileStatefulSetPods(client, statefulSet, statefulSet.Status.ReadyReplicas, 1, nil) + } + if err != nil { + cr.Status.Phase = enterprisev1.PhaseError + return err + } + + return nil } diff --git a/pkg/splunk/reconcile/searchhead.go b/pkg/splunk/reconcile/searchhead.go index a15ce52cb..55e2ab8ab 100644 --- a/pkg/splunk/reconcile/searchhead.go +++ b/pkg/splunk/reconcile/searchhead.go @@ -15,6 +15,9 @@ package deploy import ( + "context" + "fmt" + enterprisev1 "github.com/splunk/splunk-operator/pkg/apis/enterprise/v1alpha2" "github.com/splunk/splunk-operator/pkg/splunk/enterprise" ) @@ -28,9 +31,21 @@ func ReconcileSearchHead(client ControllerClient, cr *enterprisev1.SearchHead) e return err } + // updates status after function completes + cr.Status.Phase = enterprisev1.PhaseError + cr.Status.Replicas = cr.Spec.Replicas + cr.Status.Selector = fmt.Sprintf("app.kubernetes.io/instance=splunk-%s-search-head", cr.GetIdentifier()) + defer func() { + client.Status().Update(context.TODO(), cr) + }() + // check if deletion has been requested if cr.ObjectMeta.DeletionTimestamp != nil { - _, err := CheckSplunkDeletion(cr, client) + terminating, err := CheckSplunkDeletion(cr, client) + if terminating && err != nil { // don't bother if no error, since it will just be removed immmediately after + cr.Status.Phase = enterprisev1.PhaseTerminating + cr.Status.DeployerPhase = enterprisev1.PhaseTerminating + } return err } @@ -63,8 +78,12 @@ func ReconcileSearchHead(client ControllerClient, cr *enterprisev1.SearchHead) e if err != nil { return err } - err = ApplyStatefulSet(client, statefulSet) + cr.Status.DeployerPhase, err = ApplyStatefulSet(client, statefulSet) + if err == nil && cr.Status.DeployerPhase == enterprisev1.PhaseReady { + cr.Status.DeployerPhase, err = ReconcileStatefulSetPods(client, statefulSet, statefulSet.Status.ReadyReplicas, 1, nil) + } if err != nil { + cr.Status.DeployerPhase = enterprisev1.PhaseError return err } @@ -73,5 +92,15 @@ func ReconcileSearchHead(client ControllerClient, cr *enterprisev1.SearchHead) e if err != nil { return err } - return ApplyStatefulSet(client, statefulSet) + cr.Status.Phase, err = ApplyStatefulSet(client, statefulSet) + cr.Status.ReadyReplicas = statefulSet.Status.ReadyReplicas + if err == nil && cr.Status.Phase == enterprisev1.PhaseReady { + cr.Status.Phase, err = ReconcileStatefulSetPods(client, statefulSet, cr.Status.ReadyReplicas, cr.Spec.Replicas, nil) + } + if err != nil { + cr.Status.Phase = enterprisev1.PhaseError + return err + } + + return nil } diff --git a/pkg/splunk/reconcile/spark.go b/pkg/splunk/reconcile/spark.go index e10c7ee4c..5973ace0e 100644 --- a/pkg/splunk/reconcile/spark.go +++ b/pkg/splunk/reconcile/spark.go @@ -15,6 +15,9 @@ package deploy import ( + "context" + "fmt" + enterprisev1 "github.com/splunk/splunk-operator/pkg/apis/enterprise/v1alpha2" "github.com/splunk/splunk-operator/pkg/splunk/spark" ) @@ -28,9 +31,21 @@ func ReconcileSpark(client ControllerClient, cr *enterprisev1.Spark) error { return err } + // updates status after function completes + cr.Status.Phase = enterprisev1.PhaseError + cr.Status.Replicas = cr.Spec.Replicas + cr.Status.Selector = fmt.Sprintf("app.kubernetes.io/instance=splunk-%s-spark-worker", cr.GetIdentifier()) + defer func() { + client.Status().Update(context.TODO(), cr) + }() + // check if deletion has been requested if cr.ObjectMeta.DeletionTimestamp != nil { - _, err := CheckSplunkDeletion(cr, client) + terminating, err := CheckSplunkDeletion(cr, client) + if terminating && err != nil { // don't bother if no error, since it will just be removed immmediately after + cr.Status.Phase = enterprisev1.PhaseTerminating + client.Status().Update(context.TODO(), cr) + } return err } @@ -51,8 +66,9 @@ func ReconcileSpark(client ControllerClient, cr *enterprisev1.Spark) error { if err != nil { return err } - err = ApplyDeployment(client, deployment) + cr.Status.MasterPhase, err = ApplyDeployment(client, deployment) if err != nil { + cr.Status.MasterPhase = enterprisev1.PhaseError return err } @@ -61,8 +77,10 @@ func ReconcileSpark(client ControllerClient, cr *enterprisev1.Spark) error { if err != nil { return err } - err = ApplyDeployment(client, deployment) + cr.Status.Phase, err = ApplyDeployment(client, deployment) + cr.Status.ReadyReplicas = deployment.Status.ReadyReplicas if err != nil { + cr.Status.Phase = enterprisev1.PhaseError return err } diff --git a/pkg/splunk/reconcile/standalone.go b/pkg/splunk/reconcile/standalone.go index 3e25460c6..47ec4b29b 100644 --- a/pkg/splunk/reconcile/standalone.go +++ b/pkg/splunk/reconcile/standalone.go @@ -15,6 +15,9 @@ package deploy import ( + "context" + "fmt" + enterprisev1 "github.com/splunk/splunk-operator/pkg/apis/enterprise/v1alpha2" "github.com/splunk/splunk-operator/pkg/splunk/enterprise" ) @@ -28,9 +31,21 @@ func ReconcileStandalone(client ControllerClient, cr *enterprisev1.Standalone) e return err } + // updates status after function completes + cr.Status.Phase = enterprisev1.PhaseError + cr.Status.Replicas = cr.Spec.Replicas + cr.Status.Selector = fmt.Sprintf("app.kubernetes.io/instance=splunk-%s-standalone", cr.GetIdentifier()) + defer func() { + client.Status().Update(context.TODO(), cr) + }() + // check if deletion has been requested if cr.ObjectMeta.DeletionTimestamp != nil { - _, err := CheckSplunkDeletion(cr, client) + terminating, err := CheckSplunkDeletion(cr, client) + if terminating && err != nil { // don't bother if no error, since it will just be removed immmediately after + cr.Status.Phase = enterprisev1.PhaseTerminating + client.Status().Update(context.TODO(), cr) + } return err } @@ -51,5 +66,15 @@ func ReconcileStandalone(client ControllerClient, cr *enterprisev1.Standalone) e if err != nil { return err } - return ApplyStatefulSet(client, statefulSet) + cr.Status.Phase, err = ApplyStatefulSet(client, statefulSet) + cr.Status.ReadyReplicas = statefulSet.Status.ReadyReplicas + if err == nil && cr.Status.Phase == enterprisev1.PhaseReady { + cr.Status.Phase, err = ReconcileStatefulSetPods(client, statefulSet, cr.Status.ReadyReplicas, cr.Spec.Replicas, nil) + } + if err != nil { + cr.Status.Phase = enterprisev1.PhaseError + return err + } + + return nil } diff --git a/pkg/splunk/reconcile/statefulset.go b/pkg/splunk/reconcile/statefulset.go index b03a3ed1c..ab302f961 100644 --- a/pkg/splunk/reconcile/statefulset.go +++ b/pkg/splunk/reconcile/statefulset.go @@ -16,59 +16,129 @@ package deploy import ( "context" + "fmt" appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + + enterprisev1 "github.com/splunk/splunk-operator/pkg/apis/enterprise/v1alpha2" ) // ApplyStatefulSet creates or updates a Kubernetes StatefulSet -func ApplyStatefulSet(client ControllerClient, statefulSet *appsv1.StatefulSet) error { - scopedLog := log.WithName("ApplyStatefulSet").WithValues( +func ApplyStatefulSet(c ControllerClient, revised *appsv1.StatefulSet) (enterprisev1.ResourcePhase, error) { + namespacedName := types.NamespacedName{Namespace: revised.GetNamespace(), Name: revised.GetName()} + var current appsv1.StatefulSet + + err := c.Get(context.TODO(), namespacedName, ¤t) + if err != nil { + // no StatefulSet exists -> just create a new one + err = CreateResource(c, revised) + return enterprisev1.PhasePending, err + } + + // found an existing StatefulSet + + // check for changes in Pod template + hasUpdates := MergePodUpdates(¤t.Spec.Template, &revised.Spec.Template, current.GetObjectMeta().GetName()) + *revised = current // caller expects that object passed represents latest state + + // only update if there are material differences, as determined by comparison function + if hasUpdates { + // this updates the desired state template, but doesn't actually modify any pods + // because we use an "OnUpdate" strategy https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/#update-strategies + // note also that this ignores Replicas, which is handled below by ReconcileStatefulSetPods + return enterprisev1.PhaseUpdating, UpdateResource(c, revised) + } + + // scaling and pod updates are handled by ReconcileStatefulSetPods + return enterprisev1.PhaseReady, nil +} + +// ReconcileStatefulSetPods manages scaling and config updates for StatefulSets +func ReconcileStatefulSetPods(c ControllerClient, statefulSet *appsv1.StatefulSet, readyReplicas, desiredReplicas int32, + removeReadyPod func(ControllerClient, *appsv1.StatefulSet) (enterprisev1.ResourcePhase, error)) (enterprisev1.ResourcePhase, error) { + + scopedLog := log.WithName("ReconcileStatefulSetPods").WithValues( "name", statefulSet.GetObjectMeta().GetName(), "namespace", statefulSet.GetObjectMeta().GetNamespace()) - namespacedName := types.NamespacedName{Namespace: statefulSet.GetNamespace(), Name: statefulSet.GetName()} - var current appsv1.StatefulSet + // inline function used to handle scale down + scaleDown := func(replicas int32) error { + scopedLog.Info(fmt.Sprintf("Scaling replicas down to %d", replicas)) + *statefulSet.Spec.Replicas = replicas + return UpdateResource(c, statefulSet) + } - err := client.Get(context.TODO(), namespacedName, ¤t) - if err == nil { - // found existing StatefulSet - if MergeStatefulSetUpdates(¤t, statefulSet) { - // only update if there are material differences, as determined by comparison function - err = UpdateResource(client, ¤t) - } else { - scopedLog.Info("No changes for StatefulSet") + // check for scaling down + if readyReplicas > desiredReplicas { + if removeReadyPod != nil { + return removeReadyPod(c, statefulSet) } - } else { - err = CreateResource(client, statefulSet) + return enterprisev1.PhaseScalingDown, scaleDown(readyReplicas - 1) } - return err -} + // check if replicas are not yet ready + if readyReplicas < desiredReplicas { + if *statefulSet.Spec.Replicas < desiredReplicas { + // scale up StatefulSet to match desiredReplicas + scopedLog.Info(fmt.Sprintf("Scaling replicas up to %d", desiredReplicas)) + *statefulSet.Spec.Replicas = desiredReplicas + return enterprisev1.PhaseScalingUp, UpdateResource(c, statefulSet) + } -// MergeStatefulSetUpdates looks for material differences between a -// StatefulSet's current config and a revised config. It merges material -// changes from revised to current. This enables us to minimize updates. -// It returns true if there are material differences between them, or false otherwise. -func MergeStatefulSetUpdates(current *appsv1.StatefulSet, revised *appsv1.StatefulSet) bool { - scopedLog := log.WithName("MergeStatefulSetUpdates").WithValues( - "name", current.GetObjectMeta().GetName(), - "namespace", current.GetObjectMeta().GetNamespace()) - result := false - - // check for change in Replicas count - if current.Spec.Replicas != nil && revised.Spec.Replicas != nil && *current.Spec.Replicas != *revised.Spec.Replicas { - scopedLog.Info("StatefulSet Replicas differ", - "current", *current.Spec.Replicas, - "revised", *revised.Spec.Replicas) - current.Spec.Replicas = revised.Spec.Replicas - result = true + if statefulSet.Status.UpdatedReplicas < statefulSet.Status.Replicas { + scopedLog.Info("Waiting for updates to complete") + return enterprisev1.PhaseUpdating, nil + } + + scopedLog.Info("Waiting for pods to become ready") + if readyReplicas > 0 { + return enterprisev1.PhaseScalingUp, nil + } + return enterprisev1.PhasePending, nil } - // check for changes in Pod template - if MergePodUpdates(¤t.Spec.Template, &revised.Spec.Template, current.GetObjectMeta().GetName()) { - result = true + // readyReplicas == desiredReplicas + + // check if we have extra pods in statefulset + if *statefulSet.Spec.Replicas != desiredReplicas { + // scale down StatefulSet to match readyReplicas + return enterprisev1.PhaseScalingDown, scaleDown(readyReplicas) + } + + // ready and no StatefulSet scaling required + + // check existing pods for desired updates + for n := *statefulSet.Spec.Replicas; n > 0; n-- { + // get Pod + podName := fmt.Sprintf("%s-%d", statefulSet.GetName(), n-1) + namespacedName := types.NamespacedName{Namespace: statefulSet.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 enterprisev1.PhaseError, err + } + + // terminate pod if it has pending updates; k8s will start a new one with revised template + if statefulSet.Status.UpdateRevision != "" && statefulSet.Status.UpdateRevision != pod.GetLabels()["controller-revision-hash"] { + scopedLog.Info("Recycling Pod for updates", "podName", podName, + "statefulSetRevision", statefulSet.Status.CurrentRevision, + "podRevision", pod.GetLabels()["controller-revision-hash"]) + preconditions := client.Preconditions{UID: &pod.ObjectMeta.UID, ResourceVersion: &pod.ObjectMeta.ResourceVersion} + err = c.Delete(context.Background(), &pod, preconditions) + if err != nil { + scopedLog.Error(err, "Unable to delete Pod", "podName", podName) + return enterprisev1.PhaseError, err + } + // only delete one at a time + return enterprisev1.PhaseUpdating, nil + } } - return result + // all is good! + scopedLog.Info("All pods are ready") + return enterprisev1.PhaseReady, nil } diff --git a/pkg/splunk/reconcile/statefulset_test.go b/pkg/splunk/reconcile/statefulset_test.go index 1cecbf1d6..a37399764 100644 --- a/pkg/splunk/reconcile/statefulset_test.go +++ b/pkg/splunk/reconcile/statefulset_test.go @@ -36,9 +36,10 @@ func TestApplyStatefulSet(t *testing.T) { }, } revised := current.DeepCopy() - *revised.Spec.Replicas = 3 + revised.Spec.Template.ObjectMeta.Labels = map[string]string{"one": "two"} reconcile := func(c *mockClient, cr interface{}) error { - return ApplyStatefulSet(c, cr.(*appsv1.StatefulSet)) + _, err := ApplyStatefulSet(c, cr.(*appsv1.StatefulSet)) + return err } reconcileTester(t, "TestApplyStatefulSet", ¤t, revised, createCalls, updateCalls, reconcile) } diff --git a/pkg/splunk/reconcile/util.go b/pkg/splunk/reconcile/util.go index bc6cf3098..3e8707fbb 100644 --- a/pkg/splunk/reconcile/util.go +++ b/pkg/splunk/reconcile/util.go @@ -81,87 +81,106 @@ func UpdateResource(client ControllerClient, obj ResourceObject) error { // current. This enables us to minimize updates. It returns true if there // are material differences between them, or false otherwise. func MergePodUpdates(current *corev1.PodTemplateSpec, revised *corev1.PodTemplateSpec, name string) bool { - scopedLog := log.WithName("MergePodUpdates").WithValues("name", name) + result := MergePodSpecUpdates(¤t.Spec, &revised.Spec, name) + if MergePodMetaUpdates(¤t.ObjectMeta, &revised.ObjectMeta, name) { + result = true + } + return result +} + +// MergePodMetaUpdates looks for material differences between a Pod's current +// meta data and a revised meta data. It merges material changes from revised to +// current. This enables us to minimize updates. It returns true if there +// are material differences between them, or false otherwise. +func MergePodMetaUpdates(current *metav1.ObjectMeta, revised *metav1.ObjectMeta, name string) bool { + scopedLog := log.WithName("MergePodMetaUpdates").WithValues("name", name) result := false - // check for changes in Affinity - if resources.CompareByMarshall(current.Spec.Affinity, revised.Spec.Affinity) { - scopedLog.Info("Pod Affinity differs", - "current", current.Spec.Affinity, - "revised", revised.Spec.Affinity) - current.Spec.Affinity = revised.Spec.Affinity + // check Annotations + if !reflect.DeepEqual(current.Annotations, revised.Annotations) { + scopedLog.Info("Container Annotations differ", "current", current.Annotations, "revised", revised.Annotations) + current.Annotations = revised.Annotations result = true } - // check for changes in SchedulerName - if current.Spec.SchedulerName != revised.Spec.SchedulerName { - scopedLog.Info("Pod SchedulerName differs", - "current", current.Spec.SchedulerName, - "revised", revised.Spec.SchedulerName) - current.Spec.SchedulerName = revised.Spec.SchedulerName + // check Labels + if !reflect.DeepEqual(current.Labels, revised.Labels) { + scopedLog.Info("Container Labels differ", "current", current.Labels, "revised", revised.Labels) + current.Labels = revised.Labels result = true } - // check Annotations - if !reflect.DeepEqual(current.ObjectMeta.Annotations, revised.ObjectMeta.Annotations) { - scopedLog.Info("Container Annotations differ", - "current", current.ObjectMeta.Annotations, - "revised", revised.ObjectMeta.Annotations) - current.ObjectMeta.Annotations = revised.ObjectMeta.Annotations + return result +} + +// MergePodSpecUpdates looks for material differences between a Pod's current +// desired spec and a revised spec. It merges material changes from revised to +// current. This enables us to minimize updates. It returns true if there +// are material differences between them, or false otherwise. +func MergePodSpecUpdates(current *corev1.PodSpec, revised *corev1.PodSpec, name string) bool { + scopedLog := log.WithName("MergePodUpdates").WithValues("name", name) + result := false + + // check for changes in Affinity + if resources.CompareByMarshall(current.Affinity, revised.Affinity) { + scopedLog.Info("Pod Affinity differs", + "current", current.Affinity, + "revised", revised.Affinity) + current.Affinity = revised.Affinity result = true } - // check Labels - if !reflect.DeepEqual(current.ObjectMeta.Labels, revised.ObjectMeta.Labels) { - scopedLog.Info("Container Labels differ", - "current", current.ObjectMeta.Labels, - "revised", revised.ObjectMeta.Labels) - current.ObjectMeta.Labels = revised.ObjectMeta.Labels + // check for changes in SchedulerName + if current.SchedulerName != revised.SchedulerName { + scopedLog.Info("Pod SchedulerName differs", + "current", current.SchedulerName, + "revised", revised.SchedulerName) + current.SchedulerName = revised.SchedulerName result = true } // check for changes in container images; assume that the ordering is same for pods with > 1 container - if len(current.Spec.Containers) != len(revised.Spec.Containers) { + if len(current.Containers) != len(revised.Containers) { scopedLog.Info("Pod Container counts differ", - "current", len(current.Spec.Containers), - "revised", len(revised.Spec.Containers)) - current.Spec.Containers = revised.Spec.Containers + "current", len(current.Containers), + "revised", len(revised.Containers)) + current.Containers = revised.Containers result = true } else { - for idx := range current.Spec.Containers { + for idx := range current.Containers { // check Image - if current.Spec.Containers[idx].Image != revised.Spec.Containers[idx].Image { + if current.Containers[idx].Image != revised.Containers[idx].Image { scopedLog.Info("Pod Container Images differ", - "current", current.Spec.Containers[idx].Image, - "revised", revised.Spec.Containers[idx].Image) - current.Spec.Containers[idx].Image = revised.Spec.Containers[idx].Image + "current", current.Containers[idx].Image, + "revised", revised.Containers[idx].Image) + current.Containers[idx].Image = revised.Containers[idx].Image result = true } // check Ports - if resources.CompareContainerPorts(current.Spec.Containers[idx].Ports, revised.Spec.Containers[idx].Ports) { + if resources.CompareContainerPorts(current.Containers[idx].Ports, revised.Containers[idx].Ports) { scopedLog.Info("Pod Container Ports differ", - "current", current.Spec.Containers[idx].Ports, - "revised", revised.Spec.Containers[idx].Ports) - current.Spec.Containers[idx].Ports = revised.Spec.Containers[idx].Ports + "current", current.Containers[idx].Ports, + "revised", revised.Containers[idx].Ports) + current.Containers[idx].Ports = revised.Containers[idx].Ports result = true } // check VolumeMounts - if resources.CompareVolumeMounts(current.Spec.Containers[idx].VolumeMounts, revised.Spec.Containers[idx].VolumeMounts) { + if resources.CompareVolumeMounts(current.Containers[idx].VolumeMounts, revised.Containers[idx].VolumeMounts) { scopedLog.Info("Pod Container VolumeMounts differ", - "current", current.Spec.Containers[idx].VolumeMounts, - "revised", revised.Spec.Containers[idx].VolumeMounts) - current.Spec.Containers[idx].VolumeMounts = revised.Spec.Containers[idx].VolumeMounts + "current", current.Containers[idx].VolumeMounts, + "revised", revised.Containers[idx].VolumeMounts) + current.Containers[idx].VolumeMounts = revised.Containers[idx].VolumeMounts result = true } // check Resources - if resources.CompareByMarshall(¤t.Spec.Containers[idx].Resources, &revised.Spec.Containers[idx].Resources) { + if resources.CompareByMarshall(¤t.Containers[idx].Resources, &revised.Containers[idx].Resources) { scopedLog.Info("Pod Container Resources differ", - "current", current.Spec.Containers[idx].Resources, - "revised", revised.Spec.Containers[idx].Resources) - current.Spec.Containers[idx].Resources = revised.Spec.Containers[idx].Resources + "current", current.Containers[idx].Resources, + "revised", revised.Containers[idx].Resources) + current.Containers[idx].Resources = revised.Containers[idx].Resources result = true } } diff --git a/pkg/splunk/reconcile/util_test.go b/pkg/splunk/reconcile/util_test.go index 223485dbd..e48839d9f 100644 --- a/pkg/splunk/reconcile/util_test.go +++ b/pkg/splunk/reconcile/util_test.go @@ -284,35 +284,30 @@ func reconcileTester(t *testing.T, method string, } // test create new + methodPlus := fmt.Sprintf("%s(create)", method) err := reconcile(c, current) if err != nil { - t.Errorf("%s() returned %v; want nil", method, err) + t.Errorf("%s returned %v; want nil", methodPlus, err) } - c.checkCalls(t, method, createCalls) + c.checkCalls(t, methodPlus, createCalls) // test no updates required for current + methodPlus = fmt.Sprintf("%s(update-no-change)", method) c.resetCalls() err = reconcile(c, current) if err != nil { - t.Errorf("%s() returned %v; want nil", method, err) + t.Errorf("%s returned %v; want nil", methodPlus, err) } - c.checkCalls(t, method, map[string][]mockFuncCall{"Get": createCalls["Get"]}) + c.checkCalls(t, methodPlus, map[string][]mockFuncCall{"Get": createCalls["Get"]}) // test updates required + methodPlus = fmt.Sprintf("%s(update-with-change)", method) c.resetCalls() err = reconcile(c, revised) if err != nil { - t.Errorf("%s() returned %v; want nil", method, err) + t.Errorf("%s returned %v; want nil", methodPlus, err) } - c.checkCalls(t, method, updateCalls) - - // test no updates required for revised - c.resetCalls() - err = reconcile(c, revised) - if err != nil { - t.Errorf("%s() returned %v; want nil", method, err) - } - c.checkCalls(t, method, map[string][]mockFuncCall{"Get": updateCalls["Get"]}) + c.checkCalls(t, methodPlus, updateCalls) } func TestCreateResource(t *testing.T) { From bcd759e4bf972322b08c839fbd22fb8088bdf67d Mon Sep 17 00:00:00 2001 From: Mike Dickey Date: Thu, 5 Mar 2020 13:39:21 -0800 Subject: [PATCH 2/7] Added additional status fields for SearchHead, which are populated via queries to the Splunk Enterprise REST API --- deploy/all-in-one-cluster.yaml | 38 +++ deploy/all-in-one-scoped.yaml | 38 +++ deploy/crds/combined.yaml | 38 +++ ...enterprise.splunk.com_searchheads_crd.yaml | 38 +++ .../enterprise/v1alpha2/searchhead_types.go | 33 ++ .../v1alpha2/zz_generated.deepcopy.go | 23 +- pkg/splunk/enterprise/configuration.go | 61 ++++ pkg/splunk/enterprise/restapi.go | 285 ++++++++++++++++++ pkg/splunk/reconcile/config.go | 26 +- pkg/splunk/reconcile/config_test.go | 9 +- pkg/splunk/reconcile/indexer.go | 2 +- pkg/splunk/reconcile/licensemaster.go | 2 +- pkg/splunk/reconcile/searchhead.go | 9 +- pkg/splunk/reconcile/standalone.go | 2 +- 14 files changed, 584 insertions(+), 20 deletions(-) create mode 100644 pkg/splunk/enterprise/restapi.go diff --git a/deploy/all-in-one-cluster.yaml b/deploy/all-in-one-cluster.yaml index 34b2e76ba..25b04754f 100644 --- a/deploy/all-in-one-cluster.yaml +++ b/deploy/all-in-one-cluster.yaml @@ -6676,6 +6676,13 @@ spec: description: SearchHeadStatus defines the observed state of a Splunk Enterprise standalone search head or cluster of search heads properties: + 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: @@ -6687,6 +6694,37 @@ spec: - 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: SearchHeadMemberStatus is used to track the status of + each search head cluster member + properties: + activeSearches: + description: total number of active historical + realtime searches + type: integer + name: + description: Name of the search head cluster member + type: string + registered: + description: true if this member is registered with the search + head captain + type: boolean + status: + description: Status of the search head cluster member + type: string + type: object + type: array + minPeersJoined: + description: true if the minimum number of search head cluster members + have joined + type: boolean phase: description: current phase of the search head cluster enum: diff --git a/deploy/all-in-one-scoped.yaml b/deploy/all-in-one-scoped.yaml index b8357047e..e99852847 100644 --- a/deploy/all-in-one-scoped.yaml +++ b/deploy/all-in-one-scoped.yaml @@ -6676,6 +6676,13 @@ spec: description: SearchHeadStatus defines the observed state of a Splunk Enterprise standalone search head or cluster of search heads properties: + 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: @@ -6687,6 +6694,37 @@ spec: - 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: SearchHeadMemberStatus is used to track the status of + each search head cluster member + properties: + activeSearches: + description: total number of active historical + realtime searches + type: integer + name: + description: Name of the search head cluster member + type: string + registered: + description: true if this member is registered with the search + head captain + type: boolean + status: + description: Status of the search head cluster member + type: string + type: object + type: array + minPeersJoined: + description: true if the minimum number of search head cluster members + have joined + type: boolean phase: description: current phase of the search head cluster enum: diff --git a/deploy/crds/combined.yaml b/deploy/crds/combined.yaml index 47d078388..d63a0dc16 100644 --- a/deploy/crds/combined.yaml +++ b/deploy/crds/combined.yaml @@ -6676,6 +6676,13 @@ spec: description: SearchHeadStatus defines the observed state of a Splunk Enterprise standalone search head or cluster of search heads properties: + 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: @@ -6687,6 +6694,37 @@ spec: - 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: SearchHeadMemberStatus is used to track the status of + each search head cluster member + properties: + activeSearches: + description: total number of active historical + realtime searches + type: integer + name: + description: Name of the search head cluster member + type: string + registered: + description: true if this member is registered with the search + head captain + type: boolean + status: + description: Status of the search head cluster member + type: string + type: object + type: array + minPeersJoined: + description: true if the minimum number of search head cluster members + have joined + type: boolean phase: description: current phase of the search head cluster enum: diff --git a/deploy/crds/enterprise.splunk.com_searchheads_crd.yaml b/deploy/crds/enterprise.splunk.com_searchheads_crd.yaml index 108df8d6a..d4af24f6d 100644 --- a/deploy/crds/enterprise.splunk.com_searchheads_crd.yaml +++ b/deploy/crds/enterprise.splunk.com_searchheads_crd.yaml @@ -2232,6 +2232,13 @@ spec: description: SearchHeadStatus defines the observed state of a Splunk Enterprise standalone search head or cluster of search heads properties: + 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: @@ -2243,6 +2250,37 @@ spec: - 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: SearchHeadMemberStatus is used to track the status of + each search head cluster member + properties: + activeSearches: + description: total number of active historical + realtime searches + type: integer + name: + description: Name of the search head cluster member + type: string + registered: + description: true if this member is registered with the search + head captain + type: boolean + status: + description: Status of the search head cluster member + type: string + type: object + type: array + minPeersJoined: + description: true if the minimum number of search head cluster members + have joined + type: boolean phase: description: current phase of the search head cluster enum: diff --git a/pkg/apis/enterprise/v1alpha2/searchhead_types.go b/pkg/apis/enterprise/v1alpha2/searchhead_types.go index 11ca6702e..2d77d854b 100644 --- a/pkg/apis/enterprise/v1alpha2/searchhead_types.go +++ b/pkg/apis/enterprise/v1alpha2/searchhead_types.go @@ -42,6 +42,21 @@ type SearchHeadSpec struct { SparkImage string `json:"sparkImage"` } +// SearchHeadMemberStatus is used to track the status of each search head cluster member +type SearchHeadMemberStatus struct { + // Name of the search head cluster member + Name string `json:"name"` + + // Status of the search head cluster member + Status string `json:"status"` + + // true if this member is registered with the search head captain + Registered bool `json:"registered"` + + // total number of active historical + realtime searches + ActiveSearches int `json:"activeSearches"` +} + // SearchHeadStatus defines the observed state of a Splunk Enterprise standalone search head or cluster of search heads type SearchHeadStatus struct { // current phase of the search head cluster @@ -58,6 +73,24 @@ type SearchHeadStatus struct { // selector for pods, used by HorizontalPodAutoscaler Selector string `json:"selector"` + + // name or label of the search head captain + Captain string `json:"captain"` + + // true if the search head cluster's captain is ready to service requests + CaptainReady bool `json:"captainReady"` + + // true if the search head cluster has finished initialization + Initialized bool `json:"initialized"` + + // true if the minimum number of search head cluster members have joined + MinPeersJoined bool `json:"minPeersJoined"` + + // true if the search head cluster is in maintenance mode + MaintenanceMode bool `json:"maintenanceMode"` + + // status of each search head cluster member + Members []SearchHeadMemberStatus `json:"members"` } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object diff --git a/pkg/apis/enterprise/v1alpha2/zz_generated.deepcopy.go b/pkg/apis/enterprise/v1alpha2/zz_generated.deepcopy.go index 3a547fba6..0bdda408e 100644 --- a/pkg/apis/enterprise/v1alpha2/zz_generated.deepcopy.go +++ b/pkg/apis/enterprise/v1alpha2/zz_generated.deepcopy.go @@ -248,7 +248,7 @@ func (in *SearchHead) DeepCopyInto(out *SearchHead) { out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) in.Spec.DeepCopyInto(&out.Spec) - out.Status = in.Status + in.Status.DeepCopyInto(&out.Status) return } @@ -303,6 +303,22 @@ func (in *SearchHeadList) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SearchHeadMemberStatus) DeepCopyInto(out *SearchHeadMemberStatus) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SearchHeadMemberStatus. +func (in *SearchHeadMemberStatus) DeepCopy() *SearchHeadMemberStatus { + if in == nil { + return nil + } + out := new(SearchHeadMemberStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *SearchHeadSpec) DeepCopyInto(out *SearchHeadSpec) { *out = *in @@ -324,6 +340,11 @@ func (in *SearchHeadSpec) DeepCopy() *SearchHeadSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *SearchHeadStatus) DeepCopyInto(out *SearchHeadStatus) { *out = *in + if in.Members != nil { + in, out := &in.Members, &out.Members + *out = make([]SearchHeadMemberStatus, len(*in)) + copy(*out, *in) + } return } diff --git a/pkg/splunk/enterprise/configuration.go b/pkg/splunk/enterprise/configuration.go index d30159355..0dd4fd0df 100644 --- a/pkg/splunk/enterprise/configuration.go +++ b/pkg/splunk/enterprise/configuration.go @@ -248,6 +248,67 @@ func ValidateLicenseMasterSpec(spec *enterprisev1.LicenseMasterSpec) error { return validateCommonSplunkSpec(&spec.CommonSplunkSpec) } +// UpdateSearchHeadStatus uses the REST API to update the status for a SearcHead custom resource +func UpdateSearchHeadStatus(cr *enterprisev1.SearchHead, secrets *corev1.Secret) error { + username := "admin" + password := string(secrets.Data["password"]) + + // populate members status using REST API to get search head cluster member info + cr.Status.Members = []enterprisev1.SearchHeadMemberStatus{} + for n := int32(0); n < cr.Spec.Replicas; n++ { + memberName := GetSplunkStatefulsetPodName(SplunkSearchHead, cr.GetIdentifier(), n) + fqdnName := resources.GetServiceFQDN(cr.GetNamespace(), + fmt.Sprintf("%s.%s", memberName, GetSplunkServiceName(SplunkSearchHead, cr.GetIdentifier(), true))) + c := NewSplunkClient(fmt.Sprintf("https://%s:8089", fqdnName), username, password) + memberStatus := enterprisev1.SearchHeadMemberStatus{Name: memberName} + memberInfo, err := c.GetSearchHeadClusterMemberInfo() + if err == nil { + memberStatus.Status = memberInfo.Status + memberStatus.Registered = memberInfo.Registered + memberStatus.ActiveSearches = memberInfo.ActiveHistoricalSearchCount + memberInfo.ActiveRealtimeSearchCount + } + cr.Status.Members = append(cr.Status.Members, memberStatus) + } + + // get search head cluster info from captain + fqdnName := resources.GetServiceFQDN(cr.GetNamespace(), GetSplunkServiceName(SplunkSearchHead, cr.GetIdentifier(), false)) + c := NewSplunkClient(fmt.Sprintf("https://%s:8089", fqdnName), username, password) + captainInfo, err := c.GetSearchHeadCaptainInfo() + if err != nil { + return err + } + cr.Status.Captain = captainInfo.Label + cr.Status.CaptainReady = captainInfo.ServiceReadyFlag + cr.Status.Initialized = captainInfo.InitializedFlag + cr.Status.MinPeersJoined = captainInfo.MinPeersJoinedFlag + cr.Status.MaintenanceMode = captainInfo.MaintenanceMode + + return nil +} + +// DecommissionSearchHead detains and then removes a search head from the cluster +func DecommissionSearchHead(cr *enterprisev1.SearchHead, secrets *corev1.Secret, n int32) (bool, error) { + memberName := GetSplunkStatefulsetPodName(SplunkSearchHead, cr.GetIdentifier(), n) + fqdnName := resources.GetServiceFQDN(cr.GetNamespace(), + fmt.Sprintf("%s.%s", memberName, GetSplunkServiceName(SplunkSearchHead, cr.GetIdentifier(), true))) + c := NewSplunkClient(fmt.Sprintf("https://%s:8089", fqdnName), "admin", string(secrets.Data["password"])) + + switch cr.Status.Members[n].Status { + case "Up": + // Detain search head + return false, c.SetSearchHeadDetention(true) + + case "ManualDetention": + // Wait until active searches have drained + if cr.Status.Members[n].ActiveSearches != 0 { + return false, c.RemoveSearchHeadMember() + } + } + + // completed + return true, nil +} + // GetSplunkDefaults returns a Kubernetes ConfigMap containing defaults for a Splunk Enterprise resource. func GetSplunkDefaults(identifier, namespace string, instanceType InstanceType, defaults string) *corev1.ConfigMap { return &corev1.ConfigMap{ diff --git a/pkg/splunk/enterprise/restapi.go b/pkg/splunk/enterprise/restapi.go new file mode 100644 index 000000000..a07738fcc --- /dev/null +++ b/pkg/splunk/enterprise/restapi.go @@ -0,0 +1,285 @@ +// 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 enterprise + +import ( + "crypto/tls" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" +) + +// SplunkClient is a simple object used to send HTTP REST API requests +type SplunkClient struct { + // https endpoint for management interface (e.g. "https://server:8089") + managementURI string + + // username for authentication + username string + + // password for authentication + password string + + // HTTP client used to process requests + client *http.Client +} + +// NewSplunkClient returns a new SplunkClient object initialized with a username and password +func NewSplunkClient(managementURI, username, password string) *SplunkClient { + return &SplunkClient{ + managementURI: managementURI, + username: username, + password: password, + client: &http.Client{Transport: &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, // don't verify ssl certs + }}, + } +} + +// Do processes a Splunk REST API request and unmarshals response into obj, if not nil +func (c *SplunkClient) Do(request *http.Request, expectedStatus int, obj interface{}) error { + // send HTTP response and check status + request.SetBasicAuth(c.username, c.password) + response, err := c.client.Do(request) + if err != nil { + return err + } + if response.StatusCode != expectedStatus { + return fmt.Errorf("Response code=%d from %s; want %d", response.StatusCode, request.URL, expectedStatus) + } + if obj == nil { + return nil + } + + // unmarshall response if obj != nil + data, _ := ioutil.ReadAll(response.Body) + if len(data) == 0 { + return fmt.Errorf("Received empty response body from %s", request.URL) + } + return json.Unmarshal(data, obj) +} + +// Get sends a REST API request and unmarshals response into obj, if not nil +func (c *SplunkClient) Get(path string, obj interface{}) error { + endpoint := fmt.Sprintf("%s%s?count=0&output_mode=json", c.managementURI, path) + request, err := http.NewRequest("GET", endpoint, nil) + if err != nil { + return err + } + return c.Do(request, 200, obj) +} + +// SearchHeadCaptainInfo represents the status of the search head cluster +// See https://docs.splunk.com/Documentation/Splunk/latest/RESTREF/RESTcluster#shcluster.2Fcaptain.2Finfo +type SearchHeadCaptainInfo struct { + // Id of this SH cluster. This is used as the unique identifier for the Search Head Cluster in bundle replication and acceleration summary management. + Identifier string `json:"id"` + + // Time when the current captain was elected + ElectedCaptain int64 `json:"elected_captain"` + + // Indicates if the searchhead cluster is initialized. + InitializedFlag bool `json:"initialized_flag"` + + // The name for the captain. Displayed on the Splunk Web manager page. + Label string `json:"label"` + + // Indicates if the cluster is in maintenance mode. + MaintenanceMode bool `json:"maintenance_mode"` + + // Flag to indicate if more then replication_factor peers have joined the cluster. + MinPeersJoinedFlag bool `json:"min_peers_joined_flag"` + + // URI of the current captain. + PeerSchemeHostPort string `json:"peer_scheme_host_port"` + + // Indicates whether the captain is restarting the members in a searchhead cluster. + RollingRestartFlag bool `json:"rolling_restart_flag"` + + // Indicates whether the captain is ready to begin servicing, based on whether it is initialized. + ServiceReadyFlag bool `json:"service_ready_flag"` + + // Timestamp corresponding to the creation of the captain. + StartTime int64 `json:"start_time"` +} + +// GetSearchHeadCaptainInfo queries the captain for info about the search head cluster +// See https://docs.splunk.com/Documentation/Splunk/latest/RESTREF/RESTcluster#shcluster.2Fcaptain.2Finfo +func (c *SplunkClient) GetSearchHeadCaptainInfo() (*SearchHeadCaptainInfo, error) { + apiResponse := struct { + Entry []struct { + Content SearchHeadCaptainInfo `json:"content"` + } `json:"entry"` + }{} + path := "/services/shcluster/captain/info" + err := c.Get(path, &apiResponse) + if err != nil { + return nil, err + } + if len(apiResponse.Entry) < 1 { + return nil, fmt.Errorf("Invalid response from %s%s", c.managementURI, path) + } + return &apiResponse.Entry[0].Content, nil +} + +// SearchHeadCaptainMemberInfo represents the status of a search head cluster member (captain endpoint) +// See https://docs.splunk.com/Documentation/Splunk/latest/RESTREF/RESTcluster#shcluster.2Fcaptain.2Fmembers +type SearchHeadCaptainMemberInfo struct { + // Flag that indicates if this member can run scheduled searches. + Adhoc bool `json:"adhoc_searchhead"` + + // Flag to indicate if this peer advertised that it needed a restart. + AdvertiseRestartRequired bool `json:"advertise_restart_required"` + + // Number of artifacts on this peer. + ArtifactCount int `json:"artifact_count"` + + // The host and management port advertised by this peer. + HostPortPair string `json:"host_port_pair"` + + // True if this member is the SHC captain. + Captain bool `json:"is_captain"` + + // Host and port of the kv store instance of this member. + KVStoreHostPort string `json:"kv_store_host_port"` + + // The name for this member. Displayed on the Splunk Web manager page. + Label string `json:"label"` + + // Timestamp for last heartbeat recieved from the peer + LastHeartbeat int64 `json:"last_heartbeat"` + + // REST API endpoint for management + ManagementURI string `json:"mgmt_url"` + + // URI of the current captain. + PeerSchemeHostPort string `json:"peer_scheme_host_port"` + + // Used by the captain to keep track of pending jobs requested by the captain to this member. + PendingJobCount int `json:"pending_job_count"` + + // Number of replications this peer is part of, as either source or target. + ReplicationCount int `json:"replication_count"` + + // TCP port to listen for replicated data from another cluster member. + ReplicationPort int `json:"replication_port"` + + // Indicates whether to use SSL when sending replication data. + ReplicationUseSSL bool `json:"replication_use_ssl"` + + // Indicates the status of the member. + Status string `json:"status"` +} + +// GetSearchHeadCaptainMembers queries the search head captain for info about cluster members +func (c *SplunkClient) GetSearchHeadCaptainMembers() (map[string]SearchHeadCaptainMemberInfo, error) { + // query and parse + // See https://docs.splunk.com/Documentation/Splunk/latest/RESTREF/RESTcluster#shcluster.2Fcaptain.2Fmembers + apiResponse := struct { + Entry []struct { + Content SearchHeadCaptainMemberInfo `json:"content"` + } `json:"entry"` + }{} + path := "/services/shcluster/captain/members" + err := c.Get(path, &apiResponse) + if err != nil { + return nil, err + } + + members := make(map[string]SearchHeadCaptainMemberInfo) + for _, e := range apiResponse.Entry { + members[e.Content.Label] = e.Content + } + + return members, nil +} + +// SearchHeadClusterMemberInfo represents the status of a search head cluster member +// and https://docs.splunk.com/Documentation/Splunk/latest/RESTREF/RESTcluster#shcluster.2Fmember.2Finfo +type SearchHeadClusterMemberInfo struct { + // Number of currently running historical searches. + ActiveHistoricalSearchCount int `json:"active_historical_search_count"` + + // Number of currently running realtime searches. + ActiveRealtimeSearchCount int `json:"active_realtime_search_count"` + + // Flag that indicates if this member can run scheduled searches. + Adhoc bool `json:"adhoc_searchhead"` + + // Indicates if this member is registered with the searchhead cluster captain. + Registered bool `json:"is_registered"` + + // Timestamp for the last attempt to contact the captain. + LastHeartbeatAttempt int64 `json:"last_heartbeat_attempt"` + + // Number of scheduled searches run in the last 15 minutes. + PeerLoadStatsGla15m int `json:"peer_load_stats_gla_15m"` + + // Number of scheduled searches run in the last one minute. + PeerLoadStatsGla1m int `json:"peer_load_stats_gla_1m"` + + // Number of scheduled searches run in the last five minutes. + PeerLoadStatsGla5m int `json:"peer_load_stats_gla_5m"` + + // Indicates whether the member needs to be restarted to enable its searchhead cluster configuration. + RestartState string `json:"restart_state"` + + // Indicates the status of the member. + Status string `json:"status"` +} + +// GetSearchHeadClusterMemberInfo queries info from a search head cluster member using /shcluster/member/info +// See https://docs.splunk.com/Documentation/Splunk/latest/RESTREF/RESTcluster#shcluster.2Fmember.2Finfo +func (c *SplunkClient) GetSearchHeadClusterMemberInfo() (*SearchHeadClusterMemberInfo, error) { + apiResponse := struct { + Entry []struct { + Content SearchHeadClusterMemberInfo `json:"content"` + } `json:"entry"` + }{} + path := "/services/shcluster/member/info" + err := c.Get(path, &apiResponse) + if err != nil { + return nil, err + } + if len(apiResponse.Entry) < 1 { + return nil, fmt.Errorf("Invalid response from %s%s", c.managementURI, path) + } + return &apiResponse.Entry[0].Content, nil +} + +// SetSearchHeadDetention enables or disables detention of a search head cluster member +func (c *SplunkClient) SetSearchHeadDetention(detain bool) error { + mode := "off" + if detain { + mode = "on" + } + endpoint := fmt.Sprintf("%s/services/shcluster/member/control/control/set_manual_detention?manual_detention=%s", c.managementURI, mode) + request, err := http.NewRequest("POST", endpoint, nil) + if err != nil { + return err + } + return c.Do(request, 200, nil) +} + +// RemoveSearchHeadMember removes a search head cluster member +func (c *SplunkClient) RemoveSearchHeadMember() error { + endpoint := fmt.Sprintf("%s/services/shcluster/member/consensus/default/remove_server", c.managementURI) + request, err := http.NewRequest("POST", endpoint, nil) + if err != nil { + return err + } + return c.Do(request, 200, nil) +} diff --git a/pkg/splunk/reconcile/config.go b/pkg/splunk/reconcile/config.go index 2e598df28..8cefe4a20 100644 --- a/pkg/splunk/reconcile/config.go +++ b/pkg/splunk/reconcile/config.go @@ -28,7 +28,7 @@ import ( ) // ReconcileSplunkConfig reconciles the state of Kubernetes Secrets, ConfigMaps and other general settings for Splunk Enterprise instances. -func ReconcileSplunkConfig(client ControllerClient, cr enterprisev1.MetaObject, spec enterprisev1.CommonSplunkSpec, instanceType enterprise.InstanceType) error { +func ReconcileSplunkConfig(client ControllerClient, cr enterprisev1.MetaObject, spec enterprisev1.CommonSplunkSpec, instanceType enterprise.InstanceType) (*corev1.Secret, error) { var err error // if reference to indexer cluster, extract and re-use idxc.secret @@ -37,7 +37,7 @@ func ReconcileSplunkConfig(client ControllerClient, cr enterprisev1.MetaObject, if instanceType.ToKind() != "indexer" && instanceType.ToKind() != "license-master" && spec.IndexerRef.Name != "" { idxcSecret, err = GetSplunkSecret(client, cr, spec.IndexerRef, enterprise.SplunkIndexer, "idxc_secret") if err != nil { - return err + return nil, err } } @@ -46,22 +46,22 @@ func ReconcileSplunkConfig(client ControllerClient, cr enterprisev1.MetaObject, if instanceType.ToKind() != "license-master" && spec.LicenseMasterRef.Name != "" { pass4SymmKey, err = GetSplunkSecret(client, cr, spec.LicenseMasterRef, enterprise.SplunkLicenseMaster, "pass4SymmKey") if err != nil { - return err + return nil, err } if instanceType.ToKind() == "indexer" { // get pass4SymmKey from LicenseMaster to avoid cyclical dependency idxcSecret, err = GetSplunkSecret(client, cr, spec.LicenseMasterRef, enterprise.SplunkLicenseMaster, "idxc_secret") if err != nil { - return err + return nil, err } } } - // create splunk secrets + // create or retrieve splunk secrets secrets := enterprise.GetSplunkSecrets(cr, instanceType, idxcSecret, pass4SymmKey) secrets.SetOwnerReferences(append(secrets.GetOwnerReferences(), resources.AsOwner(cr))) - if err = ApplySecret(client, secrets); err != nil { - return err + if secrets, err = ApplySecret(client, secrets); err != nil { + return nil, err } // create splunk defaults (for inline config) @@ -69,11 +69,11 @@ func ReconcileSplunkConfig(client ControllerClient, cr enterprisev1.MetaObject, defaultsMap := enterprise.GetSplunkDefaults(cr.GetIdentifier(), cr.GetNamespace(), instanceType, spec.Defaults) defaultsMap.SetOwnerReferences(append(defaultsMap.GetOwnerReferences(), resources.AsOwner(cr))) if err = ApplyConfigMap(client, defaultsMap); err != nil { - return err + return nil, err } } - return nil + return secrets, nil } // ApplyConfigMap creates or updates a Kubernetes ConfigMap @@ -101,14 +101,15 @@ func ApplyConfigMap(client ControllerClient, configMap *corev1.ConfigMap) error return err } -// ApplySecret creates or updates a Kubernetes Secret -func ApplySecret(client ControllerClient, secret *corev1.Secret) error { +// ApplySecret creates or updates a Kubernetes Secret, and returns active secrets if successful +func ApplySecret(client ControllerClient, secret *corev1.Secret) (*corev1.Secret, error) { scopedLog := log.WithName("ApplySecret").WithValues( "name", secret.GetObjectMeta().GetName(), "namespace", secret.GetObjectMeta().GetNamespace()) namespacedName := types.NamespacedName{Namespace: secret.GetNamespace(), Name: secret.GetName()} var current corev1.Secret + result := ¤t err := client.Get(context.TODO(), namespacedName, ¤t) if err == nil { @@ -116,9 +117,10 @@ func ApplySecret(client ControllerClient, secret *corev1.Secret) error { scopedLog.Info("Found existing Secret") } else { err = CreateResource(client, secret) + result = secret } - return err + return result, err } // GetSplunkSecret is used to retrieve a secret from another custom resource. diff --git a/pkg/splunk/reconcile/config_test.go b/pkg/splunk/reconcile/config_test.go index 6b804726d..307cb2c63 100644 --- a/pkg/splunk/reconcile/config_test.go +++ b/pkg/splunk/reconcile/config_test.go @@ -44,7 +44,8 @@ func TestReconcileSplunkConfig(t *testing.T) { searchHeadRevised.Spec.Image = "splunk/test" reconcile := func(c *mockClient, cr interface{}) error { obj := cr.(*enterprisev1.SearchHead) - return ReconcileSplunkConfig(c, obj, obj.Spec.CommonSplunkSpec, enterprise.SplunkSearchHead) + _, err := ReconcileSplunkConfig(c, obj, obj.Spec.CommonSplunkSpec, enterprise.SplunkSearchHead) + return err } reconcileTester(t, "TestReconcileSplunkConfig", &searchHeadCR, searchHeadRevised, createCalls, updateCalls, reconcile) @@ -91,7 +92,8 @@ func TestReconcileSplunkConfig(t *testing.T) { indexerRevised.Spec.LicenseMasterRef.Name = "stack2" reconcile = func(c *mockClient, cr interface{}) error { obj := cr.(*enterprisev1.Indexer) - return ReconcileSplunkConfig(c, obj, obj.Spec.CommonSplunkSpec, enterprise.SplunkIndexer) + _, err := ReconcileSplunkConfig(c, obj, obj.Spec.CommonSplunkSpec, enterprise.SplunkIndexer) + return err } funcCalls = []mockFuncCall{ {metaName: "*v1.Secret-test-splunk-stack2-license-master-secrets"}, @@ -134,7 +136,8 @@ func TestApplySecret(t *testing.T) { revised := current.DeepCopy() revised.Data = map[string][]byte{"a": []byte{'1', '2'}} reconcile := func(c *mockClient, cr interface{}) error { - return ApplySecret(c, cr.(*corev1.Secret)) + _, err := ApplySecret(c, cr.(*corev1.Secret)) + return err } reconcileTester(t, "TestApplySecret", ¤t, revised, createCalls, updateCalls, reconcile) } diff --git a/pkg/splunk/reconcile/indexer.go b/pkg/splunk/reconcile/indexer.go index 2a0c18f6e..38afcd82d 100644 --- a/pkg/splunk/reconcile/indexer.go +++ b/pkg/splunk/reconcile/indexer.go @@ -51,7 +51,7 @@ func ReconcileIndexer(client ControllerClient, cr *enterprisev1.Indexer) error { } // create or update general config resources - err = ReconcileSplunkConfig(client, cr, cr.Spec.CommonSplunkSpec, enterprise.SplunkIndexer) + _, err = ReconcileSplunkConfig(client, cr, cr.Spec.CommonSplunkSpec, enterprise.SplunkIndexer) if err != nil { return err } diff --git a/pkg/splunk/reconcile/licensemaster.go b/pkg/splunk/reconcile/licensemaster.go index 1516f5406..e0a74e0ee 100644 --- a/pkg/splunk/reconcile/licensemaster.go +++ b/pkg/splunk/reconcile/licensemaster.go @@ -47,7 +47,7 @@ func ReconcileLicenseMaster(client ControllerClient, cr *enterprisev1.LicenseMas } // create or update general config resources - err = ReconcileSplunkConfig(client, cr, cr.Spec.CommonSplunkSpec, enterprise.SplunkLicenseMaster) + _, err = ReconcileSplunkConfig(client, cr, cr.Spec.CommonSplunkSpec, enterprise.SplunkLicenseMaster) if err != nil { return err } diff --git a/pkg/splunk/reconcile/searchhead.go b/pkg/splunk/reconcile/searchhead.go index 55e2ab8ab..e558e8217 100644 --- a/pkg/splunk/reconcile/searchhead.go +++ b/pkg/splunk/reconcile/searchhead.go @@ -24,6 +24,7 @@ import ( // ReconcileSearchHead reconciles the state for a Splunk Enterprise search head cluster. func ReconcileSearchHead(client ControllerClient, cr *enterprisev1.SearchHead) error { + scopedLog := log.WithName("ReconcileSearchHead").WithValues("name", cr.GetIdentifier(), "namespace", cr.GetNamespace()) // validate and updates defaults for CR err := enterprise.ValidateSearchHeadSpec(&cr.Spec) @@ -50,7 +51,7 @@ func ReconcileSearchHead(client ControllerClient, cr *enterprisev1.SearchHead) e } // create or update general config resources - err = ReconcileSplunkConfig(client, cr, cr.Spec.CommonSplunkSpec, enterprise.SplunkSearchHead) + secrets, err := ReconcileSplunkConfig(client, cr, cr.Spec.CommonSplunkSpec, enterprise.SplunkSearchHead) if err != nil { return err } @@ -94,6 +95,12 @@ func ReconcileSearchHead(client ControllerClient, cr *enterprisev1.SearchHead) e } cr.Status.Phase, err = ApplyStatefulSet(client, statefulSet) cr.Status.ReadyReplicas = statefulSet.Status.ReadyReplicas + if cr.Status.ReadyReplicas > 0 { + err = enterprise.UpdateSearchHeadStatus(cr, secrets) + if err != nil { + scopedLog.Error(err, "Failed to update status") + } + } if err == nil && cr.Status.Phase == enterprisev1.PhaseReady { cr.Status.Phase, err = ReconcileStatefulSetPods(client, statefulSet, cr.Status.ReadyReplicas, cr.Spec.Replicas, nil) } diff --git a/pkg/splunk/reconcile/standalone.go b/pkg/splunk/reconcile/standalone.go index 47ec4b29b..a648682a5 100644 --- a/pkg/splunk/reconcile/standalone.go +++ b/pkg/splunk/reconcile/standalone.go @@ -50,7 +50,7 @@ func ReconcileStandalone(client ControllerClient, cr *enterprisev1.Standalone) e } // create or update general config resources - err = ReconcileSplunkConfig(client, cr, cr.Spec.CommonSplunkSpec, enterprise.SplunkStandalone) + _, err = ReconcileSplunkConfig(client, cr, cr.Spec.CommonSplunkSpec, enterprise.SplunkStandalone) if err != nil { return err } From 4c0fed29365eefc92754c667c4645ee9757c3c62 Mon Sep 17 00:00:00 2001 From: Mike Dickey Date: Tue, 10 Mar 2020 11:40:09 -0700 Subject: [PATCH 3/7] Renamed CRs: SearchHead to SearchHeadCluster and Indexer to IndexerCluster Updated default volume claims for etc and var to 10Gi and 100Gi, respectively Renamed environment variables SPLUNK_IMAGE to RELATED_IMAGE_SPLUNK_ENTERPRISE and SPARK_IMAGE to RELATED_IMAGE_SPLUNK_SPARK (for Red Hat certification) --- Makefile | 4 +- deploy/all-in-one-cluster.yaml | 107 +++++++------ deploy/all-in-one-scoped.yaml | 107 +++++++------ deploy/all-in-one.yaml | 143 ------------------ deploy/cluster_operator.yaml | 4 +- deploy/crds/combined.yaml | 103 +++++++------ ...prise.splunk.com_indexerclusters_crd.yaml} | 36 ++--- ...erprise.splunk.com_licensemasters_crd.yaml | 8 +- ...se.splunk.com_searchheadclusters_crd.yaml} | 47 +++--- .../enterprise.splunk.com_sparks_crd.yaml | 2 +- ...enterprise.splunk.com_standalones_crd.yaml | 10 +- deploy/operator.yaml | 4 +- docs/ChangeLog.md | 12 +- docs/CustomResources.md | 34 ++--- docs/Examples.md | 60 ++++---- docs/Images.md | 8 +- docs/Install.md | 13 +- docs/README.md | 4 +- docs/StorageClass.md | 2 +- pkg/apis/enterprise/v1alpha2/common_types.go | 6 +- ...dexer_types.go => indexercluster_types.go} | 36 ++--- ...ad_types.go => searchheadcluster_types.go} | 48 +++--- .../enterprise/v1alpha2/standalone_types.go | 2 +- .../v1alpha2/zz_generated.deepcopy.go | 88 +++++------ .../{add_indexer.go => add_indexercluster.go} | 4 +- ...searchhead.go => add_searchheadcluster.go} | 4 +- .../indexercluster_controller.go} | 40 ++--- .../searchheadcluster_controller.go} | 40 ++--- pkg/splunk/enterprise/configuration.go | 40 ++--- pkg/splunk/enterprise/configuration_test.go | 66 ++++---- pkg/splunk/enterprise/names.go | 2 +- pkg/splunk/enterprise/names_test.go | 2 +- pkg/splunk/enterprise/restapi.go | 4 +- pkg/splunk/reconcile/config.go | 4 +- pkg/splunk/reconcile/config_test.go | 12 +- pkg/splunk/reconcile/finalizers.go | 4 +- pkg/splunk/reconcile/finalizers_test.go | 8 +- pkg/splunk/reconcile/indexer.go | 6 +- pkg/splunk/reconcile/indexer_test.go | 12 +- pkg/splunk/reconcile/searchhead.go | 10 +- pkg/splunk/reconcile/searchhead_test.go | 12 +- pkg/splunk/reconcile/util_test.go | 8 +- pkg/splunk/spark/names.go | 2 +- pkg/splunk/spark/names_test.go | 2 +- 44 files changed, 516 insertions(+), 654 deletions(-) delete mode 100644 deploy/all-in-one.yaml rename deploy/crds/{enterprise.splunk.com_indexers_crd.yaml => enterprise.splunk.com_indexerclusters_crd.yaml} (99%) rename deploy/crds/{enterprise.splunk.com_searchheads_crd.yaml => enterprise.splunk.com_searchheadclusters_crd.yaml} (99%) rename pkg/apis/enterprise/v1alpha2/{indexer_types.go => indexercluster_types.go} (75%) rename pkg/apis/enterprise/v1alpha2/{searchhead_types.go => searchheadcluster_types.go} (74%) rename pkg/controller/{add_indexer.go => add_indexercluster.go} (51%) rename pkg/controller/{add_searchhead.go => add_searchheadcluster.go} (50%) rename pkg/controller/{indexer/indexer_controller.go => indexercluster/indexercluster_controller.go} (73%) rename pkg/controller/{searchhead/searchhead_controller.go => searchheadcluster/searchheadcluster_controller.go} (72%) diff --git a/Makefile b/Makefile index 6fb0cb6f8..1c2c2f911 100644 --- a/Makefile +++ b/Makefile @@ -77,9 +77,9 @@ generate: @echo "---" >> deploy/crds/combined.yaml @cat deploy/crds/enterprise.splunk.com_licensemasters_crd.yaml >> deploy/crds/combined.yaml @echo "---" >> deploy/crds/combined.yaml - @cat deploy/crds/enterprise.splunk.com_searchheads_crd.yaml >> deploy/crds/combined.yaml + @cat deploy/crds/enterprise.splunk.com_searchheadclusters_crd.yaml >> deploy/crds/combined.yaml @echo "---" >> deploy/crds/combined.yaml - @cat deploy/crds/enterprise.splunk.com_indexers_crd.yaml >> deploy/crds/combined.yaml + @cat deploy/crds/enterprise.splunk.com_indexerclusters_crd.yaml >> deploy/crds/combined.yaml @echo "---" >> deploy/crds/combined.yaml @cat deploy/crds/enterprise.splunk.com_sparks_crd.yaml >> deploy/crds/combined.yaml @echo Rebuilding deploy/all-in-one-scoped.yaml diff --git a/deploy/all-in-one-cluster.yaml b/deploy/all-in-one-cluster.yaml index 25b04754f..d9243af45 100644 --- a/deploy/all-in-one-cluster.yaml +++ b/deploy/all-in-one-cluster.yaml @@ -640,7 +640,7 @@ spec: volume claims (default=”1Gi”) type: string image: - description: Image to use for Splunk pod containers (overrides SPLUNK_IMAGE + description: Image to use for Splunk pod containers (overrides RELATED_IMAGE_SPLUNK_ENTERPRISE environment variables) type: string imagePullPolicy: @@ -650,9 +650,9 @@ spec: - Always - IfNotPresent type: string - indexerRef: - description: IndexerRef refers to a Splunk Enterprise indexer cluster - managed by the operator within Kubernetes + indexerClusterRef: + description: IndexerClusterRef refers to a Splunk Enterprise indexer + cluster managed by the operator within Kubernetes properties: apiVersion: description: API version of the referent. @@ -992,7 +992,7 @@ spec: type: object type: object sparkImage: - description: Image to use for Spark pod containers (overrides SPARK_IMAGE + description: Image to use for Spark pod containers (overrides RELATED_IMAGE_SPLUNK_SPARK environment variables) type: string sparkRef: @@ -2885,7 +2885,7 @@ spec: volume claims (default=”1Gi”) type: string image: - description: Image to use for Splunk pod containers (overrides SPLUNK_IMAGE + description: Image to use for Splunk pod containers (overrides RELATED_IMAGE_SPLUNK_ENTERPRISE environment variables) type: string imagePullPolicy: @@ -2895,9 +2895,9 @@ spec: - Always - IfNotPresent type: string - indexerRef: - description: IndexerRef refers to a Splunk Enterprise indexer cluster - managed by the operator within Kubernetes + indexerClusterRef: + description: IndexerClusterRef refers to a Splunk Enterprise indexer + cluster managed by the operator within Kubernetes properties: apiVersion: description: API version of the referent. @@ -4445,7 +4445,7 @@ spec: apiVersion: apiextensions.k8s.io/v1beta1 kind: CustomResourceDefinition metadata: - name: searchheads.enterprise.splunk.com + name: searchheadclusters.enterprise.splunk.com spec: additionalPrinterColumns: - JSONPath: .status.phase @@ -4457,11 +4457,11 @@ spec: name: Deployer type: string - JSONPath: .status.replicas - description: Number of desired search head replicas + description: Desired number of search head cluster members name: Desired type: integer - JSONPath: .status.readyReplicas - description: Current number of ready search head replicas + description: Current number of ready search head cluster members name: Ready type: integer - JSONPath: .metadata.creationTimestamp @@ -4470,13 +4470,12 @@ spec: type: date group: enterprise.splunk.com names: - kind: SearchHead - listKind: SearchHeadList - plural: searchheads + kind: SearchHeadCluster + listKind: SearchHeadClusterList + plural: searchheadclusters shortNames: - - search - - sh - singular: searchhead + - shc + singular: searchheadcluster scope: Namespaced subresources: scale: @@ -4486,8 +4485,8 @@ spec: status: {} validation: openAPIV3Schema: - description: SearchHead is the Schema for a Splunk Enterprise standalone search - head or cluster of search heads + description: SearchHeadCluster is the Schema for a Splunk Enterprise search + head cluster properties: apiVersion: description: 'APIVersion defines the versioned schema of this representation @@ -4502,8 +4501,8 @@ spec: metadata: type: object spec: - description: SearchHeadSpec defines the desired state of a Splunk Enterprise - standalone search head or cluster of search heads + description: SearchHeadClusterSpec defines the desired state of a Splunk + Enterprise search head cluster properties: affinity: description: Kubernetes Affinity rules that control how pods are assigned @@ -5091,7 +5090,7 @@ spec: volume claims (default=”1Gi”) type: string image: - description: Image to use for Splunk pod containers (overrides SPLUNK_IMAGE + description: Image to use for Splunk pod containers (overrides RELATED_IMAGE_SPLUNK_ENTERPRISE environment variables) type: string imagePullPolicy: @@ -5101,9 +5100,9 @@ spec: - Always - IfNotPresent type: string - indexerRef: - description: IndexerRef refers to a Splunk Enterprise indexer cluster - managed by the operator within Kubernetes + indexerClusterRef: + description: IndexerClusterRef refers to a Splunk Enterprise indexer + cluster managed by the operator within Kubernetes properties: apiVersion: description: API version of the referent. @@ -5444,7 +5443,7 @@ spec: type: object type: object sparkImage: - description: Image to use for Spark pod containers (overrides SPARK_IMAGE + description: Image to use for Spark pod containers (overrides RELATED_IMAGE_SPLUNK_SPARK environment variables) type: string sparkRef: @@ -6673,8 +6672,8 @@ spec: type: array type: object status: - description: SearchHeadStatus defines the observed state of a Splunk Enterprise - standalone search head or cluster of search heads + description: SearchHeadClusterStatus defines the observed state of a Splunk + Enterprise search head cluster properties: captain: description: name or label of the search head captain @@ -6703,8 +6702,8 @@ spec: members: description: status of each search head cluster member items: - description: SearchHeadMemberStatus is used to track the status of - each search head cluster member + description: SearchHeadClusterMemberStatus is used to track the status + of each search head cluster member properties: activeSearches: description: total number of active historical + realtime searches @@ -6737,11 +6736,11 @@ spec: - Error type: string readyReplicas: - description: current number of ready search head replicas + description: current number of ready search head cluster members format: int32 type: integer replicas: - description: number of desired search head replicas + description: desired number of search head cluster members format: int32 type: integer selector: @@ -6758,7 +6757,7 @@ spec: apiVersion: apiextensions.k8s.io/v1beta1 kind: CustomResourceDefinition metadata: - name: indexers.enterprise.splunk.com + name: indexerclusters.enterprise.splunk.com spec: additionalPrinterColumns: - JSONPath: .status.phase @@ -6770,7 +6769,7 @@ spec: name: Master type: string - JSONPath: .status.replicas - description: Number of desired indexer peers + description: Desired number of indexer peers name: Desired type: integer - JSONPath: .status.readyReplicas @@ -6783,12 +6782,13 @@ spec: type: date group: enterprise.splunk.com names: - kind: Indexer - listKind: IndexerList - plural: indexers + kind: IndexerCluster + listKind: IndexerClusterList + plural: indexerclusters shortNames: - - idx - singular: indexer + - idc + - idxc + singular: indexercluster scope: Namespaced subresources: scale: @@ -6798,8 +6798,7 @@ spec: status: {} validation: openAPIV3Schema: - description: Indexer is the Schema for a Splunk Enterprise standalone indexer - or cluster of indexers + description: IndexerCluster is the Schema for a Splunk Enterprise indexer cluster properties: apiVersion: description: 'APIVersion defines the versioned schema of this representation @@ -6814,8 +6813,8 @@ spec: metadata: type: object spec: - description: IndexerSpec defines the desired state of a Splunk Enterprise - standalone indexer or cluster of indexers + description: IndexerClusterSpec defines the desired state of a Splunk Enterprise + indexer cluster properties: affinity: description: Kubernetes Affinity rules that control how pods are assigned @@ -7403,7 +7402,7 @@ spec: volume claims (default=”1Gi”) type: string image: - description: Image to use for Splunk pod containers (overrides SPLUNK_IMAGE + description: Image to use for Splunk pod containers (overrides RELATED_IMAGE_SPLUNK_ENTERPRISE environment variables) type: string imagePullPolicy: @@ -7413,9 +7412,9 @@ spec: - Always - IfNotPresent type: string - indexerRef: - description: IndexerRef refers to a Splunk Enterprise indexer cluster - managed by the operator within Kubernetes + indexerClusterRef: + description: IndexerClusterRef refers to a Splunk Enterprise indexer + cluster managed by the operator within Kubernetes properties: apiVersion: description: API version of the referent. @@ -8943,8 +8942,8 @@ spec: type: array type: object status: - description: IndexerStatus defines the observed state of a Splunk Enterprise - standalone indexer or cluster of indexers + description: IndexerClusterStatus defines the observed state of a Splunk + Enterprise indexer cluster properties: clusterMasterPhase: description: current phase of the cluster master @@ -8973,7 +8972,7 @@ spec: format: int32 type: integer replicas: - description: number of desired indexer peers + description: desired number of indexer peers format: int32 type: integer selector: @@ -9619,7 +9618,7 @@ spec: type: object type: object image: - description: Image to use for Splunk pod containers (overrides SPLUNK_IMAGE + description: Image to use for Splunk pod containers (overrides RELATED_IMAGE_SPLUNK_ENTERPRISE environment variables) type: string imagePullPolicy: @@ -10073,7 +10072,7 @@ spec: fieldPath: metadata.name - name: OPERATOR_NAME value: "splunk-operator" - - name: SPLUNK_IMAGE + - name: RELATED_IMAGE_SPLUNK_ENTERPRISE value: "splunk/splunk:edge" - - name: SPARK_IMAGE + - name: RELATED_IMAGE_SPLUNK_SPARK value: "splunk/spark" diff --git a/deploy/all-in-one-scoped.yaml b/deploy/all-in-one-scoped.yaml index e99852847..6766feb43 100644 --- a/deploy/all-in-one-scoped.yaml +++ b/deploy/all-in-one-scoped.yaml @@ -640,7 +640,7 @@ spec: volume claims (default=”1Gi”) type: string image: - description: Image to use for Splunk pod containers (overrides SPLUNK_IMAGE + description: Image to use for Splunk pod containers (overrides RELATED_IMAGE_SPLUNK_ENTERPRISE environment variables) type: string imagePullPolicy: @@ -650,9 +650,9 @@ spec: - Always - IfNotPresent type: string - indexerRef: - description: IndexerRef refers to a Splunk Enterprise indexer cluster - managed by the operator within Kubernetes + indexerClusterRef: + description: IndexerClusterRef refers to a Splunk Enterprise indexer + cluster managed by the operator within Kubernetes properties: apiVersion: description: API version of the referent. @@ -992,7 +992,7 @@ spec: type: object type: object sparkImage: - description: Image to use for Spark pod containers (overrides SPARK_IMAGE + description: Image to use for Spark pod containers (overrides RELATED_IMAGE_SPLUNK_SPARK environment variables) type: string sparkRef: @@ -2885,7 +2885,7 @@ spec: volume claims (default=”1Gi”) type: string image: - description: Image to use for Splunk pod containers (overrides SPLUNK_IMAGE + description: Image to use for Splunk pod containers (overrides RELATED_IMAGE_SPLUNK_ENTERPRISE environment variables) type: string imagePullPolicy: @@ -2895,9 +2895,9 @@ spec: - Always - IfNotPresent type: string - indexerRef: - description: IndexerRef refers to a Splunk Enterprise indexer cluster - managed by the operator within Kubernetes + indexerClusterRef: + description: IndexerClusterRef refers to a Splunk Enterprise indexer + cluster managed by the operator within Kubernetes properties: apiVersion: description: API version of the referent. @@ -4445,7 +4445,7 @@ spec: apiVersion: apiextensions.k8s.io/v1beta1 kind: CustomResourceDefinition metadata: - name: searchheads.enterprise.splunk.com + name: searchheadclusters.enterprise.splunk.com spec: additionalPrinterColumns: - JSONPath: .status.phase @@ -4457,11 +4457,11 @@ spec: name: Deployer type: string - JSONPath: .status.replicas - description: Number of desired search head replicas + description: Desired number of search head cluster members name: Desired type: integer - JSONPath: .status.readyReplicas - description: Current number of ready search head replicas + description: Current number of ready search head cluster members name: Ready type: integer - JSONPath: .metadata.creationTimestamp @@ -4470,13 +4470,12 @@ spec: type: date group: enterprise.splunk.com names: - kind: SearchHead - listKind: SearchHeadList - plural: searchheads + kind: SearchHeadCluster + listKind: SearchHeadClusterList + plural: searchheadclusters shortNames: - - search - - sh - singular: searchhead + - shc + singular: searchheadcluster scope: Namespaced subresources: scale: @@ -4486,8 +4485,8 @@ spec: status: {} validation: openAPIV3Schema: - description: SearchHead is the Schema for a Splunk Enterprise standalone search - head or cluster of search heads + description: SearchHeadCluster is the Schema for a Splunk Enterprise search + head cluster properties: apiVersion: description: 'APIVersion defines the versioned schema of this representation @@ -4502,8 +4501,8 @@ spec: metadata: type: object spec: - description: SearchHeadSpec defines the desired state of a Splunk Enterprise - standalone search head or cluster of search heads + description: SearchHeadClusterSpec defines the desired state of a Splunk + Enterprise search head cluster properties: affinity: description: Kubernetes Affinity rules that control how pods are assigned @@ -5091,7 +5090,7 @@ spec: volume claims (default=”1Gi”) type: string image: - description: Image to use for Splunk pod containers (overrides SPLUNK_IMAGE + description: Image to use for Splunk pod containers (overrides RELATED_IMAGE_SPLUNK_ENTERPRISE environment variables) type: string imagePullPolicy: @@ -5101,9 +5100,9 @@ spec: - Always - IfNotPresent type: string - indexerRef: - description: IndexerRef refers to a Splunk Enterprise indexer cluster - managed by the operator within Kubernetes + indexerClusterRef: + description: IndexerClusterRef refers to a Splunk Enterprise indexer + cluster managed by the operator within Kubernetes properties: apiVersion: description: API version of the referent. @@ -5444,7 +5443,7 @@ spec: type: object type: object sparkImage: - description: Image to use for Spark pod containers (overrides SPARK_IMAGE + description: Image to use for Spark pod containers (overrides RELATED_IMAGE_SPLUNK_SPARK environment variables) type: string sparkRef: @@ -6673,8 +6672,8 @@ spec: type: array type: object status: - description: SearchHeadStatus defines the observed state of a Splunk Enterprise - standalone search head or cluster of search heads + description: SearchHeadClusterStatus defines the observed state of a Splunk + Enterprise search head cluster properties: captain: description: name or label of the search head captain @@ -6703,8 +6702,8 @@ spec: members: description: status of each search head cluster member items: - description: SearchHeadMemberStatus is used to track the status of - each search head cluster member + description: SearchHeadClusterMemberStatus is used to track the status + of each search head cluster member properties: activeSearches: description: total number of active historical + realtime searches @@ -6737,11 +6736,11 @@ spec: - Error type: string readyReplicas: - description: current number of ready search head replicas + description: current number of ready search head cluster members format: int32 type: integer replicas: - description: number of desired search head replicas + description: desired number of search head cluster members format: int32 type: integer selector: @@ -6758,7 +6757,7 @@ spec: apiVersion: apiextensions.k8s.io/v1beta1 kind: CustomResourceDefinition metadata: - name: indexers.enterprise.splunk.com + name: indexerclusters.enterprise.splunk.com spec: additionalPrinterColumns: - JSONPath: .status.phase @@ -6770,7 +6769,7 @@ spec: name: Master type: string - JSONPath: .status.replicas - description: Number of desired indexer peers + description: Desired number of indexer peers name: Desired type: integer - JSONPath: .status.readyReplicas @@ -6783,12 +6782,13 @@ spec: type: date group: enterprise.splunk.com names: - kind: Indexer - listKind: IndexerList - plural: indexers + kind: IndexerCluster + listKind: IndexerClusterList + plural: indexerclusters shortNames: - - idx - singular: indexer + - idc + - idxc + singular: indexercluster scope: Namespaced subresources: scale: @@ -6798,8 +6798,7 @@ spec: status: {} validation: openAPIV3Schema: - description: Indexer is the Schema for a Splunk Enterprise standalone indexer - or cluster of indexers + description: IndexerCluster is the Schema for a Splunk Enterprise indexer cluster properties: apiVersion: description: 'APIVersion defines the versioned schema of this representation @@ -6814,8 +6813,8 @@ spec: metadata: type: object spec: - description: IndexerSpec defines the desired state of a Splunk Enterprise - standalone indexer or cluster of indexers + description: IndexerClusterSpec defines the desired state of a Splunk Enterprise + indexer cluster properties: affinity: description: Kubernetes Affinity rules that control how pods are assigned @@ -7403,7 +7402,7 @@ spec: volume claims (default=”1Gi”) type: string image: - description: Image to use for Splunk pod containers (overrides SPLUNK_IMAGE + description: Image to use for Splunk pod containers (overrides RELATED_IMAGE_SPLUNK_ENTERPRISE environment variables) type: string imagePullPolicy: @@ -7413,9 +7412,9 @@ spec: - Always - IfNotPresent type: string - indexerRef: - description: IndexerRef refers to a Splunk Enterprise indexer cluster - managed by the operator within Kubernetes + indexerClusterRef: + description: IndexerClusterRef refers to a Splunk Enterprise indexer + cluster managed by the operator within Kubernetes properties: apiVersion: description: API version of the referent. @@ -8943,8 +8942,8 @@ spec: type: array type: object status: - description: IndexerStatus defines the observed state of a Splunk Enterprise - standalone indexer or cluster of indexers + description: IndexerClusterStatus defines the observed state of a Splunk + Enterprise indexer cluster properties: clusterMasterPhase: description: current phase of the cluster master @@ -8973,7 +8972,7 @@ spec: format: int32 type: integer replicas: - description: number of desired indexer peers + description: desired number of indexer peers format: int32 type: integer selector: @@ -9619,7 +9618,7 @@ spec: type: object type: object image: - description: Image to use for Splunk pod containers (overrides SPLUNK_IMAGE + description: Image to use for Splunk pod containers (overrides RELATED_IMAGE_SPLUNK_ENTERPRISE environment variables) type: string imagePullPolicy: @@ -10053,7 +10052,7 @@ spec: fieldPath: metadata.name - name: OPERATOR_NAME value: "splunk-operator" - - name: SPLUNK_IMAGE + - name: RELATED_IMAGE_SPLUNK_ENTERPRISE value: "splunk/splunk:edge" - - name: SPARK_IMAGE + - name: RELATED_IMAGE_SPLUNK_SPARK value: "splunk/spark" diff --git a/deploy/all-in-one.yaml b/deploy/all-in-one.yaml deleted file mode 100644 index 76884125c..000000000 --- a/deploy/all-in-one.yaml +++ /dev/null @@ -1,143 +0,0 @@ ---- -apiVersion: apiextensions.k8s.io/v1beta1 -kind: CustomResourceDefinition -metadata: - labels: - k8s-app: splunk-kubecontroller - name: splunkenterprises.enterprise.splunk.com -spec: - group: enterprise.splunk.com - names: - kind: SplunkEnterprise - listKind: SplunkEnterpriseList - plural: splunkenterprises - shortNames: - - enterprise - singular: splunkenterprise - scope: Namespaced - version: v1alpha1 ---- -apiVersion: rbac.authorization.k8s.io/v1beta1 -kind: ClusterRole -metadata: - labels: - rbac.authorization.k8s.io/aggregate-to-admin: "true" - name: splunk:splunk-enterprise-operator -rules: - - apiGroups: - - enterprise.splunk.com - resources: - - '*' - verbs: - - '*' ---- -apiVersion: v1 -kind: ServiceAccount -metadata: - name: splunk-operator ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: Role -metadata: - creationTimestamp: null - name: splunk-operator -rules: -- apiGroups: - - "" - resources: - - services - - endpoints - - persistentvolumeclaims - - configmaps - - secrets - 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: - - '*' ---- -kind: RoleBinding -apiVersion: rbac.authorization.k8s.io/v1 -metadata: - name: splunk-operator -subjects: -- kind: ServiceAccount - name: splunk-operator -roleRef: - kind: Role - name: splunk-operator - apiGroup: rbac.authorization.k8s.io ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: splunk-operator -spec: - replicas: 1 - selector: - matchLabels: - name: splunk-operator - template: - metadata: - labels: - name: splunk-operator - spec: - serviceAccountName: splunk-operator - containers: - - name: splunk-operator - image: splunk/splunk-operator - ports: - - containerPort: 60000 - name: metrics - command: - - splunk-operator - imagePullPolicy: IfNotPresent - env: - - name: WATCH_NAMESPACE - valueFrom: - fieldRef: - fieldPath: metadata.namespace - - name: POD_NAME - valueFrom: - fieldRef: - fieldPath: metadata.name - - name: OPERATOR_NAME - value: "splunk-operator" - - name: SPLUNK_IMAGE - value: "splunk/splunk:8.0" - - name: SPARK_IMAGE - value: "splunk/spark" diff --git a/deploy/cluster_operator.yaml b/deploy/cluster_operator.yaml index a6317bb96..e1564e861 100644 --- a/deploy/cluster_operator.yaml +++ b/deploy/cluster_operator.yaml @@ -66,7 +66,7 @@ spec: fieldPath: metadata.name - name: OPERATOR_NAME value: "splunk-operator" - - name: SPLUNK_IMAGE + - name: RELATED_IMAGE_SPLUNK_ENTERPRISE value: "splunk/splunk:edge" - - name: SPARK_IMAGE + - name: RELATED_IMAGE_SPLUNK_SPARK value: "splunk/spark" diff --git a/deploy/crds/combined.yaml b/deploy/crds/combined.yaml index d63a0dc16..0c3660f49 100644 --- a/deploy/crds/combined.yaml +++ b/deploy/crds/combined.yaml @@ -640,7 +640,7 @@ spec: volume claims (default=”1Gi”) type: string image: - description: Image to use for Splunk pod containers (overrides SPLUNK_IMAGE + description: Image to use for Splunk pod containers (overrides RELATED_IMAGE_SPLUNK_ENTERPRISE environment variables) type: string imagePullPolicy: @@ -650,9 +650,9 @@ spec: - Always - IfNotPresent type: string - indexerRef: - description: IndexerRef refers to a Splunk Enterprise indexer cluster - managed by the operator within Kubernetes + indexerClusterRef: + description: IndexerClusterRef refers to a Splunk Enterprise indexer + cluster managed by the operator within Kubernetes properties: apiVersion: description: API version of the referent. @@ -992,7 +992,7 @@ spec: type: object type: object sparkImage: - description: Image to use for Spark pod containers (overrides SPARK_IMAGE + description: Image to use for Spark pod containers (overrides RELATED_IMAGE_SPLUNK_SPARK environment variables) type: string sparkRef: @@ -2885,7 +2885,7 @@ spec: volume claims (default=”1Gi”) type: string image: - description: Image to use for Splunk pod containers (overrides SPLUNK_IMAGE + description: Image to use for Splunk pod containers (overrides RELATED_IMAGE_SPLUNK_ENTERPRISE environment variables) type: string imagePullPolicy: @@ -2895,9 +2895,9 @@ spec: - Always - IfNotPresent type: string - indexerRef: - description: IndexerRef refers to a Splunk Enterprise indexer cluster - managed by the operator within Kubernetes + indexerClusterRef: + description: IndexerClusterRef refers to a Splunk Enterprise indexer + cluster managed by the operator within Kubernetes properties: apiVersion: description: API version of the referent. @@ -4445,7 +4445,7 @@ spec: apiVersion: apiextensions.k8s.io/v1beta1 kind: CustomResourceDefinition metadata: - name: searchheads.enterprise.splunk.com + name: searchheadclusters.enterprise.splunk.com spec: additionalPrinterColumns: - JSONPath: .status.phase @@ -4457,11 +4457,11 @@ spec: name: Deployer type: string - JSONPath: .status.replicas - description: Number of desired search head replicas + description: Desired number of search head cluster members name: Desired type: integer - JSONPath: .status.readyReplicas - description: Current number of ready search head replicas + description: Current number of ready search head cluster members name: Ready type: integer - JSONPath: .metadata.creationTimestamp @@ -4470,13 +4470,12 @@ spec: type: date group: enterprise.splunk.com names: - kind: SearchHead - listKind: SearchHeadList - plural: searchheads + kind: SearchHeadCluster + listKind: SearchHeadClusterList + plural: searchheadclusters shortNames: - - search - - sh - singular: searchhead + - shc + singular: searchheadcluster scope: Namespaced subresources: scale: @@ -4486,8 +4485,8 @@ spec: status: {} validation: openAPIV3Schema: - description: SearchHead is the Schema for a Splunk Enterprise standalone search - head or cluster of search heads + description: SearchHeadCluster is the Schema for a Splunk Enterprise search + head cluster properties: apiVersion: description: 'APIVersion defines the versioned schema of this representation @@ -4502,8 +4501,8 @@ spec: metadata: type: object spec: - description: SearchHeadSpec defines the desired state of a Splunk Enterprise - standalone search head or cluster of search heads + description: SearchHeadClusterSpec defines the desired state of a Splunk + Enterprise search head cluster properties: affinity: description: Kubernetes Affinity rules that control how pods are assigned @@ -5091,7 +5090,7 @@ spec: volume claims (default=”1Gi”) type: string image: - description: Image to use for Splunk pod containers (overrides SPLUNK_IMAGE + description: Image to use for Splunk pod containers (overrides RELATED_IMAGE_SPLUNK_ENTERPRISE environment variables) type: string imagePullPolicy: @@ -5101,9 +5100,9 @@ spec: - Always - IfNotPresent type: string - indexerRef: - description: IndexerRef refers to a Splunk Enterprise indexer cluster - managed by the operator within Kubernetes + indexerClusterRef: + description: IndexerClusterRef refers to a Splunk Enterprise indexer + cluster managed by the operator within Kubernetes properties: apiVersion: description: API version of the referent. @@ -5444,7 +5443,7 @@ spec: type: object type: object sparkImage: - description: Image to use for Spark pod containers (overrides SPARK_IMAGE + description: Image to use for Spark pod containers (overrides RELATED_IMAGE_SPLUNK_SPARK environment variables) type: string sparkRef: @@ -6673,8 +6672,8 @@ spec: type: array type: object status: - description: SearchHeadStatus defines the observed state of a Splunk Enterprise - standalone search head or cluster of search heads + description: SearchHeadClusterStatus defines the observed state of a Splunk + Enterprise search head cluster properties: captain: description: name or label of the search head captain @@ -6703,8 +6702,8 @@ spec: members: description: status of each search head cluster member items: - description: SearchHeadMemberStatus is used to track the status of - each search head cluster member + description: SearchHeadClusterMemberStatus is used to track the status + of each search head cluster member properties: activeSearches: description: total number of active historical + realtime searches @@ -6737,11 +6736,11 @@ spec: - Error type: string readyReplicas: - description: current number of ready search head replicas + description: current number of ready search head cluster members format: int32 type: integer replicas: - description: number of desired search head replicas + description: desired number of search head cluster members format: int32 type: integer selector: @@ -6758,7 +6757,7 @@ spec: apiVersion: apiextensions.k8s.io/v1beta1 kind: CustomResourceDefinition metadata: - name: indexers.enterprise.splunk.com + name: indexerclusters.enterprise.splunk.com spec: additionalPrinterColumns: - JSONPath: .status.phase @@ -6770,7 +6769,7 @@ spec: name: Master type: string - JSONPath: .status.replicas - description: Number of desired indexer peers + description: Desired number of indexer peers name: Desired type: integer - JSONPath: .status.readyReplicas @@ -6783,12 +6782,13 @@ spec: type: date group: enterprise.splunk.com names: - kind: Indexer - listKind: IndexerList - plural: indexers + kind: IndexerCluster + listKind: IndexerClusterList + plural: indexerclusters shortNames: - - idx - singular: indexer + - idc + - idxc + singular: indexercluster scope: Namespaced subresources: scale: @@ -6798,8 +6798,7 @@ spec: status: {} validation: openAPIV3Schema: - description: Indexer is the Schema for a Splunk Enterprise standalone indexer - or cluster of indexers + description: IndexerCluster is the Schema for a Splunk Enterprise indexer cluster properties: apiVersion: description: 'APIVersion defines the versioned schema of this representation @@ -6814,8 +6813,8 @@ spec: metadata: type: object spec: - description: IndexerSpec defines the desired state of a Splunk Enterprise - standalone indexer or cluster of indexers + description: IndexerClusterSpec defines the desired state of a Splunk Enterprise + indexer cluster properties: affinity: description: Kubernetes Affinity rules that control how pods are assigned @@ -7403,7 +7402,7 @@ spec: volume claims (default=”1Gi”) type: string image: - description: Image to use for Splunk pod containers (overrides SPLUNK_IMAGE + description: Image to use for Splunk pod containers (overrides RELATED_IMAGE_SPLUNK_ENTERPRISE environment variables) type: string imagePullPolicy: @@ -7413,9 +7412,9 @@ spec: - Always - IfNotPresent type: string - indexerRef: - description: IndexerRef refers to a Splunk Enterprise indexer cluster - managed by the operator within Kubernetes + indexerClusterRef: + description: IndexerClusterRef refers to a Splunk Enterprise indexer + cluster managed by the operator within Kubernetes properties: apiVersion: description: API version of the referent. @@ -8943,8 +8942,8 @@ spec: type: array type: object status: - description: IndexerStatus defines the observed state of a Splunk Enterprise - standalone indexer or cluster of indexers + description: IndexerClusterStatus defines the observed state of a Splunk + Enterprise indexer cluster properties: clusterMasterPhase: description: current phase of the cluster master @@ -8973,7 +8972,7 @@ spec: format: int32 type: integer replicas: - description: number of desired indexer peers + description: desired number of indexer peers format: int32 type: integer selector: @@ -9619,7 +9618,7 @@ spec: type: object type: object image: - description: Image to use for Splunk pod containers (overrides SPLUNK_IMAGE + description: Image to use for Splunk pod containers (overrides RELATED_IMAGE_SPLUNK_ENTERPRISE environment variables) type: string imagePullPolicy: diff --git a/deploy/crds/enterprise.splunk.com_indexers_crd.yaml b/deploy/crds/enterprise.splunk.com_indexerclusters_crd.yaml similarity index 99% rename from deploy/crds/enterprise.splunk.com_indexers_crd.yaml rename to deploy/crds/enterprise.splunk.com_indexerclusters_crd.yaml index b0d9bd857..06571cbdc 100644 --- a/deploy/crds/enterprise.splunk.com_indexers_crd.yaml +++ b/deploy/crds/enterprise.splunk.com_indexerclusters_crd.yaml @@ -1,7 +1,7 @@ apiVersion: apiextensions.k8s.io/v1beta1 kind: CustomResourceDefinition metadata: - name: indexers.enterprise.splunk.com + name: indexerclusters.enterprise.splunk.com spec: additionalPrinterColumns: - JSONPath: .status.phase @@ -13,7 +13,7 @@ spec: name: Master type: string - JSONPath: .status.replicas - description: Number of desired indexer peers + description: Desired number of indexer peers name: Desired type: integer - JSONPath: .status.readyReplicas @@ -26,12 +26,13 @@ spec: type: date group: enterprise.splunk.com names: - kind: Indexer - listKind: IndexerList - plural: indexers + kind: IndexerCluster + listKind: IndexerClusterList + plural: indexerclusters shortNames: - - idx - singular: indexer + - idc + - idxc + singular: indexercluster scope: Namespaced subresources: scale: @@ -41,8 +42,7 @@ spec: status: {} validation: openAPIV3Schema: - description: Indexer is the Schema for a Splunk Enterprise standalone indexer - or cluster of indexers + description: IndexerCluster is the Schema for a Splunk Enterprise indexer cluster properties: apiVersion: description: 'APIVersion defines the versioned schema of this representation @@ -57,8 +57,8 @@ spec: metadata: type: object spec: - description: IndexerSpec defines the desired state of a Splunk Enterprise - standalone indexer or cluster of indexers + description: IndexerClusterSpec defines the desired state of a Splunk Enterprise + indexer cluster properties: affinity: description: Kubernetes Affinity rules that control how pods are assigned @@ -646,7 +646,7 @@ spec: volume claims (default=”1Gi”) type: string image: - description: Image to use for Splunk pod containers (overrides SPLUNK_IMAGE + description: Image to use for Splunk pod containers (overrides RELATED_IMAGE_SPLUNK_ENTERPRISE environment variables) type: string imagePullPolicy: @@ -656,9 +656,9 @@ spec: - Always - IfNotPresent type: string - indexerRef: - description: IndexerRef refers to a Splunk Enterprise indexer cluster - managed by the operator within Kubernetes + indexerClusterRef: + description: IndexerClusterRef refers to a Splunk Enterprise indexer + cluster managed by the operator within Kubernetes properties: apiVersion: description: API version of the referent. @@ -2186,8 +2186,8 @@ spec: type: array type: object status: - description: IndexerStatus defines the observed state of a Splunk Enterprise - standalone indexer or cluster of indexers + description: IndexerClusterStatus defines the observed state of a Splunk + Enterprise indexer cluster properties: clusterMasterPhase: description: current phase of the cluster master @@ -2216,7 +2216,7 @@ spec: format: int32 type: integer replicas: - description: number of desired indexer peers + description: desired number of indexer peers format: int32 type: integer selector: diff --git a/deploy/crds/enterprise.splunk.com_licensemasters_crd.yaml b/deploy/crds/enterprise.splunk.com_licensemasters_crd.yaml index ffca31f4d..7446c19be 100644 --- a/deploy/crds/enterprise.splunk.com_licensemasters_crd.yaml +++ b/deploy/crds/enterprise.splunk.com_licensemasters_crd.yaml @@ -629,7 +629,7 @@ spec: volume claims (default=”1Gi”) type: string image: - description: Image to use for Splunk pod containers (overrides SPLUNK_IMAGE + description: Image to use for Splunk pod containers (overrides RELATED_IMAGE_SPLUNK_ENTERPRISE environment variables) type: string imagePullPolicy: @@ -639,9 +639,9 @@ spec: - Always - IfNotPresent type: string - indexerRef: - description: IndexerRef refers to a Splunk Enterprise indexer cluster - managed by the operator within Kubernetes + indexerClusterRef: + description: IndexerClusterRef refers to a Splunk Enterprise indexer + cluster managed by the operator within Kubernetes properties: apiVersion: description: API version of the referent. diff --git a/deploy/crds/enterprise.splunk.com_searchheads_crd.yaml b/deploy/crds/enterprise.splunk.com_searchheadclusters_crd.yaml similarity index 99% rename from deploy/crds/enterprise.splunk.com_searchheads_crd.yaml rename to deploy/crds/enterprise.splunk.com_searchheadclusters_crd.yaml index d4af24f6d..3e68f21cc 100644 --- a/deploy/crds/enterprise.splunk.com_searchheads_crd.yaml +++ b/deploy/crds/enterprise.splunk.com_searchheadclusters_crd.yaml @@ -1,7 +1,7 @@ apiVersion: apiextensions.k8s.io/v1beta1 kind: CustomResourceDefinition metadata: - name: searchheads.enterprise.splunk.com + name: searchheadclusters.enterprise.splunk.com spec: additionalPrinterColumns: - JSONPath: .status.phase @@ -13,11 +13,11 @@ spec: name: Deployer type: string - JSONPath: .status.replicas - description: Number of desired search head replicas + description: Desired number of search head cluster members name: Desired type: integer - JSONPath: .status.readyReplicas - description: Current number of ready search head replicas + description: Current number of ready search head cluster members name: Ready type: integer - JSONPath: .metadata.creationTimestamp @@ -26,13 +26,12 @@ spec: type: date group: enterprise.splunk.com names: - kind: SearchHead - listKind: SearchHeadList - plural: searchheads + kind: SearchHeadCluster + listKind: SearchHeadClusterList + plural: searchheadclusters shortNames: - - search - - sh - singular: searchhead + - shc + singular: searchheadcluster scope: Namespaced subresources: scale: @@ -42,8 +41,8 @@ spec: status: {} validation: openAPIV3Schema: - description: SearchHead is the Schema for a Splunk Enterprise standalone search - head or cluster of search heads + description: SearchHeadCluster is the Schema for a Splunk Enterprise search + head cluster properties: apiVersion: description: 'APIVersion defines the versioned schema of this representation @@ -58,8 +57,8 @@ spec: metadata: type: object spec: - description: SearchHeadSpec defines the desired state of a Splunk Enterprise - standalone search head or cluster of search heads + description: SearchHeadClusterSpec defines the desired state of a Splunk + Enterprise search head cluster properties: affinity: description: Kubernetes Affinity rules that control how pods are assigned @@ -647,7 +646,7 @@ spec: volume claims (default=”1Gi”) type: string image: - description: Image to use for Splunk pod containers (overrides SPLUNK_IMAGE + description: Image to use for Splunk pod containers (overrides RELATED_IMAGE_SPLUNK_ENTERPRISE environment variables) type: string imagePullPolicy: @@ -657,9 +656,9 @@ spec: - Always - IfNotPresent type: string - indexerRef: - description: IndexerRef refers to a Splunk Enterprise indexer cluster - managed by the operator within Kubernetes + indexerClusterRef: + description: IndexerClusterRef refers to a Splunk Enterprise indexer + cluster managed by the operator within Kubernetes properties: apiVersion: description: API version of the referent. @@ -1000,7 +999,7 @@ spec: type: object type: object sparkImage: - description: Image to use for Spark pod containers (overrides SPARK_IMAGE + description: Image to use for Spark pod containers (overrides RELATED_IMAGE_SPLUNK_SPARK environment variables) type: string sparkRef: @@ -2229,8 +2228,8 @@ spec: type: array type: object status: - description: SearchHeadStatus defines the observed state of a Splunk Enterprise - standalone search head or cluster of search heads + description: SearchHeadClusterStatus defines the observed state of a Splunk + Enterprise search head cluster properties: captain: description: name or label of the search head captain @@ -2259,8 +2258,8 @@ spec: members: description: status of each search head cluster member items: - description: SearchHeadMemberStatus is used to track the status of - each search head cluster member + description: SearchHeadClusterMemberStatus is used to track the status + of each search head cluster member properties: activeSearches: description: total number of active historical + realtime searches @@ -2293,11 +2292,11 @@ spec: - Error type: string readyReplicas: - description: current number of ready search head replicas + description: current number of ready search head cluster members format: int32 type: integer replicas: - description: number of desired search head replicas + description: desired number of search head cluster members format: int32 type: integer selector: diff --git a/deploy/crds/enterprise.splunk.com_sparks_crd.yaml b/deploy/crds/enterprise.splunk.com_sparks_crd.yaml index 20e99df96..e5552ebe1 100644 --- a/deploy/crds/enterprise.splunk.com_sparks_crd.yaml +++ b/deploy/crds/enterprise.splunk.com_sparks_crd.yaml @@ -630,7 +630,7 @@ spec: type: object type: object image: - description: Image to use for Splunk pod containers (overrides SPLUNK_IMAGE + description: Image to use for Splunk pod containers (overrides RELATED_IMAGE_SPLUNK_ENTERPRISE environment variables) type: string imagePullPolicy: diff --git a/deploy/crds/enterprise.splunk.com_standalones_crd.yaml b/deploy/crds/enterprise.splunk.com_standalones_crd.yaml index 2f601b7cd..e79365f0f 100644 --- a/deploy/crds/enterprise.splunk.com_standalones_crd.yaml +++ b/deploy/crds/enterprise.splunk.com_standalones_crd.yaml @@ -639,7 +639,7 @@ spec: volume claims (default=”1Gi”) type: string image: - description: Image to use for Splunk pod containers (overrides SPLUNK_IMAGE + description: Image to use for Splunk pod containers (overrides RELATED_IMAGE_SPLUNK_ENTERPRISE environment variables) type: string imagePullPolicy: @@ -649,9 +649,9 @@ spec: - Always - IfNotPresent type: string - indexerRef: - description: IndexerRef refers to a Splunk Enterprise indexer cluster - managed by the operator within Kubernetes + indexerClusterRef: + description: IndexerClusterRef refers to a Splunk Enterprise indexer + cluster managed by the operator within Kubernetes properties: apiVersion: description: API version of the referent. @@ -991,7 +991,7 @@ spec: type: object type: object sparkImage: - description: Image to use for Spark pod containers (overrides SPARK_IMAGE + description: Image to use for Spark pod containers (overrides RELATED_IMAGE_SPLUNK_SPARK environment variables) type: string sparkRef: diff --git a/deploy/operator.yaml b/deploy/operator.yaml index c8bb7c135..f9b8e77ce 100644 --- a/deploy/operator.yaml +++ b/deploy/operator.yaml @@ -46,7 +46,7 @@ spec: fieldPath: metadata.name - name: OPERATOR_NAME value: "splunk-operator" - - name: SPLUNK_IMAGE + - name: RELATED_IMAGE_SPLUNK_ENTERPRISE value: "splunk/splunk:edge" - - name: SPARK_IMAGE + - name: RELATED_IMAGE_SPLUNK_SPARK value: "splunk/spark" diff --git a/docs/ChangeLog.md b/docs/ChangeLog.md index 09f9758e5..8314a411a 100644 --- a/docs/ChangeLog.md +++ b/docs/ChangeLog.md @@ -8,12 +8,12 @@ * The API has been updated to v1alpha2, and involves the replacement of the SplunkEnterprise custom resource with 5 new custom resources: - Spark, LicenseMaster, Standalone, SearchHead and Indexer. Please read the - revised [Custom Resources](CustomResources.md) and [Examples](Examples.md) - documentation for details on all the changes. This is a major update and is - not backwards-compatible. You will have to completely remove any older - versions, and any resources managed by the operator, before upgrading to - this release. + Spark, LicenseMaster, Standalone, SearchHeadCluster and IndexerCluster. + Please read the revised [Custom Resources](CustomResources.md) and + [Examples](Examples.md) documentation for details on all the changes. This + is a major update and is not backwards-compatible. You will have to + completely remove any older versions, and any resources managed by the + operator, before upgrading to this release. * A new serviceTemplate spec parameter has been added for all Splunk Enterprise custom resources. This may be used to define a template the operator uses for diff --git a/docs/CustomResources.md b/docs/CustomResources.md index 7278b9d68..613018794 100644 --- a/docs/CustomResources.md +++ b/docs/CustomResources.md @@ -10,8 +10,8 @@ you can use to manage Splunk Enterprise deployments in your Kubernetes cluster. * [Spark Resource Spec Parameters](#spark-resource-spec-parameters) * [LicenseMaster Resource Spec Parameters](#licensemaster-resource-spec-parameters) * [Standalone Resource Spec Parameters](#standalone-resource-spec-parameters) -* [SearchHead Resource Spec Parameters](#searchhead-resource-spec-parameters) -* [Indexer Resource Spec Parameters](#indexer-resource-spec-parameters) +* [SearchHeadCluster Resource Spec Parameters](#searchheadcluster-resource-spec-parameters) +* [IndexerCluster Resource Spec Parameters](#indexercluster-resource-spec-parameters) For examples on how to use these custom resources, please see [Configuring Splunk Enterprise Deployments](Examples.md). @@ -70,7 +70,7 @@ configuration parameters: | Key | Type | Description | | --------------------- | ---------- | ---------------------------------------------------------------------------------------------------------- | -| image | string | Container image to use for pod instances (overrides `SPLUNK_IMAGE` or `SPARK_IMAGE` environment variables) | +| image | string | Container image to use for pod instances (overrides `RELATED_IMAGE_SPLUNK_ENTERPRISE` or `RELATED_IMAGE_SPLUNK_SPARK` environment variables) | | imagePullPolicy | string | Sets pull policy for all images (either "Always" or the default: "IfNotPresent") | | schedulerName | string | Name of [Scheduler](https://kubernetes.io/docs/concepts/scheduling/kube-scheduler/) to use for pod placement (defaults to "default-scheduler") | | affinity | [Affinity](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.17/#affinity-v1-core) | [Kubernetes Affinity](https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#affinity-and-anti-affinity) rules that control how pods are assigned to particular nodes | @@ -95,25 +95,25 @@ spec: name: splunk-licenses licenseMasterRef: name: example - indexerRef: + indexerClusterRef: name: example ``` The following additional configuration parameters may be used for all Splunk -Enterprise resources, including: `Standalone`, `LicenseMaster`, `SearchHead`, -and `Indexer`: +Enterprise resources, including: `Standalone`, `LicenseMaster`, +`SearchHeadCluster`, and `IndexerCluster`: | 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="1Gi") | -| varStorage | string | Storage capacity to request for Splunk var volume claims (default="200Gi") | +| 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") | | 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 | | 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 | -| indexerRef | [ObjectReference](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.17/#objectreference-v1-core) | Reference to a Splunk Operator managed `Indexer` instance (via `name` and optionally `namespace`) to use for indexing | +| indexerClusterRef | [ObjectReference](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.17/#objectreference-v1-core) | Reference to a Splunk Operator managed `IndexerCluster` instance (via `name` and optionally `namespace`) to use for indexing | ## Spark Resource Spec Parameters @@ -175,15 +175,15 @@ the `Standalone` resource provides the following `Spec` configuration parameters | Key | Type | Description | | ---------- | ------- | ------------------------------------------------- | | replicas | integer | The number of standalone replicas (defaults to 1) | -| sparkImage | string | Container image Data Fabric Search (DFS) will use for JDK and Spark libraries (overrides `SPARK_IMAGE` environment variables) | +| sparkImage | string | Container image Data Fabric Search (DFS) will use for JDK and Spark libraries (overrides `RELATED_IMAGE_SPLUNK_SPARK` environment variables) | | sparkRef | [ObjectReference](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.17/#objectreference-v1-core) | Reference to a Splunk Operator managed `Spark` instance (via `name` and optionally `namespace`). When defined, Data Fabric Search (DFS) will be enabled and configured to use it. | -## SearchHead Resource Spec Parameters +## SearchHeadCluster Resource Spec Parameters ```yaml apiVersion: enterprise.splunk.com/v1alpha2 -kind: SearchHead +kind: SearchHeadCluster metadata: name: example spec: @@ -195,20 +195,20 @@ spec: In addition to [Common Spec Parameters for All Resources](#common-spec-parameters-for-all-resources) and [Common Spec Parameters for All Splunk Enterprise Resources](#common-spec-parameters-for-all-splunk-enterprise-resources), -the `SearchHead` resource provides the following `Spec` configuration parameters: +the `SearchHeadCluster` resource provides the following `Spec` configuration parameters: | Key | Type | Description | | ---------- | ------- | ------------------------------------------------------------------------------- | | replicas | integer | The number of search heads cluster members (minimum of 3, which is the default) | -| sparkImage | string | Container image Data Fabric Search (DFS) will use for JDK and Spark libraries (overrides `SPARK_IMAGE` environment variables) | +| sparkImage | string | Container image Data Fabric Search (DFS) will use for JDK and Spark libraries (overrides `RELATED_IMAGE_SPLUNK_SPARK` environment variables) | | sparkRef | [ObjectReference](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.17/#objectreference-v1-core) | Reference to a Splunk Operator managed `Spark` instance (via `name` and optionally `namespace`). When defined, Data Fabric Search (DFS) will be enabled and configured to use it. | -## Indexer Resource Spec Parameters +## IndexerCluster Resource Spec Parameters ```yaml apiVersion: enterprise.splunk.com/v1alpha2 -kind: Indexer +kind: IndexerCluster metadata: name: example spec: @@ -217,7 +217,7 @@ spec: In addition to [Common Spec Parameters for All Resources](#common-spec-parameters-for-all-resources) and [Common Spec Parameters for All Splunk Enterprise Resources](#common-spec-parameters-for-all-splunk-enterprise-resources), -the `Indexer` resource provides the following `Spec` configuration parameters: +the `IndexerCluster` resource provides the following `Spec` configuration parameters: | Key | Type | Description | | ---------- | ------- | ----------------------------------------------------- | diff --git a/docs/Examples.md b/docs/Examples.md index e783bb23e..d2abf4618 100644 --- a/docs/Examples.md +++ b/docs/Examples.md @@ -37,12 +37,12 @@ metadata: When growing, customers will typically want to first expand by upgrading to an [indexer cluster](https://docs.splunk.com/Documentation/Splunk/latest/Indexer/Aboutindexesandindexers). -The Splunk Operator makes creation of an indexer cluster as easy as creating an `Indexer` resource: +The Splunk Operator makes creation of an indexer cluster as easy as creating an `IndexerCluster` resource: ```yaml cat < 1 Replicas int32 `json:"replicas"` } -// IndexerStatus defines the observed state of a Splunk Enterprise standalone indexer or cluster of indexers -type IndexerStatus struct { +// IndexerClusterStatus defines the observed state of a Splunk Enterprise indexer cluster +type IndexerClusterStatus struct { // current phase of the indexer cluster Phase ResourcePhase `json:"phase"` // current phase of the cluster master ClusterMasterPhase ResourcePhase `json:"clusterMasterPhase"` - // number of desired indexer peers + // desired number of indexer peers Replicas int32 `json:"replicas"` // current number of ready indexer peers @@ -54,47 +54,47 @@ type IndexerStatus struct { // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object -// Indexer is the Schema for a Splunk Enterprise standalone indexer or cluster of indexers +// IndexerCluster is the Schema for a Splunk Enterprise indexer cluster // +kubebuilder:subresource:status // +kubebuilder:subresource:scale:specpath=.spec.replicas,statuspath=.status.replicas,selectorpath=.status.selector -// +kubebuilder:resource:path=indexers,scope=Namespaced,shortName=idx +// +kubebuilder:resource:path=indexerclusters,scope=Namespaced,shortName=idc;idxc // +kubebuilder:printcolumn:name="Phase",type="string",JSONPath=".status.phase",description="Status of indexer cluster" // +kubebuilder:printcolumn:name="Master",type="string",JSONPath=".status.clusterMasterPhase",description="Status of cluster master" -// +kubebuilder:printcolumn:name="Desired",type="integer",JSONPath=".status.replicas",description="Number of desired indexer peers" +// +kubebuilder:printcolumn:name="Desired",type="integer",JSONPath=".status.replicas",description="Desired number of indexer peers" // +kubebuilder:printcolumn:name="Ready",type="integer",JSONPath=".status.readyReplicas",description="Current number of ready indexer peers" // +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="Age of indexer cluster" -type Indexer struct { +type IndexerCluster struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` - Spec IndexerSpec `json:"spec,omitempty"` - Status IndexerStatus `json:"status,omitempty"` + Spec IndexerClusterSpec `json:"spec,omitempty"` + Status IndexerClusterStatus `json:"status,omitempty"` } // GetIdentifier is a convenience function to return unique identifier for the Splunk enterprise deployment -func (cr *Indexer) GetIdentifier() string { +func (cr *IndexerCluster) GetIdentifier() string { return cr.ObjectMeta.Name } // GetNamespace is a convenience function to return namespace for a Splunk enterprise deployment -func (cr *Indexer) GetNamespace() string { +func (cr *IndexerCluster) GetNamespace() string { return cr.ObjectMeta.Namespace } // GetTypeMeta is a convenience function to return a TypeMeta object -func (cr *Indexer) GetTypeMeta() metav1.TypeMeta { +func (cr *IndexerCluster) GetTypeMeta() metav1.TypeMeta { return cr.TypeMeta } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object -// IndexerList contains a list of Indexer -type IndexerList struct { +// IndexerClusterList contains a list of IndexerCluster +type IndexerClusterList struct { metav1.TypeMeta `json:",inline"` metav1.ListMeta `json:"metadata,omitempty"` - Items []Indexer `json:"items"` + Items []IndexerCluster `json:"items"` } func init() { - SchemeBuilder.Register(&Indexer{}, &IndexerList{}) + SchemeBuilder.Register(&IndexerCluster{}, &IndexerClusterList{}) } diff --git a/pkg/apis/enterprise/v1alpha2/searchhead_types.go b/pkg/apis/enterprise/v1alpha2/searchheadcluster_types.go similarity index 74% rename from pkg/apis/enterprise/v1alpha2/searchhead_types.go rename to pkg/apis/enterprise/v1alpha2/searchheadcluster_types.go index 2d77d854b..e67df0584 100644 --- a/pkg/apis/enterprise/v1alpha2/searchhead_types.go +++ b/pkg/apis/enterprise/v1alpha2/searchheadcluster_types.go @@ -27,8 +27,8 @@ import ( // Add custom validation using kubebuilder tags: https://book-v1.book.kubebuilder.io/beyond_basics/generating_crd.html // see also https://book.kubebuilder.io/reference/markers/crd.html -// SearchHeadSpec defines the desired state of a Splunk Enterprise standalone search head or cluster of search heads -type SearchHeadSpec struct { +// SearchHeadClusterSpec defines the desired state of a Splunk Enterprise search head cluster +type SearchHeadClusterSpec struct { CommonSplunkSpec `json:",inline"` // Number of search head pods; a search head cluster will be created if > 1 @@ -38,12 +38,12 @@ type SearchHeadSpec struct { // When defined, Data Fabric Search (DFS) will be enabled and configured to use the Spark cluster. SparkRef corev1.ObjectReference `json:"sparkRef"` - // Image to use for Spark pod containers (overrides SPARK_IMAGE environment variables) + // Image to use for Spark pod containers (overrides RELATED_IMAGE_SPLUNK_SPARK environment variables) SparkImage string `json:"sparkImage"` } -// SearchHeadMemberStatus is used to track the status of each search head cluster member -type SearchHeadMemberStatus struct { +// SearchHeadClusterMemberStatus is used to track the status of each search head cluster member +type SearchHeadClusterMemberStatus struct { // Name of the search head cluster member Name string `json:"name"` @@ -57,18 +57,18 @@ type SearchHeadMemberStatus struct { ActiveSearches int `json:"activeSearches"` } -// SearchHeadStatus defines the observed state of a Splunk Enterprise standalone search head or cluster of search heads -type SearchHeadStatus struct { +// SearchHeadClusterStatus defines the observed state of a Splunk Enterprise search head cluster +type SearchHeadClusterStatus struct { // current phase of the search head cluster Phase ResourcePhase `json:"phase"` // current phase of the deployer DeployerPhase ResourcePhase `json:"deployerPhase"` - // number of desired search head replicas + // desired number of search head cluster members Replicas int32 `json:"replicas"` - // current number of ready search head replicas + // current number of ready search head cluster members ReadyReplicas int32 `json:"readyReplicas"` // selector for pods, used by HorizontalPodAutoscaler @@ -90,52 +90,52 @@ type SearchHeadStatus struct { MaintenanceMode bool `json:"maintenanceMode"` // status of each search head cluster member - Members []SearchHeadMemberStatus `json:"members"` + Members []SearchHeadClusterMemberStatus `json:"members"` } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object -// SearchHead is the Schema for a Splunk Enterprise standalone search head or cluster of search heads +// SearchHeadCluster is the Schema for a Splunk Enterprise search head cluster // +kubebuilder:subresource:status // +kubebuilder:subresource:scale:specpath=.spec.replicas,statuspath=.status.replicas,selectorpath=.status.selector -// +kubebuilder:resource:path=searchheads,scope=Namespaced,shortName=search;sh +// +kubebuilder:resource:path=searchheadclusters,scope=Namespaced,shortName=shc // +kubebuilder:printcolumn:name="Phase",type="string",JSONPath=".status.phase",description="Status of search head cluster" // +kubebuilder:printcolumn:name="Deployer",type="string",JSONPath=".status.deployerPhase",description="Status of the deployer" -// +kubebuilder:printcolumn:name="Desired",type="integer",JSONPath=".status.replicas",description="Number of desired search head replicas" -// +kubebuilder:printcolumn:name="Ready",type="integer",JSONPath=".status.readyReplicas",description="Current number of ready search head replicas" +// +kubebuilder:printcolumn:name="Desired",type="integer",JSONPath=".status.replicas",description="Desired number of search head cluster members" +// +kubebuilder:printcolumn:name="Ready",type="integer",JSONPath=".status.readyReplicas",description="Current number of ready search head cluster members" // +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="Age of search head cluster" -type SearchHead struct { +type SearchHeadCluster struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` - Spec SearchHeadSpec `json:"spec,omitempty"` - Status SearchHeadStatus `json:"status,omitempty"` + Spec SearchHeadClusterSpec `json:"spec,omitempty"` + Status SearchHeadClusterStatus `json:"status,omitempty"` } // GetIdentifier is a convenience function to return unique identifier for the Splunk enterprise deployment -func (cr *SearchHead) GetIdentifier() string { +func (cr *SearchHeadCluster) GetIdentifier() string { return cr.ObjectMeta.Name } // GetNamespace is a convenience function to return namespace for a Splunk enterprise deployment -func (cr *SearchHead) GetNamespace() string { +func (cr *SearchHeadCluster) GetNamespace() string { return cr.ObjectMeta.Namespace } // GetTypeMeta is a convenience function to return a TypeMeta object -func (cr *SearchHead) GetTypeMeta() metav1.TypeMeta { +func (cr *SearchHeadCluster) GetTypeMeta() metav1.TypeMeta { return cr.TypeMeta } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object -// SearchHeadList contains a list of SearcHead -type SearchHeadList struct { +// SearchHeadClusterList contains a list of SearcHead +type SearchHeadClusterList struct { metav1.TypeMeta `json:",inline"` metav1.ListMeta `json:"metadata,omitempty"` - Items []SearchHead `json:"items"` + Items []SearchHeadCluster `json:"items"` } func init() { - SchemeBuilder.Register(&SearchHead{}, &SearchHeadList{}) + SchemeBuilder.Register(&SearchHeadCluster{}, &SearchHeadClusterList{}) } diff --git a/pkg/apis/enterprise/v1alpha2/standalone_types.go b/pkg/apis/enterprise/v1alpha2/standalone_types.go index a1564b001..5b2c4b4dc 100644 --- a/pkg/apis/enterprise/v1alpha2/standalone_types.go +++ b/pkg/apis/enterprise/v1alpha2/standalone_types.go @@ -38,7 +38,7 @@ type StandaloneSpec struct { // When defined, Data Fabric Search (DFS) will be enabled and configured to use the Spark cluster. SparkRef corev1.ObjectReference `json:"sparkRef"` - // Image to use for Spark pod containers (overrides SPARK_IMAGE environment variables) + // Image to use for Spark pod containers (overrides RELATED_IMAGE_SPLUNK_SPARK environment variables) SparkImage string `json:"sparkImage"` } diff --git a/pkg/apis/enterprise/v1alpha2/zz_generated.deepcopy.go b/pkg/apis/enterprise/v1alpha2/zz_generated.deepcopy.go index 0bdda408e..3660d2a24 100644 --- a/pkg/apis/enterprise/v1alpha2/zz_generated.deepcopy.go +++ b/pkg/apis/enterprise/v1alpha2/zz_generated.deepcopy.go @@ -40,7 +40,7 @@ func (in *CommonSplunkSpec) DeepCopyInto(out *CommonSplunkSpec) { } } out.LicenseMasterRef = in.LicenseMasterRef - out.IndexerRef = in.IndexerRef + out.IndexerClusterRef = in.IndexerClusterRef return } @@ -55,7 +55,7 @@ func (in *CommonSplunkSpec) DeepCopy() *CommonSplunkSpec { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *Indexer) DeepCopyInto(out *Indexer) { +func (in *IndexerCluster) DeepCopyInto(out *IndexerCluster) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) @@ -64,18 +64,18 @@ func (in *Indexer) DeepCopyInto(out *Indexer) { return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Indexer. -func (in *Indexer) DeepCopy() *Indexer { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IndexerCluster. +func (in *IndexerCluster) DeepCopy() *IndexerCluster { if in == nil { return nil } - out := new(Indexer) + out := new(IndexerCluster) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *Indexer) DeepCopyObject() runtime.Object { +func (in *IndexerCluster) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } @@ -83,13 +83,13 @@ func (in *Indexer) DeepCopyObject() runtime.Object { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *IndexerList) DeepCopyInto(out *IndexerList) { +func (in *IndexerClusterList) DeepCopyInto(out *IndexerClusterList) { *out = *in out.TypeMeta = in.TypeMeta in.ListMeta.DeepCopyInto(&out.ListMeta) if in.Items != nil { in, out := &in.Items, &out.Items - *out = make([]Indexer, len(*in)) + *out = make([]IndexerCluster, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } @@ -97,18 +97,18 @@ func (in *IndexerList) DeepCopyInto(out *IndexerList) { return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IndexerList. -func (in *IndexerList) DeepCopy() *IndexerList { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IndexerClusterList. +func (in *IndexerClusterList) DeepCopy() *IndexerClusterList { if in == nil { return nil } - out := new(IndexerList) + out := new(IndexerClusterList) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *IndexerList) DeepCopyObject() runtime.Object { +func (in *IndexerClusterList) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } @@ -116,34 +116,34 @@ func (in *IndexerList) DeepCopyObject() runtime.Object { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *IndexerSpec) DeepCopyInto(out *IndexerSpec) { +func (in *IndexerClusterSpec) DeepCopyInto(out *IndexerClusterSpec) { *out = *in in.CommonSplunkSpec.DeepCopyInto(&out.CommonSplunkSpec) return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IndexerSpec. -func (in *IndexerSpec) DeepCopy() *IndexerSpec { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IndexerClusterSpec. +func (in *IndexerClusterSpec) DeepCopy() *IndexerClusterSpec { if in == nil { return nil } - out := new(IndexerSpec) + out := new(IndexerClusterSpec) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *IndexerStatus) DeepCopyInto(out *IndexerStatus) { +func (in *IndexerClusterStatus) DeepCopyInto(out *IndexerClusterStatus) { *out = *in return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IndexerStatus. -func (in *IndexerStatus) DeepCopy() *IndexerStatus { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IndexerClusterStatus. +func (in *IndexerClusterStatus) DeepCopy() *IndexerClusterStatus { if in == nil { return nil } - out := new(IndexerStatus) + out := new(IndexerClusterStatus) in.DeepCopyInto(out) return out } @@ -243,7 +243,7 @@ func (in *LicenseMasterStatus) DeepCopy() *LicenseMasterStatus { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *SearchHead) DeepCopyInto(out *SearchHead) { +func (in *SearchHeadCluster) DeepCopyInto(out *SearchHeadCluster) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) @@ -252,18 +252,18 @@ func (in *SearchHead) DeepCopyInto(out *SearchHead) { return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SearchHead. -func (in *SearchHead) DeepCopy() *SearchHead { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SearchHeadCluster. +func (in *SearchHeadCluster) DeepCopy() *SearchHeadCluster { if in == nil { return nil } - out := new(SearchHead) + out := new(SearchHeadCluster) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *SearchHead) DeepCopyObject() runtime.Object { +func (in *SearchHeadCluster) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } @@ -271,13 +271,13 @@ func (in *SearchHead) DeepCopyObject() runtime.Object { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *SearchHeadList) DeepCopyInto(out *SearchHeadList) { +func (in *SearchHeadClusterList) DeepCopyInto(out *SearchHeadClusterList) { *out = *in out.TypeMeta = in.TypeMeta in.ListMeta.DeepCopyInto(&out.ListMeta) if in.Items != nil { in, out := &in.Items, &out.Items - *out = make([]SearchHead, len(*in)) + *out = make([]SearchHeadCluster, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } @@ -285,18 +285,18 @@ func (in *SearchHeadList) DeepCopyInto(out *SearchHeadList) { return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SearchHeadList. -func (in *SearchHeadList) DeepCopy() *SearchHeadList { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SearchHeadClusterList. +func (in *SearchHeadClusterList) DeepCopy() *SearchHeadClusterList { if in == nil { return nil } - out := new(SearchHeadList) + out := new(SearchHeadClusterList) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *SearchHeadList) DeepCopyObject() runtime.Object { +func (in *SearchHeadClusterList) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } @@ -304,56 +304,56 @@ func (in *SearchHeadList) DeepCopyObject() runtime.Object { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *SearchHeadMemberStatus) DeepCopyInto(out *SearchHeadMemberStatus) { +func (in *SearchHeadClusterMemberStatus) DeepCopyInto(out *SearchHeadClusterMemberStatus) { *out = *in return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SearchHeadMemberStatus. -func (in *SearchHeadMemberStatus) DeepCopy() *SearchHeadMemberStatus { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SearchHeadClusterMemberStatus. +func (in *SearchHeadClusterMemberStatus) DeepCopy() *SearchHeadClusterMemberStatus { if in == nil { return nil } - out := new(SearchHeadMemberStatus) + out := new(SearchHeadClusterMemberStatus) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *SearchHeadSpec) DeepCopyInto(out *SearchHeadSpec) { +func (in *SearchHeadClusterSpec) DeepCopyInto(out *SearchHeadClusterSpec) { *out = *in in.CommonSplunkSpec.DeepCopyInto(&out.CommonSplunkSpec) out.SparkRef = in.SparkRef return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SearchHeadSpec. -func (in *SearchHeadSpec) DeepCopy() *SearchHeadSpec { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SearchHeadClusterSpec. +func (in *SearchHeadClusterSpec) DeepCopy() *SearchHeadClusterSpec { if in == nil { return nil } - out := new(SearchHeadSpec) + out := new(SearchHeadClusterSpec) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *SearchHeadStatus) DeepCopyInto(out *SearchHeadStatus) { +func (in *SearchHeadClusterStatus) DeepCopyInto(out *SearchHeadClusterStatus) { *out = *in if in.Members != nil { in, out := &in.Members, &out.Members - *out = make([]SearchHeadMemberStatus, len(*in)) + *out = make([]SearchHeadClusterMemberStatus, len(*in)) copy(*out, *in) } return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SearchHeadStatus. -func (in *SearchHeadStatus) DeepCopy() *SearchHeadStatus { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SearchHeadClusterStatus. +func (in *SearchHeadClusterStatus) DeepCopy() *SearchHeadClusterStatus { if in == nil { return nil } - out := new(SearchHeadStatus) + out := new(SearchHeadClusterStatus) in.DeepCopyInto(out) return out } diff --git a/pkg/controller/add_indexer.go b/pkg/controller/add_indexercluster.go similarity index 51% rename from pkg/controller/add_indexer.go rename to pkg/controller/add_indexercluster.go index 968130198..ba7589760 100644 --- a/pkg/controller/add_indexer.go +++ b/pkg/controller/add_indexercluster.go @@ -1,10 +1,10 @@ package controller import ( - "github.com/splunk/splunk-operator/pkg/controller/indexer" + "github.com/splunk/splunk-operator/pkg/controller/indexercluster" ) func init() { // AddToManagerFuncs is a list of functions to create controllers and add them to a manager. - AddToManagerFuncs = append(AddToManagerFuncs, indexer.Add) + AddToManagerFuncs = append(AddToManagerFuncs, indexercluster.Add) } diff --git a/pkg/controller/add_searchhead.go b/pkg/controller/add_searchheadcluster.go similarity index 50% rename from pkg/controller/add_searchhead.go rename to pkg/controller/add_searchheadcluster.go index ca307faa3..e00c82356 100644 --- a/pkg/controller/add_searchhead.go +++ b/pkg/controller/add_searchheadcluster.go @@ -1,10 +1,10 @@ package controller import ( - "github.com/splunk/splunk-operator/pkg/controller/searchhead" + "github.com/splunk/splunk-operator/pkg/controller/searchheadcluster" ) func init() { // AddToManagerFuncs is a list of functions to create controllers and add them to a manager. - AddToManagerFuncs = append(AddToManagerFuncs, searchhead.Add) + AddToManagerFuncs = append(AddToManagerFuncs, searchheadcluster.Add) } diff --git a/pkg/controller/indexer/indexer_controller.go b/pkg/controller/indexercluster/indexercluster_controller.go similarity index 73% rename from pkg/controller/indexer/indexer_controller.go rename to pkg/controller/indexercluster/indexercluster_controller.go index 2aa3ddc79..3dcd892b4 100644 --- a/pkg/controller/indexer/indexer_controller.go +++ b/pkg/controller/indexercluster/indexercluster_controller.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package indexer +package indexercluster import ( "context" @@ -39,7 +39,7 @@ var log = logf.Log.WithName("controller_indexer") * business logic. Delete these comments after modifying this file.* */ -// Add creates a new Indexer Controller and adds it to the Manager. The Manager will set fields on the Controller +// Add creates a new IndexerCluster Controller and adds it to the Manager. The Manager will set fields on the Controller // and Start it when the Manager is Started. func Add(mgr manager.Manager) error { return add(mgr, newReconciler(mgr)) @@ -47,7 +47,7 @@ func Add(mgr manager.Manager) error { // newReconciler returns a new reconcile.Reconciler func newReconciler(mgr manager.Manager) reconcile.Reconciler { - return &ReconcileIndexer{client: mgr.GetClient(), scheme: mgr.GetScheme()} + return &ReconcileIndexerCluster{client: mgr.GetClient(), scheme: mgr.GetScheme()} } // add adds a new Controller to mgr with r as the reconcile.Reconciler @@ -58,16 +58,16 @@ func add(mgr manager.Manager, r reconcile.Reconciler) error { return err } - // Watch for changes to primary resource Indexer - err = c.Watch(&source.Kind{Type: &enterprisev1.Indexer{}}, &handler.EnqueueRequestForObject{}) + // Watch for changes to primary resource IndexerCluster + err = c.Watch(&source.Kind{Type: &enterprisev1.IndexerCluster{}}, &handler.EnqueueRequestForObject{}) if err != nil { return err } - // Watch for changes to secondary resource StatefulSets and requeue the owner Indexer + // Watch for changes to secondary resource StatefulSets and requeue the owner IndexerCluster err = c.Watch(&source.Kind{Type: &appsv1.StatefulSet{}}, &handler.EnqueueRequestForOwner{ IsController: true, - OwnerType: &enterprisev1.Indexer{}, + OwnerType: &enterprisev1.IndexerCluster{}, }) if err != nil { return err @@ -76,30 +76,30 @@ func add(mgr manager.Manager, r reconcile.Reconciler) error { return nil } -// blank assignment to verify that ReconcileIndexer implements reconcile.Reconciler -var _ reconcile.Reconciler = &ReconcileIndexer{} +// blank assignment to verify that ReconcileIndexerCluster implements reconcile.Reconciler +var _ reconcile.Reconciler = &ReconcileIndexerCluster{} -// ReconcileIndexer reconciles a Indexer object -type ReconcileIndexer struct { +// ReconcileIndexerCluster reconciles a IndexerCluster object +type ReconcileIndexerCluster struct { // This client, initialized using mgr.Client() above, is a split client // that reads objects from the cache and writes to the apiserver client client.Client scheme *runtime.Scheme } -// Reconcile reads that state of the cluster for a Indexer object and makes changes based on the state read -// and what is in the Indexer.Spec +// Reconcile reads that state of the cluster for a IndexerCluster object and makes changes based on the state read +// and what is in the IndexerCluster.Spec // TODO(user): Modify this Reconcile function to implement your Controller logic. This example creates // a Pod as an example // Note: // The Controller will requeue the Request to be processed again if the returned error is non-nil or // Result.Requeue is true, otherwise upon completion it will remove the work from the queue. -func (r *ReconcileIndexer) Reconcile(request reconcile.Request) (reconcile.Result, error) { +func (r *ReconcileIndexerCluster) Reconcile(request reconcile.Request) (reconcile.Result, error) { reqLogger := log.WithValues("Request.Namespace", request.Namespace, "Request.Name", request.Name) - reqLogger.Info("Reconciling Indexer") + reqLogger.Info("Reconciling IndexerCluster") - // Fetch the Indexer instance - instance := &enterprisev1.Indexer{} + // Fetch the IndexerCluster instance + instance := &enterprisev1.IndexerCluster{} err := r.client.Get(context.TODO(), request.NamespacedName, instance) if err != nil { if errors.IsNotFound(err) { @@ -113,13 +113,13 @@ func (r *ReconcileIndexer) Reconcile(request reconcile.Request) (reconcile.Resul } instance.TypeMeta.APIVersion = "enterprise.splunk.com/v1alpha2" - instance.TypeMeta.Kind = "Indexer" + instance.TypeMeta.Kind = "IndexerCluster" - err = splunkreconcile.ReconcileIndexer(r.client, instance) + err = splunkreconcile.ReconcileIndexerCluster(r.client, instance) if err != nil { return reconcile.Result{}, err } - reqLogger.Info("Indexer reconciliation complete") + reqLogger.Info("IndexerCluster reconciliation complete") return reconcile.Result{}, nil } diff --git a/pkg/controller/searchhead/searchhead_controller.go b/pkg/controller/searchheadcluster/searchheadcluster_controller.go similarity index 72% rename from pkg/controller/searchhead/searchhead_controller.go rename to pkg/controller/searchheadcluster/searchheadcluster_controller.go index 9b68482a8..e7f50a41e 100644 --- a/pkg/controller/searchhead/searchhead_controller.go +++ b/pkg/controller/searchheadcluster/searchheadcluster_controller.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package searchhead +package searchheadcluster import ( "context" @@ -39,7 +39,7 @@ var log = logf.Log.WithName("controller_searchhead") * business logic. Delete these comments after modifying this file.* */ -// Add creates a new SearchHead Controller and adds it to the Manager. The Manager will set fields on the Controller +// Add creates a new SearchHeadCluster Controller and adds it to the Manager. The Manager will set fields on the Controller // and Start it when the Manager is Started. func Add(mgr manager.Manager) error { return add(mgr, newReconciler(mgr)) @@ -47,7 +47,7 @@ func Add(mgr manager.Manager) error { // newReconciler returns a new reconcile.Reconciler func newReconciler(mgr manager.Manager) reconcile.Reconciler { - return &ReconcileSearchHead{client: mgr.GetClient(), scheme: mgr.GetScheme()} + return &ReconcileSearchHeadCluster{client: mgr.GetClient(), scheme: mgr.GetScheme()} } // add adds a new Controller to mgr with r as the reconcile.Reconciler @@ -58,16 +58,16 @@ func add(mgr manager.Manager, r reconcile.Reconciler) error { return err } - // Watch for changes to primary resource SearchHead - err = c.Watch(&source.Kind{Type: &enterprisev1.SearchHead{}}, &handler.EnqueueRequestForObject{}) + // Watch for changes to primary resource SearchHeadCluster + err = c.Watch(&source.Kind{Type: &enterprisev1.SearchHeadCluster{}}, &handler.EnqueueRequestForObject{}) if err != nil { return err } - // Watch for changes to secondary resource StatefulSets and requeue the owner SearchHead + // Watch for changes to secondary resource StatefulSets and requeue the owner SearchHeadCluster err = c.Watch(&source.Kind{Type: &appsv1.StatefulSet{}}, &handler.EnqueueRequestForOwner{ IsController: true, - OwnerType: &enterprisev1.SearchHead{}, + OwnerType: &enterprisev1.SearchHeadCluster{}, }) if err != nil { return err @@ -76,30 +76,30 @@ func add(mgr manager.Manager, r reconcile.Reconciler) error { return nil } -// blank assignment to verify that ReconcileSearchHead implements reconcile.Reconciler -var _ reconcile.Reconciler = &ReconcileSearchHead{} +// blank assignment to verify that ReconcileSearchHeadCluster implements reconcile.Reconciler +var _ reconcile.Reconciler = &ReconcileSearchHeadCluster{} -// ReconcileSearchHead reconciles a SearchHead object -type ReconcileSearchHead struct { +// ReconcileSearchHeadCluster reconciles a SearchHeadCluster object +type ReconcileSearchHeadCluster struct { // This client, initialized using mgr.Client() above, is a split client // that reads objects from the cache and writes to the apiserver client client.Client scheme *runtime.Scheme } -// Reconcile reads that state of the cluster for a SearchHead object and makes changes based on the state read -// and what is in the SearchHead.Spec +// Reconcile reads that state of the cluster for a SearchHeadCluster object and makes changes based on the state read +// and what is in the SearchHeadCluster.Spec // TODO(user): Modify this Reconcile function to implement your Controller logic. This example creates // a Pod as an example // Note: // The Controller will requeue the Request to be processed again if the returned error is non-nil or // Result.Requeue is true, otherwise upon completion it will remove the work from the queue. -func (r *ReconcileSearchHead) Reconcile(request reconcile.Request) (reconcile.Result, error) { +func (r *ReconcileSearchHeadCluster) Reconcile(request reconcile.Request) (reconcile.Result, error) { reqLogger := log.WithValues("Request.Namespace", request.Namespace, "Request.Name", request.Name) - reqLogger.Info("Reconciling SearchHead") + reqLogger.Info("Reconciling SearchHeadCluster") - // Fetch the SearchHead instance - instance := &enterprisev1.SearchHead{} + // Fetch the SearchHeadCluster instance + instance := &enterprisev1.SearchHeadCluster{} err := r.client.Get(context.TODO(), request.NamespacedName, instance) if err != nil { if errors.IsNotFound(err) { @@ -113,13 +113,13 @@ func (r *ReconcileSearchHead) Reconcile(request reconcile.Request) (reconcile.Re } instance.TypeMeta.APIVersion = "enterprise.splunk.com/v1alpha2" - instance.TypeMeta.Kind = "SearchHead" + instance.TypeMeta.Kind = "SearchHeadCluster" - err = splunkreconcile.ReconcileSearchHead(r.client, instance) + err = splunkreconcile.ReconcileSearchHeadCluster(r.client, instance) if err != nil { return reconcile.Result{}, err } - reqLogger.Info("SearchHead reconciliation complete") + reqLogger.Info("SearchHeadCluster reconciliation complete") return reconcile.Result{}, nil } diff --git a/pkg/splunk/enterprise/configuration.go b/pkg/splunk/enterprise/configuration.go index 0dd4fd0df..81dc9f3b5 100644 --- a/pkg/splunk/enterprise/configuration.go +++ b/pkg/splunk/enterprise/configuration.go @@ -37,12 +37,12 @@ func getSplunkVolumeClaims(cr enterprisev1.MetaObject, spec *enterprisev1.Common var etcStorage, varStorage resource.Quantity var err error - etcStorage, err = resources.ParseResourceQuantity(spec.EtcStorage, "1Gi") + etcStorage, err = resources.ParseResourceQuantity(spec.EtcStorage, "10Gi") if err != nil { return []corev1.PersistentVolumeClaim{}, fmt.Errorf("%s: %s", "etcStorage", err) } - varStorage, err = resources.ParseResourceQuantity(spec.VarStorage, "200Gi") + varStorage, err = resources.ParseResourceQuantity(spec.VarStorage, "100Gi") if err != nil { return []corev1.PersistentVolumeClaim{}, fmt.Errorf("%s: %s", "varStorage", err) } @@ -107,7 +107,7 @@ func GetStandaloneStatefulSet(cr *enterprisev1.Standalone) (*appsv1.StatefulSet, } // GetSearchHeadStatefulSet returns a Kubernetes StatefulSet object for Splunk Enterprise search heads. -func GetSearchHeadStatefulSet(cr *enterprisev1.SearchHead) (*appsv1.StatefulSet, error) { +func GetSearchHeadStatefulSet(cr *enterprisev1.SearchHeadCluster) (*appsv1.StatefulSet, error) { // get search head env variables with deployer env := getSearchHeadExtraEnv(cr, cr.Spec.Replicas) @@ -131,17 +131,17 @@ func GetSearchHeadStatefulSet(cr *enterprisev1.SearchHead) (*appsv1.StatefulSet, } // GetIndexerStatefulSet returns a Kubernetes StatefulSet object for Splunk Enterprise indexers. -func GetIndexerStatefulSet(cr *enterprisev1.Indexer) (*appsv1.StatefulSet, error) { +func GetIndexerStatefulSet(cr *enterprisev1.IndexerCluster) (*appsv1.StatefulSet, error) { return getSplunkStatefulSet(cr, &cr.Spec.CommonSplunkSpec, SplunkIndexer, cr.Spec.Replicas, getIndexerExtraEnv(cr, cr.Spec.Replicas)) } // GetClusterMasterStatefulSet returns a Kubernetes StatefulSet object for a Splunk Enterprise license master. -func GetClusterMasterStatefulSet(cr *enterprisev1.Indexer) (*appsv1.StatefulSet, error) { +func GetClusterMasterStatefulSet(cr *enterprisev1.IndexerCluster) (*appsv1.StatefulSet, error) { return getSplunkStatefulSet(cr, &cr.Spec.CommonSplunkSpec, SplunkClusterMaster, 1, getIndexerExtraEnv(cr, cr.Spec.Replicas)) } // GetDeployerStatefulSet returns a Kubernetes StatefulSet object for a Splunk Enterprise license master. -func GetDeployerStatefulSet(cr *enterprisev1.SearchHead) (*appsv1.StatefulSet, error) { +func GetDeployerStatefulSet(cr *enterprisev1.SearchHeadCluster) (*appsv1.StatefulSet, error) { return getSplunkStatefulSet(cr, &cr.Spec.CommonSplunkSpec, SplunkDeployer, 1, getSearchHeadExtraEnv(cr, cr.Spec.Replicas)) } @@ -217,16 +217,16 @@ func validateCommonSplunkSpec(spec *enterprisev1.CommonSplunkSpec) error { return resources.ValidateCommonSpec(&spec.CommonSpec, defaultResources) } -// ValidateIndexerSpec checks validity and makes default updates to a IndexerSpec, and returns error if something is wrong. -func ValidateIndexerSpec(spec *enterprisev1.IndexerSpec) error { +// ValidateIndexerClusterSpec checks validity and makes default updates to a IndexerClusterSpec, and returns error if something is wrong. +func ValidateIndexerClusterSpec(spec *enterprisev1.IndexerClusterSpec) error { if spec.Replicas == 0 { spec.Replicas = 1 } return validateCommonSplunkSpec(&spec.CommonSplunkSpec) } -// ValidateSearchHeadSpec checks validity and makes default updates to a SearchHeadSpec, and returns error if something is wrong. -func ValidateSearchHeadSpec(spec *enterprisev1.SearchHeadSpec) error { +// ValidateSearchHeadClusterSpec checks validity and makes default updates to a SearchHeadClusterSpec, and returns error if something is wrong. +func ValidateSearchHeadClusterSpec(spec *enterprisev1.SearchHeadClusterSpec) error { if spec.Replicas < 3 { spec.Replicas = 3 } @@ -248,19 +248,19 @@ func ValidateLicenseMasterSpec(spec *enterprisev1.LicenseMasterSpec) error { return validateCommonSplunkSpec(&spec.CommonSplunkSpec) } -// UpdateSearchHeadStatus uses the REST API to update the status for a SearcHead custom resource -func UpdateSearchHeadStatus(cr *enterprisev1.SearchHead, secrets *corev1.Secret) error { +// UpdateSearchHeadClusterStatus uses the REST API to update the status for a SearcHead custom resource +func UpdateSearchHeadClusterStatus(cr *enterprisev1.SearchHeadCluster, secrets *corev1.Secret) error { username := "admin" password := string(secrets.Data["password"]) // populate members status using REST API to get search head cluster member info - cr.Status.Members = []enterprisev1.SearchHeadMemberStatus{} + cr.Status.Members = []enterprisev1.SearchHeadClusterMemberStatus{} for n := int32(0); n < cr.Spec.Replicas; n++ { memberName := GetSplunkStatefulsetPodName(SplunkSearchHead, cr.GetIdentifier(), n) fqdnName := resources.GetServiceFQDN(cr.GetNamespace(), fmt.Sprintf("%s.%s", memberName, GetSplunkServiceName(SplunkSearchHead, cr.GetIdentifier(), true))) c := NewSplunkClient(fmt.Sprintf("https://%s:8089", fqdnName), username, password) - memberStatus := enterprisev1.SearchHeadMemberStatus{Name: memberName} + memberStatus := enterprisev1.SearchHeadClusterMemberStatus{Name: memberName} memberInfo, err := c.GetSearchHeadClusterMemberInfo() if err == nil { memberStatus.Status = memberInfo.Status @@ -287,7 +287,7 @@ func UpdateSearchHeadStatus(cr *enterprisev1.SearchHead, secrets *corev1.Secret) } // DecommissionSearchHead detains and then removes a search head from the cluster -func DecommissionSearchHead(cr *enterprisev1.SearchHead, secrets *corev1.Secret, n int32) (bool, error) { +func DecommissionSearchHead(cr *enterprisev1.SearchHeadCluster, secrets *corev1.Secret, n int32) (bool, error) { memberName := GetSplunkStatefulsetPodName(SplunkSearchHead, cr.GetIdentifier(), n) fqdnName := resources.GetServiceFQDN(cr.GetNamespace(), fmt.Sprintf("%s.%s", memberName, GetSplunkServiceName(SplunkSearchHead, cr.GetIdentifier(), true))) @@ -301,7 +301,7 @@ func DecommissionSearchHead(cr *enterprisev1.SearchHead, secrets *corev1.Secret, case "ManualDetention": // Wait until active searches have drained if cr.Status.Members[n].ActiveSearches != 0 { - return false, c.RemoveSearchHeadMember() + return false, c.RemoveSearchHeadClusterMember() } } @@ -714,10 +714,10 @@ func updateSplunkPodTemplateWithConfig(podTemplateSpec *corev1.PodTemplateSpec, var clusterMasterURL string if instanceType == SplunkIndexer { clusterMasterURL = GetSplunkServiceName(SplunkClusterMaster, cr.GetIdentifier(), false) - } else if instanceType != SplunkClusterMaster && spec.IndexerRef.Name != "" { - clusterMasterURL = GetSplunkServiceName(SplunkClusterMaster, spec.IndexerRef.Name, false) - if spec.IndexerRef.Namespace != "" { - clusterMasterURL = resources.GetServiceFQDN(spec.IndexerRef.Namespace, clusterMasterURL) + } else if instanceType != SplunkClusterMaster && spec.IndexerClusterRef.Name != "" { + clusterMasterURL = GetSplunkServiceName(SplunkClusterMaster, spec.IndexerClusterRef.Name, false) + if spec.IndexerClusterRef.Namespace != "" { + clusterMasterURL = resources.GetServiceFQDN(spec.IndexerClusterRef.Namespace, clusterMasterURL) } } if clusterMasterURL != "" { diff --git a/pkg/splunk/enterprise/configuration_test.go b/pkg/splunk/enterprise/configuration_test.go index 2d95b0846..aabfa2101 100644 --- a/pkg/splunk/enterprise/configuration_test.go +++ b/pkg/splunk/enterprise/configuration_test.go @@ -41,7 +41,7 @@ func configTester(t *testing.T, method string, f func() (interface{}, error), wa } func TestGetIndexerStatefulSet(t *testing.T) { - cr := enterprisev1.Indexer{ + cr := enterprisev1.IndexerCluster{ ObjectMeta: metav1.ObjectMeta{ Name: "stack1", Namespace: "test", @@ -50,19 +50,19 @@ func TestGetIndexerStatefulSet(t *testing.T) { test := func(want string) { f := func() (interface{}, error) { - if err := ValidateIndexerSpec(&cr.Spec); err != nil { - t.Errorf("ValidateIndexerSpec() returned error: %v", err) + if err := ValidateIndexerClusterSpec(&cr.Spec); err != nil { + t.Errorf("ValidateIndexerClusterSpec() returned error: %v", err) } return GetIndexerStatefulSet(&cr) } configTester(t, "GetIndexerStatefulSet()", f, want) } - 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-stack1-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-stack1-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-secrets"}}],"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_INDEXER_URL","value":"splunk-stack1-indexer-0.splunk-stack1-indexer-headless.test.svc.cluster.local"},{"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-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-stack1-indexer"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"1Gi"}}},"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-stack1-indexer"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"200Gi"}}},"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-stack1-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-stack1-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-secrets"}}],"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_INDEXER_URL","value":"splunk-stack1-indexer-0.splunk-stack1-indexer-headless.test.svc.cluster.local"},{"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-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-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-indexer","app.kubernetes.io/managed-by":"splunk-operator","app.kubernetes.io/name":"indexer","app.kubernetes.io/part-of":"splunk-stack1-indexer"}},"spec":{"accessModes":["ReadWriteOnce"],"resources":{"requests":{"storage":"100Gi"}}},"status":{}}],"serviceName":"splunk-stack1-indexer-headless","podManagementPolicy":"Parallel","updateStrategy":{"type":"OnDelete"}},"status":{"replicas":0}}`) } func TestGetSearchHeadStatefulSet(t *testing.T) { - cr := enterprisev1.SearchHead{ + cr := enterprisev1.SearchHeadCluster{ ObjectMeta: metav1.ObjectMeta{ Name: "stack1", Namespace: "test", @@ -71,8 +71,8 @@ func TestGetSearchHeadStatefulSet(t *testing.T) { test := func(want string) { f := func() (interface{}, error) { - if err := ValidateSearchHeadSpec(&cr.Spec); err != nil { - t.Errorf("ValidateSearchHeadSpec() returned error: %v", err) + if err := ValidateSearchHeadClusterSpec(&cr.Spec); err != nil { + t.Errorf("ValidateSearchHeadClusterSpec() returned error: %v", err) } return GetSearchHeadStatefulSet(&cr) } @@ -80,19 +80,19 @@ 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-secrets"}}],"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":"datarecieve","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_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":"1Gi"}}},"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":"200Gi"}}},"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-secrets"}}],"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":"datarecieve","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_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-secrets"}}],"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":"datarecieve","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_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":"1Gi"}}},"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":"200Gi"}}},"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-secrets"}}],"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":"datarecieve","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_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.IndexerRef.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-secrets"}}],"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":"datarecieve","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_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":"1Gi"}}},"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":"200Gi"}}},"status":{}}],"serviceName":"splunk-stack1-search-head-headless","podManagementPolicy":"Parallel","updateStrategy":{"type":"OnDelete"}},"status":{"replicas":0}}`) + cr.Spec.IndexerClusterRef.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-secrets"}}],"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":"datarecieve","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_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.GetIdentifier() - cr.Spec.IndexerRef.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-secrets"}},{"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":"datarecieve","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_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":"1Gi"}}},"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":"200Gi"}}},"status":{}}],"serviceName":"splunk-stack1-search-head-headless","podManagementPolicy":"Parallel","updateStrategy":{"type":"OnDelete"}},"status":{"replicas":0}}`) + cr.Spec.IndexerClusterRef.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-secrets"}},{"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":"datarecieve","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_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 TestGetStandaloneStatefulSet(t *testing.T) { @@ -113,12 +113,12 @@ 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-secrets"}}],"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":"datarecieve","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"}],"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":"1Gi"}}},"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":"200Gi"}}},"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-secrets"}}],"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":"datarecieve","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"}],"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.SparkRef.Name = cr.GetIdentifier() - 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-secrets"}},{"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":"datarecieve","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_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":"1Gi"}}},"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":"200Gi"}}},"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-secrets"}},{"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":"datarecieve","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_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.IndexerRef.Name = "stack2" + cr.Spec.IndexerClusterRef.Name = "stack2" cr.Spec.StorageClassName = "gp2" cr.Spec.SchedulerName = "custom-scheduler" cr.Spec.Defaults = "defaults-string" @@ -126,7 +126,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-secrets"}},{"name":"mnt-splunk-defaults","configMap":{"name":"splunk-stack1-standalone-defaults"}},{"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":"datarecieve","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,/mnt/defaults/defaults.yml,/mnt/splunk-defaults/default.yml"},{"name":"SPLUNK_HOME_OWNERSHIP_ENFORCEMENT","value":"false"},{"name":"SPLUNK_ROLE","value":"splunk_standalone"},{"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":"1Gi"}},"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":"200Gi"}},"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-secrets"}},{"name":"mnt-splunk-defaults","configMap":{"name":"splunk-stack1-standalone-defaults"}},{"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":"datarecieve","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,/mnt/defaults/defaults.yml,/mnt/splunk-defaults/default.yml"},{"name":"SPLUNK_HOME_OWNERSHIP_ENFORCEMENT","value":"false"},{"name":"SPLUNK_ROLE","value":"splunk_standalone"},{"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 TestGetLicenseMasterStatefulSet(t *testing.T) { @@ -147,14 +147,14 @@ 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-secrets"}}],"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"}],"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":"1Gi"}}},"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":"200Gi"}}},"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-secrets"}}],"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"}],"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-secrets"}}],"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_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":"1Gi"}}},"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":"200Gi"}}},"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-secrets"}}],"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_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}}`) } func TestGetClusterMasterStatefulSet(t *testing.T) { - cr := enterprisev1.Indexer{ + cr := enterprisev1.IndexerCluster{ ObjectMeta: metav1.ObjectMeta{ Name: "stack1", Namespace: "test", @@ -163,8 +163,8 @@ func TestGetClusterMasterStatefulSet(t *testing.T) { test := func(want string) { f := func() (interface{}, error) { - if err := ValidateIndexerSpec(&cr.Spec); err != nil { - t.Errorf("ValidateSearchHeadSpec() returned error: %v", err) + if err := ValidateIndexerClusterSpec(&cr.Spec); err != nil { + t.Errorf("ValidateSearchHeadClusterSpec() returned error: %v", err) } return GetClusterMasterStatefulSet(&cr) } @@ -172,21 +172,21 @@ func TestGetClusterMasterStatefulSet(t *testing.T) { } cr.Spec.Replicas = 1 - 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-indexer-secrets"}}],"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_INDEXER_URL","value":"splunk-stack1-indexer-0.splunk-stack1-indexer-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-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":"1Gi"}}},"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":"200Gi"}}},"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-indexer-secrets"}}],"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_INDEXER_URL","value":"splunk-stack1-indexer-0.splunk-stack1-indexer-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-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.Replicas = 2 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-indexer-secrets"}}],"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_LICENSE_MASTER_URL","value":"splunk-stack1-license-master-service.test.svc.cluster.local"},{"name":"SPLUNK_INDEXER_URL","value":"splunk-stack1-indexer-0.splunk-stack1-indexer-headless.test.svc.cluster.local,splunk-stack1-indexer-1.splunk-stack1-indexer-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-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":"1Gi"}}},"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":"200Gi"}}},"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-indexer-secrets"}}],"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_LICENSE_MASTER_URL","value":"splunk-stack1-license-master-service.test.svc.cluster.local"},{"name":"SPLUNK_INDEXER_URL","value":"splunk-stack1-indexer-0.splunk-stack1-indexer-headless.test.svc.cluster.local,splunk-stack1-indexer-1.splunk-stack1-indexer-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-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.Replicas = 3 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-indexer-secrets"}}],"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_LICENSE_URI","value":"/mnt/splunk.lic"},{"name":"SPLUNK_INDEXER_URL","value":"splunk-stack1-indexer-0.splunk-stack1-indexer-headless.test.svc.cluster.local,splunk-stack1-indexer-1.splunk-stack1-indexer-headless.test.svc.cluster.local,splunk-stack1-indexer-2.splunk-stack1-indexer-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-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":"1Gi"}}},"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":"200Gi"}}},"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-indexer-secrets"}}],"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_LICENSE_URI","value":"/mnt/splunk.lic"},{"name":"SPLUNK_INDEXER_URL","value":"splunk-stack1-indexer-0.splunk-stack1-indexer-headless.test.svc.cluster.local,splunk-stack1-indexer-1.splunk-stack1-indexer-headless.test.svc.cluster.local,splunk-stack1-indexer-2.splunk-stack1-indexer-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-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 TestGetDeployerStatefulSet(t *testing.T) { - cr := enterprisev1.SearchHead{ + cr := enterprisev1.SearchHeadCluster{ ObjectMeta: metav1.ObjectMeta{ Name: "stack1", Namespace: "test", @@ -195,8 +195,8 @@ func TestGetDeployerStatefulSet(t *testing.T) { test := func(want string) { f := func() (interface{}, error) { - if err := ValidateSearchHeadSpec(&cr.Spec); err != nil { - t.Errorf("ValidateSearchHeadSpec() returned error: %v", err) + if err := ValidateSearchHeadClusterSpec(&cr.Spec); err != nil { + t.Errorf("ValidateSearchHeadClusterSpec() returned error: %v", err) } return GetDeployerStatefulSet(&cr) } @@ -204,11 +204,11 @@ 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-search-head-secrets"}}],"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_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":"1Gi"}}},"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":"200Gi"}}},"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-search-head-secrets"}}],"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_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}}`) } func TestGetSplunkService(t *testing.T) { - cr := enterprisev1.Indexer{ + cr := enterprisev1.IndexerCluster{ ObjectMeta: metav1.ObjectMeta{ Name: "stack1", Namespace: "test", @@ -235,12 +235,12 @@ func TestGetSplunkService(t *testing.T) { } func TestGetSplunkDefaults(t *testing.T) { - cr := enterprisev1.Indexer{ + cr := enterprisev1.IndexerCluster{ ObjectMeta: metav1.ObjectMeta{ Name: "stack1", Namespace: "test", }, - Spec: enterprisev1.IndexerSpec{ + Spec: enterprisev1.IndexerClusterSpec{ CommonSplunkSpec: enterprisev1.CommonSplunkSpec{Defaults: "defaults_string"}, }, } @@ -256,7 +256,7 @@ func TestGetSplunkDefaults(t *testing.T) { } func TestGetSplunkSecrets(t *testing.T) { - cr := enterprisev1.Indexer{ + cr := enterprisev1.IndexerCluster{ ObjectMeta: metav1.ObjectMeta{ Name: "stack1", Namespace: "test", diff --git a/pkg/splunk/enterprise/names.go b/pkg/splunk/enterprise/names.go index a3831705e..978107375 100644 --- a/pkg/splunk/enterprise/names.go +++ b/pkg/splunk/enterprise/names.go @@ -121,7 +121,7 @@ func GetSplunkImage(specImage string) string { if specImage != "" { name = specImage } else { - name = os.Getenv("SPLUNK_IMAGE") + name = os.Getenv("RELATED_IMAGE_SPLUNK_ENTERPRISE") if name == "" { name = defaultSplunkImage } diff --git a/pkg/splunk/enterprise/names_test.go b/pkg/splunk/enterprise/names_test.go index b6d810590..f816a7d5f 100644 --- a/pkg/splunk/enterprise/names_test.go +++ b/pkg/splunk/enterprise/names_test.go @@ -99,7 +99,7 @@ func TestGetSplunkImage(t *testing.T) { test("splunk/splunk") - os.Setenv("SPLUNK_IMAGE", "splunk-test/splunk") + os.Setenv("RELATED_IMAGE_SPLUNK_ENTERPRISE", "splunk-test/splunk") test("splunk-test/splunk") specImage = "splunk/splunk-test" diff --git a/pkg/splunk/enterprise/restapi.go b/pkg/splunk/enterprise/restapi.go index a07738fcc..8b7abaa8f 100644 --- a/pkg/splunk/enterprise/restapi.go +++ b/pkg/splunk/enterprise/restapi.go @@ -274,8 +274,8 @@ func (c *SplunkClient) SetSearchHeadDetention(detain bool) error { return c.Do(request, 200, nil) } -// RemoveSearchHeadMember removes a search head cluster member -func (c *SplunkClient) RemoveSearchHeadMember() error { +// RemoveSearchHeadClusterMember removes a search head cluster member +func (c *SplunkClient) RemoveSearchHeadClusterMember() error { endpoint := fmt.Sprintf("%s/services/shcluster/member/consensus/default/remove_server", c.managementURI) request, err := http.NewRequest("POST", endpoint, nil) if err != nil { diff --git a/pkg/splunk/reconcile/config.go b/pkg/splunk/reconcile/config.go index 8cefe4a20..dd31a1ee0 100644 --- a/pkg/splunk/reconcile/config.go +++ b/pkg/splunk/reconcile/config.go @@ -34,8 +34,8 @@ func ReconcileSplunkConfig(client ControllerClient, cr enterprisev1.MetaObject, // if reference to indexer cluster, extract and re-use idxc.secret // IndexerRef is not relevant for Indexer, and Indexer will use value from LicenseMaster to prevent cyclical dependency var idxcSecret []byte - if instanceType.ToKind() != "indexer" && instanceType.ToKind() != "license-master" && spec.IndexerRef.Name != "" { - idxcSecret, err = GetSplunkSecret(client, cr, spec.IndexerRef, enterprise.SplunkIndexer, "idxc_secret") + if instanceType.ToKind() != "indexer" && instanceType.ToKind() != "license-master" && spec.IndexerClusterRef.Name != "" { + idxcSecret, err = GetSplunkSecret(client, cr, spec.IndexerClusterRef, enterprise.SplunkIndexer, "idxc_secret") if err != nil { return nil, err } diff --git a/pkg/splunk/reconcile/config_test.go b/pkg/splunk/reconcile/config_test.go index 307cb2c63..57da3088b 100644 --- a/pkg/splunk/reconcile/config_test.go +++ b/pkg/splunk/reconcile/config_test.go @@ -30,7 +30,7 @@ func TestReconcileSplunkConfig(t *testing.T) { } createCalls := map[string][]mockFuncCall{"Get": funcCalls, "Create": funcCalls} updateCalls := map[string][]mockFuncCall{"Get": funcCalls} - searchHeadCR := enterprisev1.SearchHead{ + searchHeadCR := enterprisev1.SearchHeadCluster{ TypeMeta: metav1.TypeMeta{ Kind: "SearcHead", }, @@ -43,7 +43,7 @@ func TestReconcileSplunkConfig(t *testing.T) { searchHeadRevised := searchHeadCR.DeepCopy() searchHeadRevised.Spec.Image = "splunk/test" reconcile := func(c *mockClient, cr interface{}) error { - obj := cr.(*enterprisev1.SearchHead) + obj := cr.(*enterprisev1.SearchHeadCluster) _, err := ReconcileSplunkConfig(c, obj, obj.Spec.CommonSplunkSpec, enterprise.SplunkSearchHead) return err } @@ -59,7 +59,7 @@ func TestReconcileSplunkConfig(t *testing.T) { "idxc_secret": []byte{'a', 'b'}, }, } - searchHeadRevised.Spec.IndexerRef.Name = "stack2" + searchHeadRevised.Spec.IndexerClusterRef.Name = "stack2" updateCalls["Get"] = []mockFuncCall{ {metaName: "*v1.Secret-test-splunk-stack2-indexer-secrets"}, {metaName: "*v1.Secret-test-splunk-stack1-search-head-secrets"}, @@ -68,9 +68,9 @@ func TestReconcileSplunkConfig(t *testing.T) { reconcileTester(t, "TestReconcileSplunkConfig", &searchHeadCR, searchHeadRevised, createCalls, updateCalls, reconcile, &secret) // test indexer with license master - indexerCR := enterprisev1.Indexer{ + indexerCR := enterprisev1.IndexerCluster{ TypeMeta: metav1.TypeMeta{ - Kind: "Indexer", + Kind: "IndexerCluster", }, ObjectMeta: metav1.ObjectMeta{ Name: "stack1", @@ -91,7 +91,7 @@ func TestReconcileSplunkConfig(t *testing.T) { indexerRevised.Spec.Image = "splunk/test" indexerRevised.Spec.LicenseMasterRef.Name = "stack2" reconcile = func(c *mockClient, cr interface{}) error { - obj := cr.(*enterprisev1.Indexer) + obj := cr.(*enterprisev1.IndexerCluster) _, err := ReconcileSplunkConfig(c, obj, obj.Spec.CommonSplunkSpec, enterprise.SplunkIndexer) return err } diff --git a/pkg/splunk/reconcile/finalizers.go b/pkg/splunk/reconcile/finalizers.go index 0aeb7f47a..e4cc313a2 100644 --- a/pkg/splunk/reconcile/finalizers.go +++ b/pkg/splunk/reconcile/finalizers.go @@ -81,9 +81,9 @@ func DeleteSplunkPvc(cr enterprisev1.MetaObject, c ControllerClient) error { component = "standalone" case "LicenseMaster": component = "license-master" - case "SearchHead": + case "SearchHeadCluster": component = "search-head" - case "Indexer": + case "IndexerCluster": component = "indexer" default: scopedLog.Info("Skipping PVC removal") diff --git a/pkg/splunk/reconcile/finalizers_test.go b/pkg/splunk/reconcile/finalizers_test.go index 06fa03834..a7796c846 100644 --- a/pkg/splunk/reconcile/finalizers_test.go +++ b/pkg/splunk/reconcile/finalizers_test.go @@ -33,9 +33,9 @@ func splunkDeletionTester(t *testing.T, cr enterprisev1.MetaObject, delete func( component = "standalone" case "LicenseMaster": component = "license-master" - case "SearchHead": + case "SearchHeadCluster": component = "search-head" - case "Indexer": + case "IndexerCluster": component = "indexer" } @@ -84,9 +84,9 @@ func splunkDeletionTester(t *testing.T, cr enterprisev1.MetaObject, delete func( } func TestCheckSplunkDeletion(t *testing.T) { - cr := enterprisev1.Indexer{ + cr := enterprisev1.IndexerCluster{ TypeMeta: metav1.TypeMeta{ - Kind: "Indexer", + Kind: "IndexerCluster", }, ObjectMeta: metav1.ObjectMeta{ Name: "stack1", diff --git a/pkg/splunk/reconcile/indexer.go b/pkg/splunk/reconcile/indexer.go index 38afcd82d..f9dabf393 100644 --- a/pkg/splunk/reconcile/indexer.go +++ b/pkg/splunk/reconcile/indexer.go @@ -22,11 +22,11 @@ import ( "github.com/splunk/splunk-operator/pkg/splunk/enterprise" ) -// ReconcileIndexer reconciles the state of a Splunk Enterprise indexer cluster. -func ReconcileIndexer(client ControllerClient, cr *enterprisev1.Indexer) error { +// ReconcileIndexerCluster reconciles the state of a Splunk Enterprise indexer cluster. +func ReconcileIndexerCluster(client ControllerClient, cr *enterprisev1.IndexerCluster) error { // validate and updates defaults for CR - err := enterprise.ValidateIndexerSpec(&cr.Spec) + err := enterprise.ValidateIndexerClusterSpec(&cr.Spec) if err != nil { return err } diff --git a/pkg/splunk/reconcile/indexer_test.go b/pkg/splunk/reconcile/indexer_test.go index a7e6efe45..906312ea0 100644 --- a/pkg/splunk/reconcile/indexer_test.go +++ b/pkg/splunk/reconcile/indexer_test.go @@ -23,7 +23,7 @@ import ( enterprisev1 "github.com/splunk/splunk-operator/pkg/apis/enterprise/v1alpha2" ) -func TestReconcileIndexer(t *testing.T) { +func TestReconcileIndexerCluster(t *testing.T) { funcCalls := []mockFuncCall{ {metaName: "*v1.Secret-test-splunk-stack1-indexer-secrets"}, {metaName: "*v1.Service-test-splunk-stack1-indexer-headless"}, @@ -35,9 +35,9 @@ func TestReconcileIndexer(t *testing.T) { createCalls := map[string][]mockFuncCall{"Get": funcCalls, "Create": funcCalls} updateCalls := map[string][]mockFuncCall{"Get": funcCalls, "Update": []mockFuncCall{funcCalls[4], funcCalls[5]}} - current := enterprisev1.Indexer{ + current := enterprisev1.IndexerCluster{ TypeMeta: metav1.TypeMeta{ - Kind: "Indexer", + Kind: "IndexerCluster", }, ObjectMeta: metav1.ObjectMeta{ Name: "stack1", @@ -47,16 +47,16 @@ func TestReconcileIndexer(t *testing.T) { revised := current.DeepCopy() revised.Spec.Image = "splunk/test" reconcile := func(c *mockClient, cr interface{}) error { - return ReconcileIndexer(c, cr.(*enterprisev1.Indexer)) + return ReconcileIndexerCluster(c, cr.(*enterprisev1.IndexerCluster)) } - reconcileTester(t, "TestReconcileIndexer", ¤t, revised, createCalls, updateCalls, reconcile) + reconcileTester(t, "TestReconcileIndexerCluster", ¤t, revised, createCalls, updateCalls, reconcile) // test deletion currentTime := metav1.NewTime(time.Now()) revised.ObjectMeta.DeletionTimestamp = ¤tTime revised.ObjectMeta.Finalizers = []string{"enterprise.splunk.com/delete-pvc"} deleteFunc := func(cr enterprisev1.MetaObject, c ControllerClient) (bool, error) { - err := ReconcileIndexer(c, cr.(*enterprisev1.Indexer)) + err := ReconcileIndexerCluster(c, cr.(*enterprisev1.IndexerCluster)) return true, err } splunkDeletionTester(t, revised, deleteFunc) diff --git a/pkg/splunk/reconcile/searchhead.go b/pkg/splunk/reconcile/searchhead.go index e558e8217..19a84fc05 100644 --- a/pkg/splunk/reconcile/searchhead.go +++ b/pkg/splunk/reconcile/searchhead.go @@ -22,12 +22,12 @@ import ( "github.com/splunk/splunk-operator/pkg/splunk/enterprise" ) -// ReconcileSearchHead reconciles the state for a Splunk Enterprise search head cluster. -func ReconcileSearchHead(client ControllerClient, cr *enterprisev1.SearchHead) error { - scopedLog := log.WithName("ReconcileSearchHead").WithValues("name", cr.GetIdentifier(), "namespace", cr.GetNamespace()) +// ReconcileSearchHeadCluster reconciles the state for a Splunk Enterprise search head cluster. +func ReconcileSearchHeadCluster(client ControllerClient, cr *enterprisev1.SearchHeadCluster) error { + scopedLog := log.WithName("ReconcileSearchHeadCluster").WithValues("name", cr.GetIdentifier(), "namespace", cr.GetNamespace()) // validate and updates defaults for CR - err := enterprise.ValidateSearchHeadSpec(&cr.Spec) + err := enterprise.ValidateSearchHeadClusterSpec(&cr.Spec) if err != nil { return err } @@ -96,7 +96,7 @@ func ReconcileSearchHead(client ControllerClient, cr *enterprisev1.SearchHead) e cr.Status.Phase, err = ApplyStatefulSet(client, statefulSet) cr.Status.ReadyReplicas = statefulSet.Status.ReadyReplicas if cr.Status.ReadyReplicas > 0 { - err = enterprise.UpdateSearchHeadStatus(cr, secrets) + err = enterprise.UpdateSearchHeadClusterStatus(cr, secrets) if err != nil { scopedLog.Error(err, "Failed to update status") } diff --git a/pkg/splunk/reconcile/searchhead_test.go b/pkg/splunk/reconcile/searchhead_test.go index f3582516f..584a8eee5 100644 --- a/pkg/splunk/reconcile/searchhead_test.go +++ b/pkg/splunk/reconcile/searchhead_test.go @@ -23,7 +23,7 @@ import ( enterprisev1 "github.com/splunk/splunk-operator/pkg/apis/enterprise/v1alpha2" ) -func TestReconcileSearchHead(t *testing.T) { +func TestReconcileSearchHeadCluster(t *testing.T) { funcCalls := []mockFuncCall{ {metaName: "*v1.Secret-test-splunk-stack1-search-head-secrets"}, {metaName: "*v1.Service-test-splunk-stack1-search-head-headless"}, @@ -34,9 +34,9 @@ func TestReconcileSearchHead(t *testing.T) { } createCalls := map[string][]mockFuncCall{"Get": funcCalls, "Create": funcCalls} updateCalls := map[string][]mockFuncCall{"Get": funcCalls, "Update": []mockFuncCall{funcCalls[4], funcCalls[5]}} - current := enterprisev1.SearchHead{ + current := enterprisev1.SearchHeadCluster{ TypeMeta: metav1.TypeMeta{ - Kind: "SearchHead", + Kind: "SearchHeadCluster", }, ObjectMeta: metav1.ObjectMeta{ Name: "stack1", @@ -46,16 +46,16 @@ func TestReconcileSearchHead(t *testing.T) { revised := current.DeepCopy() revised.Spec.Image = "splunk/test" reconcile := func(c *mockClient, cr interface{}) error { - return ReconcileSearchHead(c, cr.(*enterprisev1.SearchHead)) + return ReconcileSearchHeadCluster(c, cr.(*enterprisev1.SearchHeadCluster)) } - reconcileTester(t, "TestReconcileSearchHead", ¤t, revised, createCalls, updateCalls, reconcile) + reconcileTester(t, "TestReconcileSearchHeadCluster", ¤t, revised, createCalls, updateCalls, reconcile) // test deletion currentTime := metav1.NewTime(time.Now()) revised.ObjectMeta.DeletionTimestamp = ¤tTime revised.ObjectMeta.Finalizers = []string{"enterprise.splunk.com/delete-pvc"} deleteFunc := func(cr enterprisev1.MetaObject, c ControllerClient) (bool, error) { - err := ReconcileSearchHead(c, cr.(*enterprisev1.SearchHead)) + err := ReconcileSearchHeadCluster(c, cr.(*enterprisev1.SearchHeadCluster)) return true, err } splunkDeletionTester(t, revised, deleteFunc) diff --git a/pkg/splunk/reconcile/util_test.go b/pkg/splunk/reconcile/util_test.go index e48839d9f..3d38ee011 100644 --- a/pkg/splunk/reconcile/util_test.go +++ b/pkg/splunk/reconcile/util_test.go @@ -44,12 +44,12 @@ func copyResource(dst runtime.Object, src runtime.Object) { *dst.(*appsv1.Deployment) = *src.(*appsv1.Deployment) case *appsv1.StatefulSet: *dst.(*appsv1.StatefulSet) = *src.(*appsv1.StatefulSet) - case *enterprisev1.Indexer: - *dst.(*enterprisev1.Indexer) = *src.(*enterprisev1.Indexer) + case *enterprisev1.IndexerCluster: + *dst.(*enterprisev1.IndexerCluster) = *src.(*enterprisev1.IndexerCluster) case *enterprisev1.LicenseMaster: *dst.(*enterprisev1.LicenseMaster) = *src.(*enterprisev1.LicenseMaster) - case *enterprisev1.SearchHead: - *dst.(*enterprisev1.SearchHead) = *src.(*enterprisev1.SearchHead) + case *enterprisev1.SearchHeadCluster: + *dst.(*enterprisev1.SearchHeadCluster) = *src.(*enterprisev1.SearchHeadCluster) case *enterprisev1.Spark: *dst.(*enterprisev1.Spark) = *src.(*enterprisev1.Spark) case *enterprisev1.Standalone: diff --git a/pkg/splunk/spark/names.go b/pkg/splunk/spark/names.go index a135a7b92..2bdcb974f 100644 --- a/pkg/splunk/spark/names.go +++ b/pkg/splunk/spark/names.go @@ -56,7 +56,7 @@ func GetSparkImage(specImage string) string { if specImage != "" { name = specImage } else { - name = os.Getenv("SPARK_IMAGE") + name = os.Getenv("RELATED_IMAGE_SPLUNK_SPARK") if name == "" { name = defaultSparkImage } diff --git a/pkg/splunk/spark/names_test.go b/pkg/splunk/spark/names_test.go index 0656a96fc..ab946338d 100644 --- a/pkg/splunk/spark/names_test.go +++ b/pkg/splunk/spark/names_test.go @@ -60,7 +60,7 @@ func TestGetSparkImage(t *testing.T) { test("splunk/spark") - os.Setenv("SPARK_IMAGE", "splunk-test/spark") + os.Setenv("RELATED_IMAGE_SPLUNK_SPARK", "splunk-test/spark") test("splunk-test/spark") specImage = "splunk/spark-test" From af5064eef549f55e2930eb8889436ce5af7e07a5 Mon Sep 17 00:00:00 2001 From: Mike Dickey Date: Fri, 13 Mar 2020 11:25:40 -0700 Subject: [PATCH 4/7] Added support for managed scaling of SearchHeadCluster --- docs/ChangeLog.md | 6 + docs/README.md | 3 - go.mod | 1 + .../indexercluster_controller.go | 9 +- .../licensemaster/licensemaster_controller.go | 9 +- .../searchheadcluster_controller.go | 9 +- pkg/controller/spark/spark_controller.go | 9 +- .../standalone/standalone_controller.go | 9 +- pkg/splunk/enterprise/configuration.go | 61 ---- pkg/splunk/enterprise/restapi.go | 66 +++- pkg/splunk/enterprise/restapi_test.go | 233 +++++++++++++++ .../{indexer.go => indexercluster.go} | 44 ++- ...indexer_test.go => indexercluster_test.go} | 5 +- pkg/splunk/reconcile/licensemaster.go | 33 +- pkg/splunk/reconcile/licensemaster_test.go | 5 +- pkg/splunk/reconcile/searchhead.go | 113 ------- pkg/splunk/reconcile/searchheadcluster.go | 281 ++++++++++++++++++ ...head_test.go => searchheadcluster_test.go} | 5 +- pkg/splunk/reconcile/spark.go | 34 ++- pkg/splunk/reconcile/spark_test.go | 5 +- pkg/splunk/reconcile/standalone.go | 33 +- pkg/splunk/reconcile/standalone_test.go | 5 +- pkg/splunk/reconcile/statefulset.go | 133 ++++++--- 23 files changed, 819 insertions(+), 292 deletions(-) create mode 100644 pkg/splunk/enterprise/restapi_test.go rename pkg/splunk/reconcile/{indexer.go => indexercluster.go} (81%) rename pkg/splunk/reconcile/{indexer_test.go => indexercluster_test.go} (93%) delete mode 100644 pkg/splunk/reconcile/searchhead.go create mode 100644 pkg/splunk/reconcile/searchheadcluster.go rename pkg/splunk/reconcile/{searchhead_test.go => searchheadcluster_test.go} (92%) diff --git a/docs/ChangeLog.md b/docs/ChangeLog.md index 8314a411a..a76a7854e 100644 --- a/docs/ChangeLog.md +++ b/docs/ChangeLog.md @@ -15,6 +15,12 @@ completely remove any older versions, and any resources managed by the operator, before upgrading to this release. +* Scaling, upgrades and other updates are now more actively managed for the + SearchHeadCluster and IndexerCluster resources. This helps protect against + data loss and maximizes availability while changes are being made. You can + now also use the "kubectl scale" command, and Horizontal Pod Autoscalers + with all resources, except LicenseMaster which always uses a single Pod. + * A new serviceTemplate spec parameter has been added for all Splunk Enterprise custom resources. This may be used to define a template the operator uses for the creation of (non headless) services. diff --git a/docs/README.md b/docs/README.md index 4dd2d12de..e6757b6fd 100644 --- a/docs/README.md +++ b/docs/README.md @@ -41,9 +41,6 @@ covered by support, and we strongly discourage using it in production.* We are working to resolve the following in future releases: -* Scale down of indexer cluster peers does not remove them in a clean manner -by ensuring that the cluster is always valid and complete. This can lead to -data loss. * The Deployment Monitoring Console is not currently configured properly for new deployments diff --git a/go.mod b/go.mod index 14ca1c5c4..84b877aa8 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/splunk/splunk-operator go 1.13 require ( + github.com/go-logr/logr v0.1.0 github.com/operator-framework/operator-sdk v0.15.1 github.com/spf13/pflag v1.0.5 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898 // indirect diff --git a/pkg/controller/indexercluster/indexercluster_controller.go b/pkg/controller/indexercluster/indexercluster_controller.go index 3dcd892b4..935066884 100644 --- a/pkg/controller/indexercluster/indexercluster_controller.go +++ b/pkg/controller/indexercluster/indexercluster_controller.go @@ -115,9 +115,14 @@ func (r *ReconcileIndexerCluster) Reconcile(request reconcile.Request) (reconcil instance.TypeMeta.APIVersion = "enterprise.splunk.com/v1alpha2" instance.TypeMeta.Kind = "IndexerCluster" - err = splunkreconcile.ReconcileIndexerCluster(r.client, instance) + result, err := splunkreconcile.ReconcileIndexerCluster(r.client, instance) if err != nil { - return reconcile.Result{}, err + reqLogger.Error(err, "IndexerCluster reconciliation requeued", "RequeueAfter", result.RequeueAfter) + return result, nil + } + if result.Requeue { + reqLogger.Info("IndexerCluster reconciliation requeued", "RequeueAfter", result.RequeueAfter) + return result, nil } reqLogger.Info("IndexerCluster reconciliation complete") diff --git a/pkg/controller/licensemaster/licensemaster_controller.go b/pkg/controller/licensemaster/licensemaster_controller.go index 582a7b820..30894c176 100644 --- a/pkg/controller/licensemaster/licensemaster_controller.go +++ b/pkg/controller/licensemaster/licensemaster_controller.go @@ -115,9 +115,14 @@ func (r *ReconcileLicenseMaster) Reconcile(request reconcile.Request) (reconcile instance.TypeMeta.APIVersion = "enterprise.splunk.com/v1alpha2" instance.TypeMeta.Kind = "LicenseMaster" - err = splunkreconcile.ReconcileLicenseMaster(r.client, instance) + result, err := splunkreconcile.ReconcileLicenseMaster(r.client, instance) if err != nil { - return reconcile.Result{}, err + reqLogger.Error(err, "LicenseMaster reconciliation requeued", "RequeueAfter", result.RequeueAfter) + return result, nil + } + if result.Requeue { + reqLogger.Info("LicenseMaster reconciliation requeued", "RequeueAfter", result.RequeueAfter) + return result, nil } reqLogger.Info("LicenseMaster reconciliation complete") diff --git a/pkg/controller/searchheadcluster/searchheadcluster_controller.go b/pkg/controller/searchheadcluster/searchheadcluster_controller.go index e7f50a41e..b5768d446 100644 --- a/pkg/controller/searchheadcluster/searchheadcluster_controller.go +++ b/pkg/controller/searchheadcluster/searchheadcluster_controller.go @@ -115,9 +115,14 @@ func (r *ReconcileSearchHeadCluster) Reconcile(request reconcile.Request) (recon instance.TypeMeta.APIVersion = "enterprise.splunk.com/v1alpha2" instance.TypeMeta.Kind = "SearchHeadCluster" - err = splunkreconcile.ReconcileSearchHeadCluster(r.client, instance) + result, err := splunkreconcile.ReconcileSearchHeadCluster(r.client, instance) if err != nil { - return reconcile.Result{}, err + reqLogger.Error(err, "SearchHeadCluster reconciliation requeued", "RequeueAfter", result.RequeueAfter) + return result, nil + } + if result.Requeue { + reqLogger.Info("SearchHeadCluster reconciliation requeued", "RequeueAfter", result.RequeueAfter) + return result, nil } reqLogger.Info("SearchHeadCluster reconciliation complete") diff --git a/pkg/controller/spark/spark_controller.go b/pkg/controller/spark/spark_controller.go index ebf57b700..8f682b142 100644 --- a/pkg/controller/spark/spark_controller.go +++ b/pkg/controller/spark/spark_controller.go @@ -115,9 +115,14 @@ func (r *ReconcileSpark) Reconcile(request reconcile.Request) (reconcile.Result, instance.TypeMeta.APIVersion = "enterprise.splunk.com/v1alpha2" instance.TypeMeta.Kind = "Spark" - err = splunkreconcile.ReconcileSpark(r.client, instance) + result, err := splunkreconcile.ReconcileSpark(r.client, instance) if err != nil { - return reconcile.Result{}, err + reqLogger.Error(err, "Spark reconciliation requeued", "RequeueAfter", result.RequeueAfter) + return result, nil + } + if result.Requeue { + reqLogger.Info("Spark reconciliation requeued", "RequeueAfter", result.RequeueAfter) + return result, nil } reqLogger.Info("Spark reconciliation complete") diff --git a/pkg/controller/standalone/standalone_controller.go b/pkg/controller/standalone/standalone_controller.go index 2792c5cf3..33e3d0460 100644 --- a/pkg/controller/standalone/standalone_controller.go +++ b/pkg/controller/standalone/standalone_controller.go @@ -115,9 +115,14 @@ func (r *ReconcileStandalone) Reconcile(request reconcile.Request) (reconcile.Re instance.TypeMeta.APIVersion = "enterprise.splunk.com/v1alpha2" instance.TypeMeta.Kind = "Standalone" - err = splunkreconcile.ReconcileStandalone(r.client, instance) + result, err := splunkreconcile.ReconcileStandalone(r.client, instance) if err != nil { - return reconcile.Result{}, err + reqLogger.Error(err, "Standalone reconciliation requeued", "RequeueAfter", result.RequeueAfter) + return result, nil + } + if result.Requeue { + reqLogger.Info("Standalone reconciliation requeued", "RequeueAfter", result.RequeueAfter) + return result, nil } reqLogger.Info("Standalone reconciliation complete") diff --git a/pkg/splunk/enterprise/configuration.go b/pkg/splunk/enterprise/configuration.go index 81dc9f3b5..1a2c5da26 100644 --- a/pkg/splunk/enterprise/configuration.go +++ b/pkg/splunk/enterprise/configuration.go @@ -248,67 +248,6 @@ func ValidateLicenseMasterSpec(spec *enterprisev1.LicenseMasterSpec) error { return validateCommonSplunkSpec(&spec.CommonSplunkSpec) } -// UpdateSearchHeadClusterStatus uses the REST API to update the status for a SearcHead custom resource -func UpdateSearchHeadClusterStatus(cr *enterprisev1.SearchHeadCluster, secrets *corev1.Secret) error { - username := "admin" - password := string(secrets.Data["password"]) - - // populate members status using REST API to get search head cluster member info - cr.Status.Members = []enterprisev1.SearchHeadClusterMemberStatus{} - for n := int32(0); n < cr.Spec.Replicas; n++ { - memberName := GetSplunkStatefulsetPodName(SplunkSearchHead, cr.GetIdentifier(), n) - fqdnName := resources.GetServiceFQDN(cr.GetNamespace(), - fmt.Sprintf("%s.%s", memberName, GetSplunkServiceName(SplunkSearchHead, cr.GetIdentifier(), true))) - c := NewSplunkClient(fmt.Sprintf("https://%s:8089", fqdnName), username, password) - memberStatus := enterprisev1.SearchHeadClusterMemberStatus{Name: memberName} - memberInfo, err := c.GetSearchHeadClusterMemberInfo() - if err == nil { - memberStatus.Status = memberInfo.Status - memberStatus.Registered = memberInfo.Registered - memberStatus.ActiveSearches = memberInfo.ActiveHistoricalSearchCount + memberInfo.ActiveRealtimeSearchCount - } - cr.Status.Members = append(cr.Status.Members, memberStatus) - } - - // get search head cluster info from captain - fqdnName := resources.GetServiceFQDN(cr.GetNamespace(), GetSplunkServiceName(SplunkSearchHead, cr.GetIdentifier(), false)) - c := NewSplunkClient(fmt.Sprintf("https://%s:8089", fqdnName), username, password) - captainInfo, err := c.GetSearchHeadCaptainInfo() - if err != nil { - return err - } - cr.Status.Captain = captainInfo.Label - cr.Status.CaptainReady = captainInfo.ServiceReadyFlag - cr.Status.Initialized = captainInfo.InitializedFlag - cr.Status.MinPeersJoined = captainInfo.MinPeersJoinedFlag - cr.Status.MaintenanceMode = captainInfo.MaintenanceMode - - return nil -} - -// DecommissionSearchHead detains and then removes a search head from the cluster -func DecommissionSearchHead(cr *enterprisev1.SearchHeadCluster, secrets *corev1.Secret, n int32) (bool, error) { - memberName := GetSplunkStatefulsetPodName(SplunkSearchHead, cr.GetIdentifier(), n) - fqdnName := resources.GetServiceFQDN(cr.GetNamespace(), - fmt.Sprintf("%s.%s", memberName, GetSplunkServiceName(SplunkSearchHead, cr.GetIdentifier(), true))) - c := NewSplunkClient(fmt.Sprintf("https://%s:8089", fqdnName), "admin", string(secrets.Data["password"])) - - switch cr.Status.Members[n].Status { - case "Up": - // Detain search head - return false, c.SetSearchHeadDetention(true) - - case "ManualDetention": - // Wait until active searches have drained - if cr.Status.Members[n].ActiveSearches != 0 { - return false, c.RemoveSearchHeadClusterMember() - } - } - - // completed - return true, nil -} - // GetSplunkDefaults returns a Kubernetes ConfigMap containing defaults for a Splunk Enterprise resource. func GetSplunkDefaults(identifier, namespace string, instanceType InstanceType, defaults string) *corev1.ConfigMap { return &corev1.ConfigMap{ diff --git a/pkg/splunk/enterprise/restapi.go b/pkg/splunk/enterprise/restapi.go index 8b7abaa8f..9135baa2d 100644 --- a/pkg/splunk/enterprise/restapi.go +++ b/pkg/splunk/enterprise/restapi.go @@ -20,8 +20,16 @@ import ( "fmt" "io/ioutil" "net/http" + "regexp" + "time" ) +// SplunkHTTPClient defines the interface used by SplunkClient. +// It is used to mock alternative implementations used for testing. +type SplunkHTTPClient interface { + Do(*http.Request) (*http.Response, error) +} + // SplunkClient is a simple object used to send HTTP REST API requests type SplunkClient struct { // https endpoint for management interface (e.g. "https://server:8089") @@ -34,7 +42,7 @@ type SplunkClient struct { password string // HTTP client used to process requests - client *http.Client + client SplunkHTTPClient } // NewSplunkClient returns a new SplunkClient object initialized with a username and password @@ -43,9 +51,12 @@ func NewSplunkClient(managementURI, username, password string) *SplunkClient { managementURI: managementURI, username: username, password: password, - client: &http.Client{Transport: &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, // don't verify ssl certs - }}, + client: &http.Client{ + Timeout: 5 * time.Second, + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, // don't verify ssl certs + }, + }, } } @@ -261,6 +272,7 @@ func (c *SplunkClient) GetSearchHeadClusterMemberInfo() (*SearchHeadClusterMembe } // SetSearchHeadDetention enables or disables detention of a search head cluster member +// See https://docs.splunk.com/Documentation/Splunk/latest/DistSearch/SHdetention func (c *SplunkClient) SetSearchHeadDetention(detain bool) error { mode := "off" if detain { @@ -275,11 +287,53 @@ func (c *SplunkClient) SetSearchHeadDetention(detain bool) error { } // RemoveSearchHeadClusterMember removes a search head cluster member +// See https://docs.splunk.com/Documentation/Splunk/latest/DistSearch/Removeaclustermember func (c *SplunkClient) RemoveSearchHeadClusterMember() error { - endpoint := fmt.Sprintf("%s/services/shcluster/member/consensus/default/remove_server", c.managementURI) + // sent request to remove from search head cluster consensus + endpoint := fmt.Sprintf("%s/services/shcluster/member/consensus/default/remove_server?output_mode=json", c.managementURI) request, err := http.NewRequest("POST", endpoint, nil) if err != nil { return err } - return c.Do(request, 200, nil) + + // send HTTP response and check status + request.SetBasicAuth(c.username, c.password) + response, err := c.client.Do(request) + if err != nil { + return err + } + if response.StatusCode == 200 { + return nil + } + if response.StatusCode != 503 { + return fmt.Errorf("Response code=%d from %s; want %d", response.StatusCode, request.URL, 200) + } + + // unmarshall 503 response + apiResponse := struct { + Messages []struct { + Text string `json:"text"` + } `json:"messages"` + }{} + data, _ := ioutil.ReadAll(response.Body) + if len(data) == 0 { + return fmt.Errorf("Received 503 response with empty body from %s", request.URL) + } + err = json.Unmarshal(data, &apiResponse) + if err != nil { + return fmt.Errorf("Failed to unmarshal response from %s: %v", request.URL, err) + } + + // check if request failed because member was already removed + if len(apiResponse.Messages) == 0 { + return fmt.Errorf("Received 503 response with empty Messages from %s", request.URL) + } + msg1 := regexp.MustCompile(`Server .* is not part of configuration, hence cannot be removed`) + msg2 := regexp.MustCompile(`This node is not part of any cluster configuration`) + if msg1.Match([]byte(apiResponse.Messages[0].Text)) || msg2.Match([]byte(apiResponse.Messages[0].Text)) { + // it was already removed -> ignore error + return nil + } + + return fmt.Errorf("Received unrecognized 503 response from %s", request.URL) } diff --git a/pkg/splunk/enterprise/restapi_test.go b/pkg/splunk/enterprise/restapi_test.go new file mode 100644 index 000000000..816d4602f --- /dev/null +++ b/pkg/splunk/enterprise/restapi_test.go @@ -0,0 +1,233 @@ +// 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 enterprise + +import ( + "io/ioutil" + "net/http" + "reflect" + "strings" + "testing" +) + +// MockHTTPClient is used to replicate an http.Client +type MockHTTPClient struct { + Requests []http.Request + Response http.Response + Err error +} + +// Do method for MockHTTPClient is called by all methods of SplunkClient +func (c *MockHTTPClient) Do(req *http.Request) (*http.Response, error) { + c.Requests = append(c.Requests, *req) + return &c.Response, c.Err +} + +func splunkClientTester(t *testing.T, testMethod string, status int, body string, wantRequests []http.Request, test func(SplunkClient) error) { + mockClient := MockHTTPClient{ + Err: nil, + Response: http.Response{ + StatusCode: status, + Body: ioutil.NopCloser(strings.NewReader(body)), + }, + } + c := NewSplunkClient("https://localhost:8089", "admin", "p@ssw0rd") + c.client = &mockClient + err := test(*c) + if err != nil { + t.Errorf("%s err = %v", testMethod, err) + } + if len(mockClient.Requests) != len(wantRequests) { + t.Fatalf("%s got %d Requests; want %d", testMethod, len(mockClient.Requests), len(wantRequests)) + } + for n := range mockClient.Requests { + wantRequests[n].SetBasicAuth(c.username, c.password) + if !reflect.DeepEqual(mockClient.Requests[n], wantRequests[n]) { + t.Errorf("%s Requests[%d]=%v; want %v", testMethod, n, mockClient.Requests[n], wantRequests[n]) + } + } +} + +func TestGetSearchHeadCaptainInfo(t *testing.T) { + wantRequest, _ := http.NewRequest("GET", "https://localhost:8089/services/shcluster/captain/info?count=0&output_mode=json", nil) + want := []http.Request{*wantRequest} + wantCaptainLabel := "splunk-s2-search-head-0" + test := func(c SplunkClient) error { + captainInfo, err := c.GetSearchHeadCaptainInfo() + if err != nil { + return err + } + if captainInfo.Label != wantCaptainLabel { + t.Errorf("captainInfo.Label=%s; want %s", captainInfo.Label, wantCaptainLabel) + } + return nil + } + body := `{"links":{},"origin":"https://localhost:8089/services/shcluster/captain/info","updated":"2020-03-15T16:36:42+00:00","generator":{"build":"a7f645ddaf91","version":"8.0.2"},"entry":[{"name":"captain","id":"https://localhost:8089/services/shcluster/captain/info/captain","updated":"1970-01-01T00:00:00+00:00","links":{"alternate":"/services/shcluster/captain/info/captain","list":"/services/shcluster/captain/info/captain"},"author":"system","acl":{"app":"","can_list":true,"can_write":true,"modifiable":false,"owner":"system","perms":{"read":["admin","splunk-system-role"],"write":["admin","splunk-system-role"]},"removable":false,"sharing":"system"},"content":{"eai:acl":null,"elected_captain":1584139352,"id":"A9D5FCCF-EB93-4E0A-93E1-45B56483EA7A","initialized_flag":true,"label":"splunk-s2-search-head-0","maintenance_mode":false,"mgmt_uri":"https://splunk-s2-search-head-0.splunk-s2-search-head-headless.splunk.svc.cluster.local:8089","min_peers_joined_flag":true,"peer_scheme_host_port":"https://splunk-s2-search-head-0.splunk-s2-search-head-headless.splunk.svc.cluster.local:8089","rolling_restart_flag":false,"service_ready_flag":true,"start_time":1584139291}}],"paging":{"total":1,"perPage":30,"offset":0},"messages":[]}` + splunkClientTester(t, "TestGetSearchHeadCaptainInfo", 200, body, want, test) + + // test body with no entries + test = func(c SplunkClient) error { + _, err := c.GetSearchHeadCaptainInfo() + if err == nil { + t.Errorf("GetSearchHeadCaptainInfo returned nil; want error") + } + return nil + } + body = `{"links":{},"origin":"https://localhost:8089/services/shcluster/captain/info","updated":"2020-03-15T16:36:42+00:00","generator":{"build":"a7f645ddaf91","version":"8.0.2"},"entry":[]}` + splunkClientTester(t, "TestGetSearchHeadCaptainInfo", 200, body, want, test) + + // test empty body + splunkClientTester(t, "TestGetSearchHeadCaptainInfo", 200, "", want, test) + + // test error code + splunkClientTester(t, "TestGetSearchHeadCaptainInfo", 500, "", want, test) +} + +func TestGetSearchHeadClusterMemberInfo(t *testing.T) { + wantRequest, _ := http.NewRequest("GET", "https://localhost:8089/services/shcluster/member/info?count=0&output_mode=json", nil) + want := []http.Request{*wantRequest} + wantMemberStatus := "Up" + test := func(c SplunkClient) error { + memberInfo, err := c.GetSearchHeadClusterMemberInfo() + if err != nil { + return err + } + if memberInfo.Status != wantMemberStatus { + t.Errorf("memberInfo.Status=%s; want %s", memberInfo.Status, wantMemberStatus) + } + return nil + } + body := `{"links":{},"origin":"https://localhost:8089/services/shcluster/member/info","updated":"2020-03-15T16:30:38+00:00","generator":{"build":"a7f645ddaf91","version":"8.0.2"},"entry":[{"name":"member","id":"https://localhost:8089/services/shcluster/member/info/member","updated":"1970-01-01T00:00:00+00:00","links":{"alternate":"/services/shcluster/member/info/member","list":"/services/shcluster/member/info/member"},"author":"system","acl":{"app":"","can_list":true,"can_write":true,"modifiable":false,"owner":"system","perms":{"read":["admin","splunk-system-role"],"write":["admin","splunk-system-role"]},"removable":false,"sharing":"system"},"content":{"active_historical_search_count":0,"active_realtime_search_count":0,"adhoc_searchhead":false,"eai:acl":null,"is_registered":true,"last_heartbeat_attempt":1584289836,"maintenance_mode":false,"no_artifact_replications":false,"peer_load_stats_gla_15m":0,"peer_load_stats_gla_1m":0,"peer_load_stats_gla_5m":0,"peer_load_stats_max_runtime":0,"peer_load_stats_num_autosummary":0,"peer_load_stats_num_historical":0,"peer_load_stats_num_realtime":0,"peer_load_stats_num_running":0,"peer_load_stats_total_runtime":0,"restart_state":"NoRestart","status":"Up"}}],"paging":{"total":1,"perPage":30,"offset":0},"messages":[]}` + splunkClientTester(t, "TestGetSearchHeadClusterMemberInfo", 200, body, want, test) + + // test body with no entries + test = func(c SplunkClient) error { + _, err := c.GetSearchHeadClusterMemberInfo() + if err == nil { + t.Errorf("GetSearchHeadClusterMemberInfo returned nil; want error") + } + return nil + } + body = `{"links":{},"origin":"https://localhost:8089/services/shcluster/captain/info","updated":"2020-03-15T16:36:42+00:00","generator":{"build":"a7f645ddaf91","version":"8.0.2"},"entry":[]}` + splunkClientTester(t, "TestGetSearchHeadCaptainInfo", 200, body, want, test) + + // test empty body + splunkClientTester(t, "TestGetSearchHeadClusterMemberInfo", 200, "", want, test) + + // test error code + splunkClientTester(t, "TestGetSearchHeadClusterMemberInfo", 500, "", want, test) +} + +func TestGetSearchHeadCaptainMembers(t *testing.T) { + wantRequest, _ := http.NewRequest("GET", "https://localhost:8089/services/shcluster/captain/members?count=0&output_mode=json", nil) + want := []http.Request{*wantRequest} + wantMembers := []string{ + "splunk-s2-search-head-0", "splunk-s2-search-head-1", "splunk-s2-search-head-2", "splunk-s2-search-head-3", "splunk-s2-search-head-4", + } + wantStatus := "Up" + wantCaptain := "splunk-s2-search-head-0" + test := func(c SplunkClient) error { + members, err := c.GetSearchHeadCaptainMembers() + if err != nil { + return err + } + if len(members) != len(wantMembers) { + t.Errorf("len(members)=%d; want %d", len(members), len(wantMembers)) + } + for n := range wantMembers { + member, ok := members[wantMembers[n]] + if !ok { + t.Errorf("wanted member not found: %s", wantMembers[n]) + } + if member.Status != wantStatus { + t.Errorf("member %s want Status=%s: got %s", wantMembers[n], member.Status, wantStatus) + } + if member.Captain { + if wantMembers[n] != wantCaptain { + t.Errorf("member %s want Captain=%t: got %t", wantMembers[n], false, true) + } + } else { + if wantMembers[n] == wantCaptain { + t.Errorf("member %s want Captain=%t: got %t", wantMembers[n], true, false) + } + } + } + return nil + } + body := `{"links":{"create":"/services/shcluster/captain/members/_new"},"origin":"https://localhost:8089/services/shcluster/captain/members","updated":"2020-03-15T16:40:20+00:00","generator":{"build":"a7f645ddaf91","version":"8.0.2"},"entry":[{"name":"7D571849-CD52-48F4-B76A-E83C4E86E300","id":"https://localhost:8089/services/shcluster/captain/members/7D571849-CD52-48F4-B76A-E83C4E86E300","updated":"1970-01-01T00:00:00+00:00","links":{"alternate":"/services/shcluster/captain/members/7D571849-CD52-48F4-B76A-E83C4E86E300","list":"/services/shcluster/captain/members/7D571849-CD52-48F4-B76A-E83C4E86E300","edit":"/services/shcluster/captain/members/7D571849-CD52-48F4-B76A-E83C4E86E300"},"author":"system","acl":{"app":"","can_list":true,"can_write":true,"modifiable":false,"owner":"system","perms":{"read":["admin","splunk-system-role"],"write":["admin","splunk-system-role"]},"removable":false,"sharing":"system"},"content":{"adhoc_searchhead":false,"advertise_restart_required":false,"artifact_count":2,"delayed_artifacts_to_discard":[],"eai:acl":null,"fixup_set":[],"host_port_pair":"10.42.0.3:8089","is_captain":false,"kv_store_host_port":"splunk-s2-search-head-2.splunk-s2-search-head-headless.splunk.svc.cluster.local:8191","label":"splunk-s2-search-head-2","last_heartbeat":1584290418,"mgmt_uri":"https://splunk-s2-search-head-2.splunk-s2-search-head-headless.splunk.svc.cluster.local:8089","no_artifact_replications":false,"peer_scheme_host_port":"https://10.42.0.3:8089","pending_job_count":0,"preferred_captain":false,"replication_count":0,"replication_port":9887,"replication_use_ssl":false,"site":"default","status":"Up","status_counter":{"Complete":2,"NonStreamingTarget":0,"PendingDiscard":0}}},{"name":"90D7E074-9880-4867-BAA1-31A74EC28DC0","id":"https://localhost:8089/services/shcluster/captain/members/90D7E074-9880-4867-BAA1-31A74EC28DC0","updated":"1970-01-01T00:00:00+00:00","links":{"alternate":"/services/shcluster/captain/members/90D7E074-9880-4867-BAA1-31A74EC28DC0","list":"/services/shcluster/captain/members/90D7E074-9880-4867-BAA1-31A74EC28DC0","edit":"/services/shcluster/captain/members/90D7E074-9880-4867-BAA1-31A74EC28DC0"},"author":"system","acl":{"app":"","can_list":true,"can_write":true,"modifiable":false,"owner":"system","perms":{"read":["admin","splunk-system-role"],"write":["admin","splunk-system-role"]},"removable":false,"sharing":"system"},"content":{"adhoc_searchhead":false,"advertise_restart_required":false,"artifact_count":0,"delayed_artifacts_to_discard":[],"eai:acl":null,"fixup_set":[],"host_port_pair":"10.42.0.2:8089","is_captain":true,"kv_store_host_port":"splunk-s2-search-head-0.splunk-s2-search-head-headless.splunk.svc.cluster.local:8191","label":"splunk-s2-search-head-0","last_heartbeat":1584290416,"mgmt_uri":"https://splunk-s2-search-head-0.splunk-s2-search-head-headless.splunk.svc.cluster.local:8089","no_artifact_replications":false,"peer_scheme_host_port":"https://10.42.0.2:8089","pending_job_count":0,"preferred_captain":true,"replication_count":0,"replication_port":9887,"replication_use_ssl":false,"site":"default","status":"Up","status_counter":{"Complete":0,"NonStreamingTarget":0,"PendingDiscard":0}}},{"name":"97B56FAE-E9C9-4B12-8B1E-A428E7859417","id":"https://localhost:8089/services/shcluster/captain/members/97B56FAE-E9C9-4B12-8B1E-A428E7859417","updated":"1970-01-01T00:00:00+00:00","links":{"alternate":"/services/shcluster/captain/members/97B56FAE-E9C9-4B12-8B1E-A428E7859417","list":"/services/shcluster/captain/members/97B56FAE-E9C9-4B12-8B1E-A428E7859417","edit":"/services/shcluster/captain/members/97B56FAE-E9C9-4B12-8B1E-A428E7859417"},"author":"system","acl":{"app":"","can_list":true,"can_write":true,"modifiable":false,"owner":"system","perms":{"read":["admin","splunk-system-role"],"write":["admin","splunk-system-role"]},"removable":false,"sharing":"system"},"content":{"adhoc_searchhead":false,"advertise_restart_required":false,"artifact_count":1,"delayed_artifacts_to_discard":[],"eai:acl":null,"fixup_set":[],"host_port_pair":"10.36.0.7:8089","is_captain":false,"kv_store_host_port":"splunk-s2-search-head-1.splunk-s2-search-head-headless.splunk.svc.cluster.local:8191","label":"splunk-s2-search-head-1","last_heartbeat":1584290418,"mgmt_uri":"https://splunk-s2-search-head-1.splunk-s2-search-head-headless.splunk.svc.cluster.local:8089","no_artifact_replications":false,"peer_scheme_host_port":"https://10.36.0.7:8089","pending_job_count":0,"preferred_captain":false,"replication_count":0,"replication_port":9887,"replication_use_ssl":false,"site":"default","status":"Up","status_counter":{"Complete":1,"NonStreamingTarget":0,"PendingDiscard":0}}},{"name":"AA55C39A-5A3A-47CC-BF2C-2B60F0F6C561","id":"https://localhost:8089/services/shcluster/captain/members/AA55C39A-5A3A-47CC-BF2C-2B60F0F6C561","updated":"1970-01-01T00:00:00+00:00","links":{"alternate":"/services/shcluster/captain/members/AA55C39A-5A3A-47CC-BF2C-2B60F0F6C561","list":"/services/shcluster/captain/members/AA55C39A-5A3A-47CC-BF2C-2B60F0F6C561","edit":"/services/shcluster/captain/members/AA55C39A-5A3A-47CC-BF2C-2B60F0F6C561"},"author":"system","acl":{"app":"","can_list":true,"can_write":true,"modifiable":false,"owner":"system","perms":{"read":["admin","splunk-system-role"],"write":["admin","splunk-system-role"]},"removable":false,"sharing":"system"},"content":{"adhoc_searchhead":false,"advertise_restart_required":false,"artifact_count":1,"delayed_artifacts_to_discard":[],"eai:acl":null,"fixup_set":[],"host_port_pair":"10.42.0.5:8089","is_captain":false,"kv_store_host_port":"splunk-s2-search-head-4.splunk-s2-search-head-headless.splunk.svc.cluster.local:8191","label":"splunk-s2-search-head-4","last_heartbeat":1584290417,"mgmt_uri":"https://splunk-s2-search-head-4.splunk-s2-search-head-headless.splunk.svc.cluster.local:8089","no_artifact_replications":false,"peer_scheme_host_port":"https://10.42.0.5:8089","pending_job_count":0,"preferred_captain":false,"replication_count":0,"replication_port":9887,"replication_use_ssl":false,"site":"default","status":"Up","status_counter":{"Complete":1,"NonStreamingTarget":0,"PendingDiscard":0}}},{"name":"E271B238-921F-4F6E-BD99-E110EB7B0FDA","id":"https://localhost:8089/services/shcluster/captain/members/E271B238-921F-4F6E-BD99-E110EB7B0FDA","updated":"1970-01-01T00:00:00+00:00","links":{"alternate":"/services/shcluster/captain/members/E271B238-921F-4F6E-BD99-E110EB7B0FDA","list":"/services/shcluster/captain/members/E271B238-921F-4F6E-BD99-E110EB7B0FDA","edit":"/services/shcluster/captain/members/E271B238-921F-4F6E-BD99-E110EB7B0FDA"},"author":"system","acl":{"app":"","can_list":true,"can_write":true,"modifiable":false,"owner":"system","perms":{"read":["admin","splunk-system-role"],"write":["admin","splunk-system-role"]},"removable":false,"sharing":"system"},"content":{"adhoc_searchhead":false,"advertise_restart_required":false,"artifact_count":2,"delayed_artifacts_to_discard":[],"eai:acl":null,"fixup_set":[],"host_port_pair":"10.40.0.4:8089","is_captain":false,"kv_store_host_port":"splunk-s2-search-head-3.splunk-s2-search-head-headless.splunk.svc.cluster.local:8191","label":"splunk-s2-search-head-3","last_heartbeat":1584290420,"mgmt_uri":"https://splunk-s2-search-head-3.splunk-s2-search-head-headless.splunk.svc.cluster.local:8089","no_artifact_replications":false,"peer_scheme_host_port":"https://10.40.0.4:8089","pending_job_count":0,"preferred_captain":false,"replication_count":0,"replication_port":9887,"replication_use_ssl":false,"site":"default","status":"Up","status_counter":{"Complete":2,"NonStreamingTarget":0,"PendingDiscard":0}}}],"paging":{"total":5,"perPage":30,"offset":0},"messages":[]}` + splunkClientTester(t, "TestGetSearchHeadCaptainMembers", 200, body, want, test) + + // test error response + test = func(c SplunkClient) error { + _, err := c.GetSearchHeadCaptainMembers() + if err == nil { + t.Errorf("GetSearchHeadCaptainMembers returned nil; want error") + } + return nil + } + splunkClientTester(t, "TestGetSearchHeadCaptainMembers", 503, "", want, test) +} + +func TestSetSearchHeadDetention(t *testing.T) { + wantRequest, _ := http.NewRequest("POST", "https://localhost:8089/services/shcluster/member/control/control/set_manual_detention?manual_detention=on", nil) + want := []http.Request{*wantRequest} + test := func(c SplunkClient) error { + return c.SetSearchHeadDetention(true) + } + splunkClientTester(t, "TestSetSearchHeadDetention", 200, "", want, test) +} + +func TestRemoveSearchHeadClusterMember(t *testing.T) { + // test for 200 response first (sent on first removal request) + wantRequest, _ := http.NewRequest("POST", "https://localhost:8089/services/shcluster/member/consensus/default/remove_server?output_mode=json", nil) + want := []http.Request{*wantRequest} + test := func(c SplunkClient) error { + return c.RemoveSearchHeadClusterMember() + } + splunkClientTester(t, "TestRemoveSearchHeadClusterMember", 200, "", want, test) + + // next test 503 error message (sent for short period after removal, while SH is updating itself) + body := `{"messages":[{"type":"ERROR","text":"Failed to proxy call to member https://splunk-s2-search-head-0.splunk-s2-search-head-headless.splunk.svc.cluster.local:8089. ERROR: Server https://splunk-s2-search-head-3.splunk-s2-search-head-headless.splunk.svc.cluster.local:8089 is not part of configuration, hence cannot be removed. Check configuration by making GET request onto /services/shcluster/member/consensus"}]}` + splunkClientTester(t, "TestRemoveSearchHeadClusterMember", 503, body, want, test) + + // check alternate 503 message (sent after SH has completed removal) + body = `{"messages":[{"type":"ERROR","text":"This node is not part of any cluster configuration, please re-run the command from an active cluster member. Also see \"splunk add shcluster-member\" to add this member to an existing cluster or see \"splunk bootstrap shcluster-captain\" to bootstrap a new cluster with this member."}]}` + splunkClientTester(t, "TestRemoveSearchHeadClusterMember", 503, body, want, test) + + // test unrecognized response message + test = func(c SplunkClient) error { + err := c.RemoveSearchHeadClusterMember() + if err == nil { + t.Errorf("RemoveSearchHeadClusterMember returned nil; want error") + } + return nil + } + body = `{"messages":[{"type":"ERROR","text":"Nothing that we are expecting."}]}` + splunkClientTester(t, "TestRemoveSearchHeadClusterMember", 503, body, want, test) + + // test empty messages array in response + body = `{"messages":[]}` + splunkClientTester(t, "TestRemoveSearchHeadClusterMember", 503, body, want, test) + + // test unmarshal failure + body = `` + splunkClientTester(t, "TestRemoveSearchHeadClusterMember", 503, body, want, test) + + // test empty response + splunkClientTester(t, "TestRemoveSearchHeadClusterMember", 503, "", want, test) + + // test bad response code + splunkClientTester(t, "TestRemoveSearchHeadClusterMember", 404, "", want, test) +} diff --git a/pkg/splunk/reconcile/indexer.go b/pkg/splunk/reconcile/indexercluster.go similarity index 81% rename from pkg/splunk/reconcile/indexer.go rename to pkg/splunk/reconcile/indexercluster.go index f9dabf393..a1a78bef7 100644 --- a/pkg/splunk/reconcile/indexer.go +++ b/pkg/splunk/reconcile/indexercluster.go @@ -17,18 +17,27 @@ package deploy import ( "context" "fmt" + "time" + + "sigs.k8s.io/controller-runtime/pkg/reconcile" enterprisev1 "github.com/splunk/splunk-operator/pkg/apis/enterprise/v1alpha2" "github.com/splunk/splunk-operator/pkg/splunk/enterprise" ) // ReconcileIndexerCluster reconciles the state of a Splunk Enterprise indexer cluster. -func ReconcileIndexerCluster(client ControllerClient, cr *enterprisev1.IndexerCluster) error { +func ReconcileIndexerCluster(client ControllerClient, cr *enterprisev1.IndexerCluster) (reconcile.Result, error) { + + // unless modified, reconcile for this object will be requeued after 5 seconds + result := reconcile.Result{ + Requeue: true, + RequeueAfter: time.Second * 5, + } // validate and updates defaults for CR err := enterprise.ValidateIndexerClusterSpec(&cr.Spec) if err != nil { - return err + return result, err } // updates status after function completes @@ -45,63 +54,66 @@ func ReconcileIndexerCluster(client ControllerClient, cr *enterprisev1.IndexerCl if terminating && err != nil { // don't bother if no error, since it will just be removed immmediately after cr.Status.Phase = enterprisev1.PhaseTerminating cr.Status.ClusterMasterPhase = enterprisev1.PhaseTerminating - client.Status().Update(context.TODO(), cr) + } else { + result.Requeue = false } - return err + return result, err } // create or update general config resources _, err = ReconcileSplunkConfig(client, cr, cr.Spec.CommonSplunkSpec, enterprise.SplunkIndexer) if err != nil { - return err + return result, err } // create or update a headless service for indexer cluster err = ApplyService(client, enterprise.GetSplunkService(cr, cr.Spec.CommonSpec, enterprise.SplunkIndexer, true)) if err != nil { - return err + return result, err } // create or update a regular service for indexer cluster (ingestion) err = ApplyService(client, enterprise.GetSplunkService(cr, cr.Spec.CommonSpec, enterprise.SplunkIndexer, false)) if err != nil { - return err + return result, err } // create or update a regular service for the cluster master err = ApplyService(client, enterprise.GetSplunkService(cr, cr.Spec.CommonSpec, enterprise.SplunkClusterMaster, false)) if err != nil { - return err + return result, err } // create or update statefulset for the cluster master statefulSet, err := enterprise.GetClusterMasterStatefulSet(cr) if err != nil { - return err + return result, err } cr.Status.ClusterMasterPhase, err = ApplyStatefulSet(client, statefulSet) if err == nil && cr.Status.Phase == enterprisev1.PhaseReady { - cr.Status.ClusterMasterPhase, err = ReconcileStatefulSetPods(client, statefulSet, statefulSet.Status.ReadyReplicas, 1, nil) + mgr := DefaultStatefulSetPodManager{} + cr.Status.ClusterMasterPhase, err = ReconcileStatefulSetPods(client, statefulSet, &mgr, 1) } if err != nil { cr.Status.ClusterMasterPhase = enterprisev1.PhaseError - return err + return result, err } // create or update statefulset for the indexers statefulSet, err = enterprise.GetIndexerStatefulSet(cr) if err != nil { - return err + return result, err } cr.Status.Phase, err = ApplyStatefulSet(client, statefulSet) cr.Status.ReadyReplicas = statefulSet.Status.ReadyReplicas if err == nil && cr.Status.Phase == enterprisev1.PhaseReady { - cr.Status.Phase, err = ReconcileStatefulSetPods(client, statefulSet, cr.Status.ReadyReplicas, cr.Spec.Replicas, nil) + mgr := DefaultStatefulSetPodManager{} + cr.Status.Phase, err = ReconcileStatefulSetPods(client, statefulSet, &mgr, cr.Spec.Replicas) } if err != nil { cr.Status.Phase = enterprisev1.PhaseError - return err + } else if cr.Status.Phase == enterprisev1.PhaseReady { + result.Requeue = false } - - return nil + return result, err } diff --git a/pkg/splunk/reconcile/indexer_test.go b/pkg/splunk/reconcile/indexercluster_test.go similarity index 93% rename from pkg/splunk/reconcile/indexer_test.go rename to pkg/splunk/reconcile/indexercluster_test.go index 906312ea0..06d1f729c 100644 --- a/pkg/splunk/reconcile/indexer_test.go +++ b/pkg/splunk/reconcile/indexercluster_test.go @@ -47,7 +47,8 @@ func TestReconcileIndexerCluster(t *testing.T) { revised := current.DeepCopy() revised.Spec.Image = "splunk/test" reconcile := func(c *mockClient, cr interface{}) error { - return ReconcileIndexerCluster(c, cr.(*enterprisev1.IndexerCluster)) + _, err := ReconcileIndexerCluster(c, cr.(*enterprisev1.IndexerCluster)) + return err } reconcileTester(t, "TestReconcileIndexerCluster", ¤t, revised, createCalls, updateCalls, reconcile) @@ -56,7 +57,7 @@ func TestReconcileIndexerCluster(t *testing.T) { revised.ObjectMeta.DeletionTimestamp = ¤tTime revised.ObjectMeta.Finalizers = []string{"enterprise.splunk.com/delete-pvc"} deleteFunc := func(cr enterprisev1.MetaObject, c ControllerClient) (bool, error) { - err := ReconcileIndexerCluster(c, cr.(*enterprisev1.IndexerCluster)) + _, err := ReconcileIndexerCluster(c, cr.(*enterprisev1.IndexerCluster)) return true, err } splunkDeletionTester(t, revised, deleteFunc) diff --git a/pkg/splunk/reconcile/licensemaster.go b/pkg/splunk/reconcile/licensemaster.go index e0a74e0ee..a99739090 100644 --- a/pkg/splunk/reconcile/licensemaster.go +++ b/pkg/splunk/reconcile/licensemaster.go @@ -16,18 +16,27 @@ package deploy import ( "context" + "time" + + "sigs.k8s.io/controller-runtime/pkg/reconcile" enterprisev1 "github.com/splunk/splunk-operator/pkg/apis/enterprise/v1alpha2" "github.com/splunk/splunk-operator/pkg/splunk/enterprise" ) // ReconcileLicenseMaster reconciles the state for the Splunk Enterprise license master. -func ReconcileLicenseMaster(client ControllerClient, cr *enterprisev1.LicenseMaster) error { +func ReconcileLicenseMaster(client ControllerClient, cr *enterprisev1.LicenseMaster) (reconcile.Result, error) { + + // unless modified, reconcile for this object will be requeued after 5 seconds + result := reconcile.Result{ + Requeue: true, + RequeueAfter: time.Second * 5, + } // validate and updates defaults for CR err := enterprise.ValidateLicenseMasterSpec(&cr.Spec) if err != nil { - return err + return result, err } // updates status after function completes @@ -41,36 +50,38 @@ func ReconcileLicenseMaster(client ControllerClient, cr *enterprisev1.LicenseMas terminating, err := CheckSplunkDeletion(cr, client) if terminating && err != nil { // don't bother if no error, since it will just be removed immmediately after cr.Status.Phase = enterprisev1.PhaseTerminating - client.Status().Update(context.TODO(), cr) + } else { + result.Requeue = false } - return err + return result, err } // create or update general config resources _, err = ReconcileSplunkConfig(client, cr, cr.Spec.CommonSplunkSpec, enterprise.SplunkLicenseMaster) if err != nil { - return err + return result, err } // create or update a service err = ApplyService(client, enterprise.GetSplunkService(cr, cr.Spec.CommonSpec, enterprise.SplunkLicenseMaster, false)) if err != nil { - return err + return result, err } // create or update statefulset statefulSet, err := enterprise.GetLicenseMasterStatefulSet(cr) if err != nil { - return err + return result, err } cr.Status.Phase, err = ApplyStatefulSet(client, statefulSet) if err == nil && cr.Status.Phase == enterprisev1.PhaseReady { - cr.Status.Phase, err = ReconcileStatefulSetPods(client, statefulSet, statefulSet.Status.ReadyReplicas, 1, nil) + mgr := DefaultStatefulSetPodManager{} + cr.Status.Phase, err = ReconcileStatefulSetPods(client, statefulSet, &mgr, 1) } if err != nil { cr.Status.Phase = enterprisev1.PhaseError - return err + } else if cr.Status.Phase == enterprisev1.PhaseReady { + result.Requeue = false } - - return nil + return result, err } diff --git a/pkg/splunk/reconcile/licensemaster_test.go b/pkg/splunk/reconcile/licensemaster_test.go index 2f80b244d..70d03d7a0 100644 --- a/pkg/splunk/reconcile/licensemaster_test.go +++ b/pkg/splunk/reconcile/licensemaster_test.go @@ -43,7 +43,8 @@ func TestReconcileLicenseMaster(t *testing.T) { revised := current.DeepCopy() revised.Spec.Image = "splunk/test" reconcile := func(c *mockClient, cr interface{}) error { - return ReconcileLicenseMaster(c, cr.(*enterprisev1.LicenseMaster)) + _, err := ReconcileLicenseMaster(c, cr.(*enterprisev1.LicenseMaster)) + return err } reconcileTester(t, "TestReconcileLicenseMaster", ¤t, revised, createCalls, updateCalls, reconcile) @@ -52,7 +53,7 @@ func TestReconcileLicenseMaster(t *testing.T) { revised.ObjectMeta.DeletionTimestamp = ¤tTime revised.ObjectMeta.Finalizers = []string{"enterprise.splunk.com/delete-pvc"} deleteFunc := func(cr enterprisev1.MetaObject, c ControllerClient) (bool, error) { - err := ReconcileLicenseMaster(c, cr.(*enterprisev1.LicenseMaster)) + _, err := ReconcileLicenseMaster(c, cr.(*enterprisev1.LicenseMaster)) return true, err } splunkDeletionTester(t, revised, deleteFunc) diff --git a/pkg/splunk/reconcile/searchhead.go b/pkg/splunk/reconcile/searchhead.go deleted file mode 100644 index 19a84fc05..000000000 --- a/pkg/splunk/reconcile/searchhead.go +++ /dev/null @@ -1,113 +0,0 @@ -// 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 deploy - -import ( - "context" - "fmt" - - enterprisev1 "github.com/splunk/splunk-operator/pkg/apis/enterprise/v1alpha2" - "github.com/splunk/splunk-operator/pkg/splunk/enterprise" -) - -// ReconcileSearchHeadCluster reconciles the state for a Splunk Enterprise search head cluster. -func ReconcileSearchHeadCluster(client ControllerClient, cr *enterprisev1.SearchHeadCluster) error { - scopedLog := log.WithName("ReconcileSearchHeadCluster").WithValues("name", cr.GetIdentifier(), "namespace", cr.GetNamespace()) - - // validate and updates defaults for CR - err := enterprise.ValidateSearchHeadClusterSpec(&cr.Spec) - if err != nil { - return err - } - - // updates status after function completes - cr.Status.Phase = enterprisev1.PhaseError - cr.Status.Replicas = cr.Spec.Replicas - cr.Status.Selector = fmt.Sprintf("app.kubernetes.io/instance=splunk-%s-search-head", cr.GetIdentifier()) - defer func() { - client.Status().Update(context.TODO(), cr) - }() - - // check if deletion has been requested - if cr.ObjectMeta.DeletionTimestamp != nil { - terminating, err := CheckSplunkDeletion(cr, client) - if terminating && err != nil { // don't bother if no error, since it will just be removed immmediately after - cr.Status.Phase = enterprisev1.PhaseTerminating - cr.Status.DeployerPhase = enterprisev1.PhaseTerminating - } - return err - } - - // create or update general config resources - secrets, err := ReconcileSplunkConfig(client, cr, cr.Spec.CommonSplunkSpec, enterprise.SplunkSearchHead) - if err != nil { - return err - } - - // create or update a headless search head cluster service - err = ApplyService(client, enterprise.GetSplunkService(cr, cr.Spec.CommonSpec, enterprise.SplunkSearchHead, true)) - if err != nil { - return err - } - - // create or update a regular search head cluster service - err = ApplyService(client, enterprise.GetSplunkService(cr, cr.Spec.CommonSpec, enterprise.SplunkSearchHead, false)) - if err != nil { - return err - } - - // create or update a deployer service - err = ApplyService(client, enterprise.GetSplunkService(cr, cr.Spec.CommonSpec, enterprise.SplunkDeployer, false)) - if err != nil { - return err - } - - // create or update statefulset for the deployer - statefulSet, err := enterprise.GetDeployerStatefulSet(cr) - if err != nil { - return err - } - cr.Status.DeployerPhase, err = ApplyStatefulSet(client, statefulSet) - if err == nil && cr.Status.DeployerPhase == enterprisev1.PhaseReady { - cr.Status.DeployerPhase, err = ReconcileStatefulSetPods(client, statefulSet, statefulSet.Status.ReadyReplicas, 1, nil) - } - if err != nil { - cr.Status.DeployerPhase = enterprisev1.PhaseError - return err - } - - // create or update statefulset for the search heads - statefulSet, err = enterprise.GetSearchHeadStatefulSet(cr) - if err != nil { - return err - } - cr.Status.Phase, err = ApplyStatefulSet(client, statefulSet) - cr.Status.ReadyReplicas = statefulSet.Status.ReadyReplicas - if cr.Status.ReadyReplicas > 0 { - err = enterprise.UpdateSearchHeadClusterStatus(cr, secrets) - if err != nil { - scopedLog.Error(err, "Failed to update status") - } - } - if err == nil && cr.Status.Phase == enterprisev1.PhaseReady { - cr.Status.Phase, err = ReconcileStatefulSetPods(client, statefulSet, cr.Status.ReadyReplicas, cr.Spec.Replicas, nil) - } - if err != nil { - cr.Status.Phase = enterprisev1.PhaseError - return err - } - - return nil -} diff --git a/pkg/splunk/reconcile/searchheadcluster.go b/pkg/splunk/reconcile/searchheadcluster.go new file mode 100644 index 000000000..9fff44755 --- /dev/null +++ b/pkg/splunk/reconcile/searchheadcluster.go @@ -0,0 +1,281 @@ +// 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 deploy + +import ( + "context" + "fmt" + "time" + + "github.com/go-logr/logr" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + enterprisev1 "github.com/splunk/splunk-operator/pkg/apis/enterprise/v1alpha2" + "github.com/splunk/splunk-operator/pkg/splunk/enterprise" + "github.com/splunk/splunk-operator/pkg/splunk/resources" +) + +// ReconcileSearchHeadCluster reconciles the state for a Splunk Enterprise search head cluster. +func ReconcileSearchHeadCluster(client ControllerClient, cr *enterprisev1.SearchHeadCluster) (reconcile.Result, error) { + // unless modified, reconcile for this object will be requeued after 5 seconds + result := reconcile.Result{ + Requeue: true, + RequeueAfter: time.Second * 5, + } + scopedLog := log.WithName("ReconcileSearchHeadCluster").WithValues("name", cr.GetIdentifier(), "namespace", cr.GetNamespace()) + + // validate and updates defaults for CR + err := enterprise.ValidateSearchHeadClusterSpec(&cr.Spec) + if err != nil { + return result, err + } + + // updates status after function completes + cr.Status.Phase = enterprisev1.PhaseError + cr.Status.Replicas = cr.Spec.Replicas + cr.Status.Selector = fmt.Sprintf("app.kubernetes.io/instance=splunk-%s-search-head", cr.GetIdentifier()) + defer func() { + client.Status().Update(context.TODO(), cr) + }() + + // check if deletion has been requested + if cr.ObjectMeta.DeletionTimestamp != nil { + terminating, err := CheckSplunkDeletion(cr, client) + if terminating && err != nil { // don't bother if no error, since it will just be removed immmediately after + cr.Status.Phase = enterprisev1.PhaseTerminating + cr.Status.DeployerPhase = enterprisev1.PhaseTerminating + } else { + result.Requeue = false + } + return result, err + } + + // create or update general config resources + secrets, err := ReconcileSplunkConfig(client, cr, cr.Spec.CommonSplunkSpec, enterprise.SplunkSearchHead) + if err != nil { + return result, err + } + + // create or update a headless search head cluster service + err = ApplyService(client, enterprise.GetSplunkService(cr, cr.Spec.CommonSpec, enterprise.SplunkSearchHead, true)) + if err != nil { + return result, err + } + + // create or update a regular search head cluster service + err = ApplyService(client, enterprise.GetSplunkService(cr, cr.Spec.CommonSpec, enterprise.SplunkSearchHead, false)) + if err != nil { + return result, err + } + + // create or update a deployer service + err = ApplyService(client, enterprise.GetSplunkService(cr, cr.Spec.CommonSpec, enterprise.SplunkDeployer, false)) + if err != nil { + return result, err + } + + // create or update statefulset for the deployer + statefulSet, err := enterprise.GetDeployerStatefulSet(cr) + if err != nil { + return result, err + } + cr.Status.DeployerPhase, err = ApplyStatefulSet(client, statefulSet) + if err == nil && cr.Status.DeployerPhase == enterprisev1.PhaseReady { + mgr := DefaultStatefulSetPodManager{} + cr.Status.DeployerPhase, err = ReconcileStatefulSetPods(client, statefulSet, &mgr, 1) + } + if err != nil { + cr.Status.DeployerPhase = enterprisev1.PhaseError + return result, err + } + + // create or update statefulset for the search heads + statefulSet, err = enterprise.GetSearchHeadStatefulSet(cr) + if err != nil { + return result, err + } + cr.Status.Phase, err = ApplyStatefulSet(client, statefulSet) + if err != nil { + return result, err + } + + // update CR status with SHC information + mgr := SearchHeadClusterPodManager{client: client, log: scopedLog, cr: cr, secrets: secrets} + err = mgr.updateStatus(statefulSet) + if err != nil || cr.Status.ReadyReplicas == 0 || !cr.Status.Initialized || !cr.Status.CaptainReady { + scopedLog.Error(err, "Search head cluster is not ready") + cr.Status.Phase = enterprisev1.PhasePending + return result, nil + } + + // manage scaling and updates + cr.Status.Phase, err = ReconcileStatefulSetPods(client, statefulSet, &mgr, cr.Spec.Replicas) + if err != nil { + cr.Status.Phase = enterprisev1.PhaseError + return result, err + } + + // no need to requeue if everything is ready + if cr.Status.Phase == enterprisev1.PhaseReady { + result.Requeue = false + } + return result, nil +} + +// SearchHeadClusterPodManager is used to manage the pods within a search head cluster +type SearchHeadClusterPodManager struct { + client ControllerClient + log logr.Logger + cr *enterprisev1.SearchHeadCluster + secrets *corev1.Secret +} + +// Decommission for SearchHeadClusterPodManager decommissions search head cluster member; it returns true when complete +func (mgr *SearchHeadClusterPodManager) Decommission(n int32) (bool, error) { + // start by quarantining the pod + result, err := mgr.Quarantine(n) + if err != nil || !result { + return result, err + } + + // pod is quarantined; decommission it + memberName := enterprise.GetSplunkStatefulsetPodName(enterprise.SplunkSearchHead, mgr.cr.GetIdentifier(), n) + mgr.log.Info("Removing member from search head cluster", "memberName", memberName) + c := mgr.getClient(n) + err = c.RemoveSearchHeadClusterMember() + if err != nil { + return false, err + } + + // delete PVCs used by the pod so that a future scale up will have clean state + for _, vol := range []string{"pvc-etc", "pvc-var"} { + namespacedName := types.NamespacedName{ + Namespace: mgr.cr.GetNamespace(), + Name: fmt.Sprintf("%s-%s", vol, memberName), + } + var pvc corev1.PersistentVolumeClaim + err := mgr.client.Get(context.TODO(), namespacedName, &pvc) + if err != nil { + return false, err + } + + log.Info("Deleting PVC", "name", pvc.ObjectMeta.Name) + err = mgr.client.Delete(context.Background(), &pvc) + if err != nil { + return false, err + } + } + + // all done -> ok to scale down the statefulset + return true, nil +} + +// Quarantine for SearchHeadClusterPodManager quarantines a search head cluster member; it returns true when complete +func (mgr *SearchHeadClusterPodManager) Quarantine(n int32) (bool, error) { + memberName := enterprise.GetSplunkStatefulsetPodName(enterprise.SplunkSearchHead, mgr.cr.GetIdentifier(), n) + + switch mgr.cr.Status.Members[n].Status { + case "Up": + // Detain search head + mgr.log.Info("Detaining search head cluster member", "memberName", memberName) + c := mgr.getClient(n) + return false, c.SetSearchHeadDetention(true) + + case "ManualDetention": + // Wait until active searches have drained + searchesComplete := mgr.cr.Status.Members[n].ActiveSearches == 0 + if searchesComplete { + mgr.log.Info("Detention complete", "memberName", memberName) + } else { + mgr.log.Info("Waiting for active searches to complete", "memberName", memberName) + } + return searchesComplete, nil + } + + // unhandled status + return false, fmt.Errorf("Status=%s", mgr.cr.Status.Members[n].Status) +} + +// ReleaseQuarantine for SearchHeadClusterPodManager releases quarantine and returns true, or returns false if not quarantined +func (mgr *SearchHeadClusterPodManager) ReleaseQuarantine(n int32) (bool, error) { + memberName := enterprise.GetSplunkStatefulsetPodName(enterprise.SplunkSearchHead, mgr.cr.GetIdentifier(), n) + + switch mgr.cr.Status.Members[n].Status { + case "Up": + // not in detention + return false, nil + + case "ManualDetention": + // release from detention + mgr.log.Info("Releasing search head cluster member from detention", "memberName", memberName) + c := mgr.getClient(n) + return true, c.SetSearchHeadDetention(false) + } + + // unhandled status + return false, fmt.Errorf("Status=%s", mgr.cr.Status.Members[n].Status) +} + +// getClient for SearchHeadClusterPodManager returns a SplunkClient for the member n +func (mgr *SearchHeadClusterPodManager) getClient(n int32) *enterprise.SplunkClient { + memberName := enterprise.GetSplunkStatefulsetPodName(enterprise.SplunkSearchHead, mgr.cr.GetIdentifier(), n) + fqdnName := resources.GetServiceFQDN(mgr.cr.GetNamespace(), + fmt.Sprintf("%s.%s", memberName, enterprise.GetSplunkServiceName(enterprise.SplunkSearchHead, mgr.cr.GetIdentifier(), true))) + return enterprise.NewSplunkClient(fmt.Sprintf("https://%s:8089", fqdnName), "admin", string(mgr.secrets.Data["password"])) +} + +// updateStatus for SearchHeadClusterPodManager uses the REST API to update the status for a SearcHead custom resource +func (mgr *SearchHeadClusterPodManager) updateStatus(statefulSet *appsv1.StatefulSet) error { + // populate members status using REST API to get search head cluster member info + mgr.cr.Status.ReadyReplicas = statefulSet.Status.ReadyReplicas + if mgr.cr.Status.ReadyReplicas == 0 { + return nil + } + mgr.cr.Status.Members = []enterprisev1.SearchHeadClusterMemberStatus{} + for n := int32(0); n < mgr.cr.Status.ReadyReplicas; n++ { + c := mgr.getClient(n) + memberName := enterprise.GetSplunkStatefulsetPodName(enterprise.SplunkSearchHead, mgr.cr.GetIdentifier(), n) + memberStatus := enterprisev1.SearchHeadClusterMemberStatus{Name: memberName} + memberInfo, err := c.GetSearchHeadClusterMemberInfo() + if err == nil { + memberStatus.Status = memberInfo.Status + memberStatus.Registered = memberInfo.Registered + memberStatus.ActiveSearches = memberInfo.ActiveHistoricalSearchCount + memberInfo.ActiveRealtimeSearchCount + } else if n < statefulSet.Status.Replicas { + // ignore error if pod was just terminated for scale down event (n >= Replicas) + mgr.log.Error(err, "Unable to retrieve search head cluster member info", "memberName", memberName) + return err + } + mgr.cr.Status.Members = append(mgr.cr.Status.Members, memberStatus) + } + + // get search head cluster info from captain + fqdnName := resources.GetServiceFQDN(mgr.cr.GetNamespace(), enterprise.GetSplunkServiceName(enterprise.SplunkSearchHead, mgr.cr.GetIdentifier(), false)) + c := enterprise.NewSplunkClient(fmt.Sprintf("https://%s:8089", fqdnName), "admin", string(mgr.secrets.Data["password"])) + captainInfo, err := c.GetSearchHeadCaptainInfo() + if err != nil { + return err + } + mgr.cr.Status.Captain = captainInfo.Label + mgr.cr.Status.CaptainReady = captainInfo.ServiceReadyFlag + mgr.cr.Status.Initialized = captainInfo.InitializedFlag + mgr.cr.Status.MinPeersJoined = captainInfo.MinPeersJoinedFlag + mgr.cr.Status.MaintenanceMode = captainInfo.MaintenanceMode + + return nil +} diff --git a/pkg/splunk/reconcile/searchhead_test.go b/pkg/splunk/reconcile/searchheadcluster_test.go similarity index 92% rename from pkg/splunk/reconcile/searchhead_test.go rename to pkg/splunk/reconcile/searchheadcluster_test.go index 584a8eee5..723aca723 100644 --- a/pkg/splunk/reconcile/searchhead_test.go +++ b/pkg/splunk/reconcile/searchheadcluster_test.go @@ -46,7 +46,8 @@ func TestReconcileSearchHeadCluster(t *testing.T) { revised := current.DeepCopy() revised.Spec.Image = "splunk/test" reconcile := func(c *mockClient, cr interface{}) error { - return ReconcileSearchHeadCluster(c, cr.(*enterprisev1.SearchHeadCluster)) + _, err := ReconcileSearchHeadCluster(c, cr.(*enterprisev1.SearchHeadCluster)) + return err } reconcileTester(t, "TestReconcileSearchHeadCluster", ¤t, revised, createCalls, updateCalls, reconcile) @@ -55,7 +56,7 @@ func TestReconcileSearchHeadCluster(t *testing.T) { revised.ObjectMeta.DeletionTimestamp = ¤tTime revised.ObjectMeta.Finalizers = []string{"enterprise.splunk.com/delete-pvc"} deleteFunc := func(cr enterprisev1.MetaObject, c ControllerClient) (bool, error) { - err := ReconcileSearchHeadCluster(c, cr.(*enterprisev1.SearchHeadCluster)) + _, err := ReconcileSearchHeadCluster(c, cr.(*enterprisev1.SearchHeadCluster)) return true, err } splunkDeletionTester(t, revised, deleteFunc) diff --git a/pkg/splunk/reconcile/spark.go b/pkg/splunk/reconcile/spark.go index 5973ace0e..8cb4422c7 100644 --- a/pkg/splunk/reconcile/spark.go +++ b/pkg/splunk/reconcile/spark.go @@ -17,18 +17,27 @@ package deploy import ( "context" "fmt" + "time" + + "sigs.k8s.io/controller-runtime/pkg/reconcile" enterprisev1 "github.com/splunk/splunk-operator/pkg/apis/enterprise/v1alpha2" "github.com/splunk/splunk-operator/pkg/splunk/spark" ) // ReconcileSpark reconciles the Deployments and Services for a Spark cluster. -func ReconcileSpark(client ControllerClient, cr *enterprisev1.Spark) error { +func ReconcileSpark(client ControllerClient, cr *enterprisev1.Spark) (reconcile.Result, error) { + + // unless modified, reconcile for this object will be requeued after 5 seconds + result := reconcile.Result{ + Requeue: true, + RequeueAfter: time.Second * 5, + } // validate and updates defaults for CR err := spark.ValidateSparkSpec(&cr.Spec) if err != nil { - return err + return result, err } // updates status after function completes @@ -44,45 +53,46 @@ func ReconcileSpark(client ControllerClient, cr *enterprisev1.Spark) error { terminating, err := CheckSplunkDeletion(cr, client) if terminating && err != nil { // don't bother if no error, since it will just be removed immmediately after cr.Status.Phase = enterprisev1.PhaseTerminating - client.Status().Update(context.TODO(), cr) + } else { + result.Requeue = false } - return err + return result, err } // create or update a service for spark master err = ApplyService(client, spark.GetSparkService(cr, spark.SparkMaster, false)) if err != nil { - return err + return result, err } // create or update a headless service for spark workers err = ApplyService(client, spark.GetSparkService(cr, spark.SparkWorker, true)) if err != nil { - return err + return result, err } // create or update deployment for spark master deployment, err := spark.GetSparkDeployment(cr, spark.SparkMaster) if err != nil { - return err + return result, err } cr.Status.MasterPhase, err = ApplyDeployment(client, deployment) if err != nil { cr.Status.MasterPhase = enterprisev1.PhaseError - return err + return result, err } // create or update deployment for spark worker deployment, err = spark.GetSparkDeployment(cr, spark.SparkWorker) if err != nil { - return err + return result, err } cr.Status.Phase, err = ApplyDeployment(client, deployment) cr.Status.ReadyReplicas = deployment.Status.ReadyReplicas if err != nil { cr.Status.Phase = enterprisev1.PhaseError - return err + } else if cr.Status.Phase == enterprisev1.PhaseReady { + result.Requeue = false } - - return nil + return result, err } diff --git a/pkg/splunk/reconcile/spark_test.go b/pkg/splunk/reconcile/spark_test.go index 17e1eee7d..e1a13bb35 100644 --- a/pkg/splunk/reconcile/spark_test.go +++ b/pkg/splunk/reconcile/spark_test.go @@ -44,7 +44,8 @@ func TestReconcileSpark(t *testing.T) { revised := current.DeepCopy() revised.Spec.Image = "splunk/test" reconcile := func(c *mockClient, cr interface{}) error { - return ReconcileSpark(c, cr.(*enterprisev1.Spark)) + _, err := ReconcileSpark(c, cr.(*enterprisev1.Spark)) + return err } reconcileTester(t, "TestReconcileSpark", ¤t, revised, createCalls, updateCalls, reconcile) @@ -53,7 +54,7 @@ func TestReconcileSpark(t *testing.T) { revised.ObjectMeta.DeletionTimestamp = ¤tTime revised.ObjectMeta.Finalizers = []string{"enterprise.splunk.com/delete-pvc"} deleteFunc := func(cr enterprisev1.MetaObject, c ControllerClient) (bool, error) { - err := ReconcileSpark(c, cr.(*enterprisev1.Spark)) + _, err := ReconcileSpark(c, cr.(*enterprisev1.Spark)) return true, err } splunkDeletionTester(t, revised, deleteFunc) diff --git a/pkg/splunk/reconcile/standalone.go b/pkg/splunk/reconcile/standalone.go index a648682a5..129c00da1 100644 --- a/pkg/splunk/reconcile/standalone.go +++ b/pkg/splunk/reconcile/standalone.go @@ -17,18 +17,27 @@ package deploy import ( "context" "fmt" + "time" + + "sigs.k8s.io/controller-runtime/pkg/reconcile" enterprisev1 "github.com/splunk/splunk-operator/pkg/apis/enterprise/v1alpha2" "github.com/splunk/splunk-operator/pkg/splunk/enterprise" ) // ReconcileStandalone reconciles the StatefulSet for N standalone instances of Splunk Enterprise. -func ReconcileStandalone(client ControllerClient, cr *enterprisev1.Standalone) error { +func ReconcileStandalone(client ControllerClient, cr *enterprisev1.Standalone) (reconcile.Result, error) { + + // unless modified, reconcile for this object will be requeued after 5 seconds + result := reconcile.Result{ + Requeue: true, + RequeueAfter: time.Second * 5, + } // validate and updates defaults for CR err := enterprise.ValidateStandaloneSpec(&cr.Spec) if err != nil { - return err + return result, err } // updates status after function completes @@ -44,37 +53,39 @@ func ReconcileStandalone(client ControllerClient, cr *enterprisev1.Standalone) e terminating, err := CheckSplunkDeletion(cr, client) if terminating && err != nil { // don't bother if no error, since it will just be removed immmediately after cr.Status.Phase = enterprisev1.PhaseTerminating - client.Status().Update(context.TODO(), cr) + } else { + result.Requeue = false } - return err + return result, err } // create or update general config resources _, err = ReconcileSplunkConfig(client, cr, cr.Spec.CommonSplunkSpec, enterprise.SplunkStandalone) if err != nil { - return err + return result, err } // create or update a headless service (this is required by DFS for Spark->standalone comms, possibly other things) err = ApplyService(client, enterprise.GetSplunkService(cr, cr.Spec.CommonSpec, enterprise.SplunkStandalone, true)) if err != nil { - return err + return result, err } // create or update statefulset statefulSet, err := enterprise.GetStandaloneStatefulSet(cr) if err != nil { - return err + return result, err } cr.Status.Phase, err = ApplyStatefulSet(client, statefulSet) cr.Status.ReadyReplicas = statefulSet.Status.ReadyReplicas if err == nil && cr.Status.Phase == enterprisev1.PhaseReady { - cr.Status.Phase, err = ReconcileStatefulSetPods(client, statefulSet, cr.Status.ReadyReplicas, cr.Spec.Replicas, nil) + mgr := DefaultStatefulSetPodManager{} + cr.Status.Phase, err = ReconcileStatefulSetPods(client, statefulSet, &mgr, cr.Spec.Replicas) } if err != nil { cr.Status.Phase = enterprisev1.PhaseError - return err + } else if cr.Status.Phase == enterprisev1.PhaseReady { + result.Requeue = false } - - return nil + return result, err } diff --git a/pkg/splunk/reconcile/standalone_test.go b/pkg/splunk/reconcile/standalone_test.go index ce2dd6e7d..8b44b94ac 100644 --- a/pkg/splunk/reconcile/standalone_test.go +++ b/pkg/splunk/reconcile/standalone_test.go @@ -43,7 +43,8 @@ func TestReconcileStandalone(t *testing.T) { revised := current.DeepCopy() revised.Spec.Image = "splunk/test" reconcile := func(c *mockClient, cr interface{}) error { - return ReconcileStandalone(c, cr.(*enterprisev1.Standalone)) + _, err := ReconcileStandalone(c, cr.(*enterprisev1.Standalone)) + return err } reconcileTester(t, "TestReconcileStandalone", ¤t, revised, createCalls, updateCalls, reconcile) @@ -52,7 +53,7 @@ func TestReconcileStandalone(t *testing.T) { revised.ObjectMeta.DeletionTimestamp = ¤tTime revised.ObjectMeta.Finalizers = []string{"enterprise.splunk.com/delete-pvc"} deleteFunc := func(cr enterprisev1.MetaObject, c ControllerClient) (bool, error) { - err := ReconcileStandalone(c, cr.(*enterprisev1.Standalone)) + _, err := ReconcileStandalone(c, cr.(*enterprisev1.Standalone)) return true, err } splunkDeletionTester(t, revised, deleteFunc) diff --git a/pkg/splunk/reconcile/statefulset.go b/pkg/splunk/reconcile/statefulset.go index ab302f961..12a59f409 100644 --- a/pkg/splunk/reconcile/statefulset.go +++ b/pkg/splunk/reconcile/statefulset.go @@ -26,6 +26,36 @@ import ( enterprisev1 "github.com/splunk/splunk-operator/pkg/apis/enterprise/v1alpha2" ) +// StatefulSetPodManager is used to manage the pods within a StatefulSet +type StatefulSetPodManager interface { + // Decommision pod and return true if complete + Decommission(n int32) (bool, error) + + // Quarantine pod and return true if complete + Quarantine(n int32) (bool, error) + + // ReleaseQuarantine will release a quarantine and return true, if active; it returns false if none active + ReleaseQuarantine(n int32) (bool, error) +} + +// DefaultStatefulSetPodManager is a simple StatefulSetPodManager that does nothing +type DefaultStatefulSetPodManager struct{} + +// Decommission for DefaultStatefulSetPodManager does nothing and returns true +func (mgr *DefaultStatefulSetPodManager) Decommission(n int32) (bool, error) { + return true, nil +} + +// Quarantine for DefaultStatefulSetPodManager does nothing and returns true +func (mgr *DefaultStatefulSetPodManager) Quarantine(n int32) (bool, error) { + return true, nil +} + +// ReleaseQuarantine for DefaultStatefulSetPodManager does nothing and returns false +func (mgr *DefaultStatefulSetPodManager) ReleaseQuarantine(n int32) (bool, error) { + return false, nil +} + // ApplyStatefulSet creates or updates a Kubernetes StatefulSet func ApplyStatefulSet(c ControllerClient, revised *appsv1.StatefulSet) (enterprisev1.ResourcePhase, error) { namespacedName := types.NamespacedName{Namespace: revised.GetNamespace(), Name: revised.GetName()} @@ -57,63 +87,64 @@ func ApplyStatefulSet(c ControllerClient, revised *appsv1.StatefulSet) (enterpri } // ReconcileStatefulSetPods manages scaling and config updates for StatefulSets -func ReconcileStatefulSetPods(c ControllerClient, statefulSet *appsv1.StatefulSet, readyReplicas, desiredReplicas int32, - removeReadyPod func(ControllerClient, *appsv1.StatefulSet) (enterprisev1.ResourcePhase, error)) (enterprisev1.ResourcePhase, error) { +func ReconcileStatefulSetPods(c ControllerClient, statefulSet *appsv1.StatefulSet, mgr StatefulSetPodManager, desiredReplicas int32) (enterprisev1.ResourcePhase, error) { scopedLog := log.WithName("ReconcileStatefulSetPods").WithValues( "name", statefulSet.GetObjectMeta().GetName(), "namespace", statefulSet.GetObjectMeta().GetNamespace()) - // inline function used to handle scale down - scaleDown := func(replicas int32) error { - scopedLog.Info(fmt.Sprintf("Scaling replicas down to %d", replicas)) - *statefulSet.Spec.Replicas = replicas - return UpdateResource(c, statefulSet) - } - - // check for scaling down - if readyReplicas > desiredReplicas { - if removeReadyPod != nil { - return removeReadyPod(c, statefulSet) - } - return enterprisev1.PhaseScalingDown, scaleDown(readyReplicas - 1) - } - - // check if replicas are not yet ready - if readyReplicas < desiredReplicas { - if *statefulSet.Spec.Replicas < desiredReplicas { - // scale up StatefulSet to match desiredReplicas - scopedLog.Info(fmt.Sprintf("Scaling replicas up to %d", desiredReplicas)) - *statefulSet.Spec.Replicas = desiredReplicas - return enterprisev1.PhaseScalingUp, UpdateResource(c, statefulSet) - } - - if statefulSet.Status.UpdatedReplicas < statefulSet.Status.Replicas { - scopedLog.Info("Waiting for updates to complete") - return enterprisev1.PhaseUpdating, nil - } - + // wait for all replicas ready + replicas := statefulSet.Status.Replicas + readyReplicas := statefulSet.Status.ReadyReplicas + if readyReplicas < replicas { scopedLog.Info("Waiting for pods to become ready") if readyReplicas > 0 { return enterprisev1.PhaseScalingUp, nil } return enterprisev1.PhasePending, nil + } else if readyReplicas > replicas { + scopedLog.Info("Waiting for scale down to complete") + return enterprisev1.PhaseScalingDown, nil } - // readyReplicas == desiredReplicas + // readyReplicas == replicas + + // check for scaling up + if readyReplicas < desiredReplicas { + // scale up StatefulSet to match desiredReplicas + scopedLog.Info("Scaling replicas up", "replicas", desiredReplicas) + *statefulSet.Spec.Replicas = desiredReplicas + return enterprisev1.PhaseScalingUp, UpdateResource(c, statefulSet) + } + + // check for scaling down + if readyReplicas > desiredReplicas { + // decommission pod to prepare for removal + n := readyReplicas - 1 + complete, err := mgr.Decommission(n) + if err != nil { + podName := fmt.Sprintf("%s-%d", statefulSet.GetName(), n) + scopedLog.Error(err, "Unable to decommission Pod", "podName", podName) + return enterprisev1.PhaseError, err + } + if !complete { + // wait until pod quarantine has completed before deleting it + return enterprisev1.PhaseScalingDown, nil + } - // check if we have extra pods in statefulset - if *statefulSet.Spec.Replicas != desiredReplicas { - // scale down StatefulSet to match readyReplicas - return enterprisev1.PhaseScalingDown, scaleDown(readyReplicas) + // scale down statefulset to terminate pod + scopedLog.Info("Scaling replicas down", "replicas", n) + *statefulSet.Spec.Replicas = n + return enterprisev1.PhaseScalingDown, UpdateResource(c, statefulSet) } - // ready and no StatefulSet scaling required + // ready and no StatefulSet scaling is required + // readyReplicas == desiredReplicas // check existing pods for desired updates - for n := *statefulSet.Spec.Replicas; n > 0; n-- { + for n := readyReplicas - 1; n >= 0; n-- { // get Pod - podName := fmt.Sprintf("%s-%d", statefulSet.GetName(), n-1) + podName := fmt.Sprintf("%s-%d", statefulSet.GetName(), n) namespacedName := types.NamespacedName{Namespace: statefulSet.GetNamespace(), Name: podName} var pod corev1.Pod err := c.Get(context.TODO(), namespacedName, &pod) @@ -124,6 +155,18 @@ func ReconcileStatefulSetPods(c ControllerClient, statefulSet *appsv1.StatefulSe // terminate pod if it has pending updates; k8s will start a new one with revised template if statefulSet.Status.UpdateRevision != "" && statefulSet.Status.UpdateRevision != pod.GetLabels()["controller-revision-hash"] { + // pod needs to be updated; first, quarantine it to prepare for restart + complete, err := mgr.Quarantine(n) + if err != nil { + scopedLog.Error(err, "Unable to quarantine Pod", "podName", podName) + return enterprisev1.PhaseError, err + } + if !complete { + // wait until pod quarantine has completed before deleting it + return enterprisev1.PhaseUpdating, nil + } + + // deleting pod will cause StatefulSet controller to create a new one with latest template scopedLog.Info("Recycling Pod for updates", "podName", podName, "statefulSetRevision", statefulSet.Status.CurrentRevision, "podRevision", pod.GetLabels()["controller-revision-hash"]) @@ -133,9 +176,21 @@ func ReconcileStatefulSetPods(c ControllerClient, statefulSet *appsv1.StatefulSe scopedLog.Error(err, "Unable to delete Pod", "podName", podName) return enterprisev1.PhaseError, err } + // only delete one at a time return enterprisev1.PhaseUpdating, nil } + + // check if pod was previously quarantined; if so, it's ok to release it + released, err := mgr.ReleaseQuarantine(n) + if err != nil { + scopedLog.Error(err, "Unable to release Pod from quarantine", "podName", podName) + return enterprisev1.PhaseError, err + } + if released { + // if pod was released, return and wait until next reconcile to let things settle down + return enterprisev1.PhaseUpdating, nil + } } // all is good! From 3e0c19da2be30ca8867626cb734ed8b146847f53 Mon Sep 17 00:00:00 2001 From: Mike Dickey Date: Tue, 17 Mar 2020 18:00:45 -0700 Subject: [PATCH 5/7] Added support for managed scaling of IndexerCluster Fixed naming of splunk.reconcile package --- build/Dockerfile | 8 +- ..._Universal_Base_Image_English_20190422.pdf | Bin 0 -> 243884 bytes deploy/all-in-one-cluster.yaml | 64 +++- deploy/all-in-one-scoped.yaml | 64 +++- deploy/crds/combined.yaml | 64 +++- ...rprise.splunk.com_indexerclusters_crd.yaml | 43 +++ ...ise.splunk.com_searchheadclusters_crd.yaml | 21 +- .../v1alpha2/indexercluster_types.go | 36 ++ .../v1alpha2/searchheadcluster_types.go | 16 +- .../v1alpha2/zz_generated.deepcopy.go | 23 +- .../indexercluster_controller.go | 2 +- .../licensemaster/licensemaster_controller.go | 2 +- .../searchheadcluster_controller.go | 2 +- pkg/controller/spark/spark_controller.go | 2 +- .../standalone/standalone_controller.go | 2 +- pkg/splunk/enterprise/restapi.go | 307 +++++++++++++++++- pkg/splunk/enterprise/restapi_test.go | 149 +++++++++ pkg/splunk/reconcile/config.go | 6 +- pkg/splunk/reconcile/config_test.go | 14 +- pkg/splunk/reconcile/deployment.go | 2 +- pkg/splunk/reconcile/deployment_test.go | 2 +- pkg/splunk/reconcile/doc.go | 6 +- pkg/splunk/reconcile/finalizers.go | 2 +- pkg/splunk/reconcile/finalizers_test.go | 2 +- pkg/splunk/reconcile/indexercluster.go | 174 +++++++++- pkg/splunk/reconcile/indexercluster_test.go | 10 +- pkg/splunk/reconcile/licensemaster.go | 10 +- pkg/splunk/reconcile/licensemaster_test.go | 10 +- pkg/splunk/reconcile/searchheadcluster.go | 64 ++-- .../reconcile/searchheadcluster_test.go | 10 +- pkg/splunk/reconcile/service.go | 2 +- pkg/splunk/reconcile/service_test.go | 2 +- pkg/splunk/reconcile/spark.go | 6 +- pkg/splunk/reconcile/spark_test.go | 10 +- pkg/splunk/reconcile/standalone.go | 10 +- pkg/splunk/reconcile/standalone_test.go | 10 +- pkg/splunk/reconcile/statefulset.go | 64 ++-- pkg/splunk/reconcile/statefulset_test.go | 2 +- pkg/splunk/reconcile/util.go | 13 +- pkg/splunk/reconcile/util_test.go | 4 +- 40 files changed, 1042 insertions(+), 198 deletions(-) create mode 100644 build/EULA_Red_Hat_Universal_Base_Image_English_20190422.pdf diff --git a/build/Dockerfile b/build/Dockerfile index 38f42143c..c96f5163d 100644 --- a/build/Dockerfile +++ b/build/Dockerfile @@ -16,10 +16,10 @@ LABEL name="splunk" \ COPY build/_output/bin/splunk-operator ${OPERATOR} COPY build/bin /usr/local/bin -RUN mkdir /licenses \ - && curl -o /licenses/apache-2.0.txt https://www.apache.org/licenses/LICENSE-2.0.txt \ - && curl -o /licenses/EULA_Red_Hat_Universal_Base_Image_English_20190422.pdf https://www.redhat.com/licenses/EULA_Red_Hat_Universal_Base_Image_English_20190422.pdf \ - && /usr/local/bin/user_setup +RUN mkdir /licenses && /usr/local/bin/user_setup + +COPY build/EULA_Red_Hat_Universal_Base_Image_English_20190422.pdf /licenses +COPY LICENSE /licenses/LICENSE-2.0.txt ENTRYPOINT ["/usr/local/bin/entrypoint"] diff --git a/build/EULA_Red_Hat_Universal_Base_Image_English_20190422.pdf b/build/EULA_Red_Hat_Universal_Base_Image_English_20190422.pdf new file mode 100644 index 0000000000000000000000000000000000000000..3a32abd7580e3337c6851279a52bec45298b9614 GIT binary patch literal 243884 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 ePPj%Y@!= Replicas) mgr.log.Error(err, "Unable to retrieve search head cluster member info", "memberName", memberName) @@ -264,18 +279,5 @@ func (mgr *SearchHeadClusterPodManager) updateStatus(statefulSet *appsv1.Statefu mgr.cr.Status.Members = append(mgr.cr.Status.Members, memberStatus) } - // get search head cluster info from captain - fqdnName := resources.GetServiceFQDN(mgr.cr.GetNamespace(), enterprise.GetSplunkServiceName(enterprise.SplunkSearchHead, mgr.cr.GetIdentifier(), false)) - c := enterprise.NewSplunkClient(fmt.Sprintf("https://%s:8089", fqdnName), "admin", string(mgr.secrets.Data["password"])) - captainInfo, err := c.GetSearchHeadCaptainInfo() - if err != nil { - return err - } - mgr.cr.Status.Captain = captainInfo.Label - mgr.cr.Status.CaptainReady = captainInfo.ServiceReadyFlag - mgr.cr.Status.Initialized = captainInfo.InitializedFlag - mgr.cr.Status.MinPeersJoined = captainInfo.MinPeersJoinedFlag - mgr.cr.Status.MaintenanceMode = captainInfo.MaintenanceMode - return nil } diff --git a/pkg/splunk/reconcile/searchheadcluster_test.go b/pkg/splunk/reconcile/searchheadcluster_test.go index 723aca723..37aa7fb6e 100644 --- a/pkg/splunk/reconcile/searchheadcluster_test.go +++ b/pkg/splunk/reconcile/searchheadcluster_test.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package deploy +package reconcile import ( "testing" @@ -23,7 +23,7 @@ import ( enterprisev1 "github.com/splunk/splunk-operator/pkg/apis/enterprise/v1alpha2" ) -func TestReconcileSearchHeadCluster(t *testing.T) { +func TestApplySearchHeadCluster(t *testing.T) { funcCalls := []mockFuncCall{ {metaName: "*v1.Secret-test-splunk-stack1-search-head-secrets"}, {metaName: "*v1.Service-test-splunk-stack1-search-head-headless"}, @@ -46,17 +46,17 @@ func TestReconcileSearchHeadCluster(t *testing.T) { revised := current.DeepCopy() revised.Spec.Image = "splunk/test" reconcile := func(c *mockClient, cr interface{}) error { - _, err := ReconcileSearchHeadCluster(c, cr.(*enterprisev1.SearchHeadCluster)) + _, err := ApplySearchHeadCluster(c, cr.(*enterprisev1.SearchHeadCluster)) return err } - reconcileTester(t, "TestReconcileSearchHeadCluster", ¤t, revised, createCalls, updateCalls, reconcile) + reconcileTester(t, "TestApplySearchHeadCluster", ¤t, revised, createCalls, updateCalls, reconcile) // test deletion currentTime := metav1.NewTime(time.Now()) revised.ObjectMeta.DeletionTimestamp = ¤tTime revised.ObjectMeta.Finalizers = []string{"enterprise.splunk.com/delete-pvc"} deleteFunc := func(cr enterprisev1.MetaObject, c ControllerClient) (bool, error) { - _, err := ReconcileSearchHeadCluster(c, cr.(*enterprisev1.SearchHeadCluster)) + _, err := ApplySearchHeadCluster(c, cr.(*enterprisev1.SearchHeadCluster)) return true, err } splunkDeletionTester(t, revised, deleteFunc) diff --git a/pkg/splunk/reconcile/service.go b/pkg/splunk/reconcile/service.go index 0bf52137c..ba42571a0 100644 --- a/pkg/splunk/reconcile/service.go +++ b/pkg/splunk/reconcile/service.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package deploy +package reconcile import ( "context" diff --git a/pkg/splunk/reconcile/service_test.go b/pkg/splunk/reconcile/service_test.go index ec0d4cb42..f2e5898cd 100644 --- a/pkg/splunk/reconcile/service_test.go +++ b/pkg/splunk/reconcile/service_test.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package deploy +package reconcile import ( "testing" diff --git a/pkg/splunk/reconcile/spark.go b/pkg/splunk/reconcile/spark.go index 8cb4422c7..c8a31bf7f 100644 --- a/pkg/splunk/reconcile/spark.go +++ b/pkg/splunk/reconcile/spark.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package deploy +package reconcile import ( "context" @@ -25,8 +25,8 @@ import ( "github.com/splunk/splunk-operator/pkg/splunk/spark" ) -// ReconcileSpark reconciles the Deployments and Services for a Spark cluster. -func ReconcileSpark(client ControllerClient, cr *enterprisev1.Spark) (reconcile.Result, error) { +// ApplySpark reconciles the Deployments and Services for a Spark cluster. +func ApplySpark(client ControllerClient, cr *enterprisev1.Spark) (reconcile.Result, error) { // unless modified, reconcile for this object will be requeued after 5 seconds result := reconcile.Result{ diff --git a/pkg/splunk/reconcile/spark_test.go b/pkg/splunk/reconcile/spark_test.go index e1a13bb35..1f6f72dd0 100644 --- a/pkg/splunk/reconcile/spark_test.go +++ b/pkg/splunk/reconcile/spark_test.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package deploy +package reconcile import ( "testing" @@ -23,7 +23,7 @@ import ( enterprisev1 "github.com/splunk/splunk-operator/pkg/apis/enterprise/v1alpha2" ) -func TestReconcileSpark(t *testing.T) { +func TestApplySpark(t *testing.T) { funcCalls := []mockFuncCall{ {metaName: "*v1.Service-test-splunk-stack1-spark-master-service"}, {metaName: "*v1.Service-test-splunk-stack1-spark-worker-headless"}, @@ -44,17 +44,17 @@ func TestReconcileSpark(t *testing.T) { revised := current.DeepCopy() revised.Spec.Image = "splunk/test" reconcile := func(c *mockClient, cr interface{}) error { - _, err := ReconcileSpark(c, cr.(*enterprisev1.Spark)) + _, err := ApplySpark(c, cr.(*enterprisev1.Spark)) return err } - reconcileTester(t, "TestReconcileSpark", ¤t, revised, createCalls, updateCalls, reconcile) + reconcileTester(t, "TestApplySpark", ¤t, revised, createCalls, updateCalls, reconcile) // test deletion currentTime := metav1.NewTime(time.Now()) revised.ObjectMeta.DeletionTimestamp = ¤tTime revised.ObjectMeta.Finalizers = []string{"enterprise.splunk.com/delete-pvc"} deleteFunc := func(cr enterprisev1.MetaObject, c ControllerClient) (bool, error) { - _, err := ReconcileSpark(c, cr.(*enterprisev1.Spark)) + _, err := ApplySpark(c, cr.(*enterprisev1.Spark)) return true, err } splunkDeletionTester(t, revised, deleteFunc) diff --git a/pkg/splunk/reconcile/standalone.go b/pkg/splunk/reconcile/standalone.go index 129c00da1..ed0cdac94 100644 --- a/pkg/splunk/reconcile/standalone.go +++ b/pkg/splunk/reconcile/standalone.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package deploy +package reconcile import ( "context" @@ -25,8 +25,8 @@ import ( "github.com/splunk/splunk-operator/pkg/splunk/enterprise" ) -// ReconcileStandalone reconciles the StatefulSet for N standalone instances of Splunk Enterprise. -func ReconcileStandalone(client ControllerClient, cr *enterprisev1.Standalone) (reconcile.Result, error) { +// ApplyStandalone reconciles the StatefulSet for N standalone instances of Splunk Enterprise. +func ApplyStandalone(client ControllerClient, cr *enterprisev1.Standalone) (reconcile.Result, error) { // unless modified, reconcile for this object will be requeued after 5 seconds result := reconcile.Result{ @@ -60,7 +60,7 @@ func ReconcileStandalone(client ControllerClient, cr *enterprisev1.Standalone) ( } // create or update general config resources - _, err = ReconcileSplunkConfig(client, cr, cr.Spec.CommonSplunkSpec, enterprise.SplunkStandalone) + _, err = ApplySplunkConfig(client, cr, cr.Spec.CommonSplunkSpec, enterprise.SplunkStandalone) if err != nil { return result, err } @@ -80,7 +80,7 @@ func ReconcileStandalone(client ControllerClient, cr *enterprisev1.Standalone) ( cr.Status.ReadyReplicas = statefulSet.Status.ReadyReplicas if err == nil && cr.Status.Phase == enterprisev1.PhaseReady { mgr := DefaultStatefulSetPodManager{} - cr.Status.Phase, err = ReconcileStatefulSetPods(client, statefulSet, &mgr, cr.Spec.Replicas) + cr.Status.Phase, err = UpdateStatefulSetPods(client, statefulSet, &mgr, cr.Spec.Replicas) } if err != nil { cr.Status.Phase = enterprisev1.PhaseError diff --git a/pkg/splunk/reconcile/standalone_test.go b/pkg/splunk/reconcile/standalone_test.go index 8b44b94ac..a4627d590 100644 --- a/pkg/splunk/reconcile/standalone_test.go +++ b/pkg/splunk/reconcile/standalone_test.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package deploy +package reconcile import ( "testing" @@ -23,7 +23,7 @@ import ( enterprisev1 "github.com/splunk/splunk-operator/pkg/apis/enterprise/v1alpha2" ) -func TestReconcileStandalone(t *testing.T) { +func TestApplyStandalone(t *testing.T) { funcCalls := []mockFuncCall{ {metaName: "*v1.Secret-test-splunk-stack1-standalone-secrets"}, {metaName: "*v1.Service-test-splunk-stack1-standalone-headless"}, @@ -43,17 +43,17 @@ func TestReconcileStandalone(t *testing.T) { revised := current.DeepCopy() revised.Spec.Image = "splunk/test" reconcile := func(c *mockClient, cr interface{}) error { - _, err := ReconcileStandalone(c, cr.(*enterprisev1.Standalone)) + _, err := ApplyStandalone(c, cr.(*enterprisev1.Standalone)) return err } - reconcileTester(t, "TestReconcileStandalone", ¤t, revised, createCalls, updateCalls, reconcile) + reconcileTester(t, "TestApplyStandalone", ¤t, revised, createCalls, updateCalls, reconcile) // test deletion currentTime := metav1.NewTime(time.Now()) revised.ObjectMeta.DeletionTimestamp = ¤tTime revised.ObjectMeta.Finalizers = []string{"enterprise.splunk.com/delete-pvc"} deleteFunc := func(cr enterprisev1.MetaObject, c ControllerClient) (bool, error) { - _, err := ReconcileStandalone(c, cr.(*enterprisev1.Standalone)) + _, err := ApplyStandalone(c, cr.(*enterprisev1.Standalone)) return true, err } splunkDeletionTester(t, revised, deleteFunc) diff --git a/pkg/splunk/reconcile/statefulset.go b/pkg/splunk/reconcile/statefulset.go index 12a59f409..569d4cb4a 100644 --- a/pkg/splunk/reconcile/statefulset.go +++ b/pkg/splunk/reconcile/statefulset.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package deploy +package reconcile import ( "context" @@ -28,32 +28,32 @@ import ( // StatefulSetPodManager is used to manage the pods within a StatefulSet type StatefulSetPodManager interface { - // Decommision pod and return true if complete - Decommission(n int32) (bool, error) + // PrepareScaleDown prepares pod to be removed via scale down event; it returns true when ready + PrepareScaleDown(n int32) (bool, error) - // Quarantine pod and return true if complete - Quarantine(n int32) (bool, error) + // PrepareRecycle prepares pod to be recycled for updates; it returns true when ready + PrepareRecycle(n int32) (bool, error) - // ReleaseQuarantine will release a quarantine and return true, if active; it returns false if none active - ReleaseQuarantine(n int32) (bool, error) + // FinishRecycle completes recycle event for pod and returns true, or returns false if nothing to do + FinishRecycle(n int32) (bool, error) } // DefaultStatefulSetPodManager is a simple StatefulSetPodManager that does nothing type DefaultStatefulSetPodManager struct{} -// Decommission for DefaultStatefulSetPodManager does nothing and returns true -func (mgr *DefaultStatefulSetPodManager) Decommission(n int32) (bool, error) { +// PrepareScaleDown for DefaultStatefulSetPodManager does nothing and returns true +func (mgr *DefaultStatefulSetPodManager) PrepareScaleDown(n int32) (bool, error) { return true, nil } -// Quarantine for DefaultStatefulSetPodManager does nothing and returns true -func (mgr *DefaultStatefulSetPodManager) Quarantine(n int32) (bool, error) { +// PrepareRecycle for DefaultStatefulSetPodManager does nothing and returns true +func (mgr *DefaultStatefulSetPodManager) PrepareRecycle(n int32) (bool, error) { return true, nil } -// ReleaseQuarantine for DefaultStatefulSetPodManager does nothing and returns false -func (mgr *DefaultStatefulSetPodManager) ReleaseQuarantine(n int32) (bool, error) { - return false, nil +// FinishRecycle for DefaultStatefulSetPodManager does nothing and returns false +func (mgr *DefaultStatefulSetPodManager) FinishRecycle(n int32) (bool, error) { + return true, nil } // ApplyStatefulSet creates or updates a Kubernetes StatefulSet @@ -78,23 +78,23 @@ func ApplyStatefulSet(c ControllerClient, revised *appsv1.StatefulSet) (enterpri if hasUpdates { // this updates the desired state template, but doesn't actually modify any pods // because we use an "OnUpdate" strategy https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/#update-strategies - // note also that this ignores Replicas, which is handled below by ReconcileStatefulSetPods + // note also that this ignores Replicas, which is handled below by UpdateStatefulSetPods return enterprisev1.PhaseUpdating, UpdateResource(c, revised) } - // scaling and pod updates are handled by ReconcileStatefulSetPods + // scaling and pod updates are handled by UpdateStatefulSetPods return enterprisev1.PhaseReady, nil } -// ReconcileStatefulSetPods manages scaling and config updates for StatefulSets -func ReconcileStatefulSetPods(c ControllerClient, statefulSet *appsv1.StatefulSet, mgr StatefulSetPodManager, desiredReplicas int32) (enterprisev1.ResourcePhase, error) { +// UpdateStatefulSetPods manages scaling and config updates for StatefulSets +func UpdateStatefulSetPods(c ControllerClient, statefulSet *appsv1.StatefulSet, mgr StatefulSetPodManager, desiredReplicas int32) (enterprisev1.ResourcePhase, error) { - scopedLog := log.WithName("ReconcileStatefulSetPods").WithValues( + scopedLog := log.WithName("UpdateStatefulSetPods").WithValues( "name", statefulSet.GetObjectMeta().GetName(), "namespace", statefulSet.GetObjectMeta().GetNamespace()) // wait for all replicas ready - replicas := statefulSet.Status.Replicas + replicas := *statefulSet.Spec.Replicas readyReplicas := statefulSet.Status.ReadyReplicas if readyReplicas < replicas { scopedLog.Info("Waiting for pods to become ready") @@ -119,15 +119,15 @@ func ReconcileStatefulSetPods(c ControllerClient, statefulSet *appsv1.StatefulSe // check for scaling down if readyReplicas > desiredReplicas { - // decommission pod to prepare for removal + // prepare pod for removal via scale down n := readyReplicas - 1 - complete, err := mgr.Decommission(n) + ready, err := mgr.PrepareScaleDown(n) if err != nil { podName := fmt.Sprintf("%s-%d", statefulSet.GetName(), n) scopedLog.Error(err, "Unable to decommission Pod", "podName", podName) return enterprisev1.PhaseError, err } - if !complete { + if !ready { // wait until pod quarantine has completed before deleting it return enterprisev1.PhaseScalingDown, nil } @@ -155,13 +155,13 @@ func ReconcileStatefulSetPods(c ControllerClient, statefulSet *appsv1.StatefulSe // terminate pod if it has pending updates; k8s will start a new one with revised template if statefulSet.Status.UpdateRevision != "" && statefulSet.Status.UpdateRevision != pod.GetLabels()["controller-revision-hash"] { - // pod needs to be updated; first, quarantine it to prepare for restart - complete, err := mgr.Quarantine(n) + // pod needs to be updated; first, prepare it to be recycled + ready, err := mgr.PrepareRecycle(n) if err != nil { - scopedLog.Error(err, "Unable to quarantine Pod", "podName", podName) + scopedLog.Error(err, "Unable to prepare Pod for recycling", "podName", podName) return enterprisev1.PhaseError, err } - if !complete { + if !ready { // wait until pod quarantine has completed before deleting it return enterprisev1.PhaseUpdating, nil } @@ -181,14 +181,14 @@ func ReconcileStatefulSetPods(c ControllerClient, statefulSet *appsv1.StatefulSe return enterprisev1.PhaseUpdating, nil } - // check if pod was previously quarantined; if so, it's ok to release it - released, err := mgr.ReleaseQuarantine(n) + // check if pod was previously prepared for recycling; if so, complete + complete, err := mgr.FinishRecycle(n) if err != nil { - scopedLog.Error(err, "Unable to release Pod from quarantine", "podName", podName) + scopedLog.Error(err, "Unable to complete recycling of pod", "podName", podName) return enterprisev1.PhaseError, err } - if released { - // if pod was released, return and wait until next reconcile to let things settle down + if !complete { + // return and wait until next reconcile to let things settle down return enterprisev1.PhaseUpdating, nil } } diff --git a/pkg/splunk/reconcile/statefulset_test.go b/pkg/splunk/reconcile/statefulset_test.go index a37399764..e7dfa794a 100644 --- a/pkg/splunk/reconcile/statefulset_test.go +++ b/pkg/splunk/reconcile/statefulset_test.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package deploy +package reconcile import ( "testing" diff --git a/pkg/splunk/reconcile/util.go b/pkg/splunk/reconcile/util.go index 3e8707fbb..3f6bde78e 100644 --- a/pkg/splunk/reconcile/util.go +++ b/pkg/splunk/reconcile/util.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package deploy +package reconcile import ( "context" @@ -23,13 +23,19 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "sigs.k8s.io/controller-runtime/pkg/client" + logf "sigs.k8s.io/controller-runtime/pkg/log" + //stdlog "log" + //"github.com/go-logr/stdr" "github.com/splunk/splunk-operator/pkg/splunk/resources" ) -// logger used by splunk.deploy package -var log = logf.Log.WithName("splunk.deploy") +// kubernetes logger used by splunk.reconcile package +var log = logf.Log.WithName("splunk.reconcile") + +// simple stdout logger, used for debugging +//var log = stdr.New(stdlog.New(os.Stderr, "", stdlog.LstdFlags|stdlog.Lshortfile)).WithName("splunk.reconcile") // The ResourceObject type implements methods of runtime.Object and GetObjectMeta() type ResourceObject interface { @@ -47,6 +53,7 @@ func CreateResource(client ControllerClient, obj ResourceObject) error { scopedLog := log.WithName("CreateResource").WithValues( "name", obj.GetObjectMeta().GetName(), "namespace", obj.GetObjectMeta().GetNamespace()) + err := client.Create(context.TODO(), obj) if err != nil && !errors.IsAlreadyExists(err) { diff --git a/pkg/splunk/reconcile/util_test.go b/pkg/splunk/reconcile/util_test.go index 3d38ee011..9e194de0b 100644 --- a/pkg/splunk/reconcile/util_test.go +++ b/pkg/splunk/reconcile/util_test.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package deploy +package reconcile import ( "context" @@ -258,7 +258,7 @@ func (c *mockClient) checkCalls(t *testing.T, testname string, wantCalls map[str } if notEmptyWantCalls != len(c.calls) { - t.Errorf("%s: MockClient functions called = %d; want %d", testname, len(c.calls), len(wantCalls)) + t.Errorf("%s: MockClient functions called = %d; want %d: calls=%v", testname, len(c.calls), len(wantCalls), c.calls) } } From 71d37a719cfffebf579bfdff8fb8f167d2815a1c Mon Sep 17 00:00:00 2001 From: Mike Dickey Date: Thu, 19 Mar 2020 18:11:29 -0700 Subject: [PATCH 6/7] Added many tests for new PodManager code Moved Splunk REST API client to a separate client package --- Makefile | 4 +- README.md | 2 + pkg/splunk/client/doc.go | 19 +++ .../restapi.go => client/enterprise.go} | 44 ++--- .../enterprise_test.go} | 111 ++++-------- pkg/splunk/reconcile/indexercluster.go | 70 ++++---- pkg/splunk/reconcile/indexercluster_test.go | 147 ++++++++++++++++ pkg/splunk/reconcile/licensemaster.go | 17 +- pkg/splunk/reconcile/searchheadcluster.go | 82 ++++----- .../reconcile/searchheadcluster_test.go | 158 +++++++++++++++++- pkg/splunk/reconcile/standalone.go | 17 +- pkg/splunk/reconcile/statefulset.go | 50 +++++- pkg/splunk/reconcile/statefulset_test.go | 147 +++++++++++++++- pkg/splunk/reconcile/util_test.go | 22 ++- pkg/splunk/test/client.go | 95 +++++++++++ pkg/splunk/test/doc.go | 18 ++ 16 files changed, 791 insertions(+), 212 deletions(-) create mode 100644 pkg/splunk/client/doc.go rename pkg/splunk/{enterprise/restapi.go => client/enterprise.go} (95%) rename pkg/splunk/{enterprise/restapi_test.go => client/enterprise_test.go} (93%) create mode 100644 pkg/splunk/test/client.go create mode 100644 pkg/splunk/test/doc.go diff --git a/Makefile b/Makefile index 1c2c2f911..bfa8158b1 100644 --- a/Makefile +++ b/Makefile @@ -28,7 +28,7 @@ builder-image: builder-test: @echo Running unit tests for splunk-operator inside of builder container - @docker run -v /var/run/docker.sock:/var/run/docker.sock -v ${PWD}:/opt/app-root/src/splunk-operator -w /opt/app-root/src/splunk-operator -u root -it splunk/splunk-operator-builder bash -c "go test -v -covermode=count -coverprofile=coverage.out --timeout=300s github.com/splunk/splunk-operator/pkg/splunk/resources github.com/splunk/splunk-operator/pkg/splunk/spark github.com/splunk/splunk-operator/pkg/splunk/enterprise github.com/splunk/splunk-operator/pkg/splunk/reconcile" + @docker run -v /var/run/docker.sock:/var/run/docker.sock -v ${PWD}:/opt/app-root/src/splunk-operator -w /opt/app-root/src/splunk-operator -u root -it splunk/splunk-operator-builder bash -c "go test -v -covermode=count -coverprofile=coverage.out --timeout=300s github.com/splunk/splunk-operator/pkg/splunk/resources github.com/splunk/splunk-operator/pkg/splunk/spark github.com/splunk/splunk-operator/pkg/splunk/enterprise github.com/splunk/splunk-operator/pkg/splunk/reconcile github.com/splunk/splunk-operator/pkg/splunk/client" image: @echo Building splunk-operator image @@ -41,7 +41,7 @@ local: test: @echo Running unit tests for splunk-operator - @go test -v -covermode=count -coverprofile=coverage.out --timeout=300s github.com/splunk/splunk-operator/pkg/splunk/resources github.com/splunk/splunk-operator/pkg/splunk/spark github.com/splunk/splunk-operator/pkg/splunk/enterprise github.com/splunk/splunk-operator/pkg/splunk/reconcile + @go test -v -covermode=count -coverprofile=coverage.out --timeout=300s github.com/splunk/splunk-operator/pkg/splunk/resources github.com/splunk/splunk-operator/pkg/splunk/spark github.com/splunk/splunk-operator/pkg/splunk/enterprise github.com/splunk/splunk-operator/pkg/splunk/reconcile github.com/splunk/splunk-operator/pkg/splunk/client stop_clair_scanner: @docker stop clair_db || true diff --git a/README.md b/README.md index 787e670fc..a3380384f 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,8 @@ This repository consists of the following code used to build the splunk-operator * `pkg/splunk/enterprise/`: Source code for managing Splunk Enterprise deployments * `pkg/splunk/spark/`: Source code for managing Spark cluster deployments * `pkg/splunk/resources/`: Generic utility code used by other splunk modules +* `pkg/splunk/client/`: Simple client for Splunk Enterprise REST API +* `pkg/splunk/test/`: Common code used for testing other modules `main()` basically just instantiates the `controllers`, and the `controllers` call into the `reconcile` module to perform actions. The `reconcile` module uses the `enterprise` diff --git a/pkg/splunk/client/doc.go b/pkg/splunk/client/doc.go new file mode 100644 index 000000000..2ae81ce56 --- /dev/null +++ b/pkg/splunk/client/doc.go @@ -0,0 +1,19 @@ +// 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 client provides a simple client for the Splunk Enterprise REST API. +This package has no depedencies outside of the standard go library. +*/ +package client diff --git a/pkg/splunk/enterprise/restapi.go b/pkg/splunk/client/enterprise.go similarity index 95% rename from pkg/splunk/enterprise/restapi.go rename to pkg/splunk/client/enterprise.go index c1ce40f60..f00a77f97 100644 --- a/pkg/splunk/enterprise/restapi.go +++ b/pkg/splunk/client/enterprise.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package enterprise +package client import ( "crypto/tls" @@ -33,25 +33,25 @@ type SplunkHTTPClient interface { // SplunkClient is a simple object used to send HTTP REST API requests type SplunkClient struct { // https endpoint for management interface (e.g. "https://server:8089") - managementURI string + ManagementURI string // username for authentication - username string + Username string // password for authentication - password string + Password string // HTTP client used to process requests - client SplunkHTTPClient + Client SplunkHTTPClient } // NewSplunkClient returns a new SplunkClient object initialized with a username and password. func NewSplunkClient(managementURI, username, password string) *SplunkClient { return &SplunkClient{ - managementURI: managementURI, - username: username, - password: password, - client: &http.Client{ + ManagementURI: managementURI, + Username: username, + Password: password, + Client: &http.Client{ Timeout: 5 * time.Second, Transport: &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, // don't verify ssl certs @@ -63,8 +63,8 @@ func NewSplunkClient(managementURI, username, password string) *SplunkClient { // Do processes a Splunk REST API request and unmarshals response into obj, if not nil. func (c *SplunkClient) Do(request *http.Request, expectedStatus int, obj interface{}) error { // send HTTP response and check status - request.SetBasicAuth(c.username, c.password) - response, err := c.client.Do(request) + request.SetBasicAuth(c.Username, c.Password) + response, err := c.Client.Do(request) if err != nil { return err } @@ -85,7 +85,7 @@ func (c *SplunkClient) Do(request *http.Request, expectedStatus int, obj interfa // Get sends a REST API request and unmarshals response into obj, if not nil. func (c *SplunkClient) Get(path string, obj interface{}) error { - endpoint := fmt.Sprintf("%s%s?count=0&output_mode=json", c.managementURI, path) + endpoint := fmt.Sprintf("%s%s?count=0&output_mode=json", c.ManagementURI, path) request, err := http.NewRequest("GET", endpoint, nil) if err != nil { return err @@ -142,7 +142,7 @@ func (c *SplunkClient) GetSearchHeadCaptainInfo() (*SearchHeadCaptainInfo, error return nil, err } if len(apiResponse.Entry) < 1 { - return nil, fmt.Errorf("Invalid response from %s%s", c.managementURI, path) + return nil, fmt.Errorf("Invalid response from %s%s", c.ManagementURI, path) } return &apiResponse.Entry[0].Content, nil } @@ -268,7 +268,7 @@ func (c *SplunkClient) GetSearchHeadClusterMemberInfo() (*SearchHeadClusterMembe return nil, err } if len(apiResponse.Entry) < 1 { - return nil, fmt.Errorf("Invalid response from %s%s", c.managementURI, path) + return nil, fmt.Errorf("Invalid response from %s%s", c.ManagementURI, path) } return &apiResponse.Entry[0].Content, nil } @@ -281,7 +281,7 @@ func (c *SplunkClient) SetSearchHeadDetention(detain bool) error { if detain { mode = "on" } - endpoint := fmt.Sprintf("%s/services/shcluster/member/control/control/set_manual_detention?manual_detention=%s", c.managementURI, mode) + endpoint := fmt.Sprintf("%s/services/shcluster/member/control/control/set_manual_detention?manual_detention=%s", c.ManagementURI, mode) request, err := http.NewRequest("POST", endpoint, nil) if err != nil { return err @@ -294,15 +294,15 @@ func (c *SplunkClient) SetSearchHeadDetention(detain bool) error { // See https://docs.splunk.com/Documentation/Splunk/latest/DistSearch/Removeaclustermember func (c *SplunkClient) RemoveSearchHeadClusterMember() error { // sent request to remove from search head cluster consensus - endpoint := fmt.Sprintf("%s/services/shcluster/member/consensus/default/remove_server?output_mode=json", c.managementURI) + endpoint := fmt.Sprintf("%s/services/shcluster/member/consensus/default/remove_server?output_mode=json", c.ManagementURI) request, err := http.NewRequest("POST", endpoint, nil) if err != nil { return err } // send HTTP response and check status - request.SetBasicAuth(c.username, c.password) - response, err := c.client.Do(request) + request.SetBasicAuth(c.Username, c.Password) + response, err := c.Client.Do(request) if err != nil { return err } @@ -401,7 +401,7 @@ func (c *SplunkClient) GetClusterMasterInfo() (*ClusterMasterInfo, error) { return nil, err } if len(apiResponse.Entry) < 1 { - return nil, fmt.Errorf("Invalid response from %s%s", c.managementURI, path) + return nil, fmt.Errorf("Invalid response from %s%s", c.ManagementURI, path) } return &apiResponse.Entry[0].Content, nil } @@ -448,7 +448,7 @@ func (c *SplunkClient) GetIndexerClusterPeerInfo() (*IndexerClusterPeerInfo, err return nil, err } if len(apiResponse.Entry) < 1 { - return nil, fmt.Errorf("Invalid response from %s%s", c.managementURI, path) + return nil, fmt.Errorf("Invalid response from %s%s", c.ManagementURI, path) } return &apiResponse.Entry[0].Content, nil } @@ -585,7 +585,7 @@ func (c *SplunkClient) GetClusterMasterPeers() (map[string]ClusterMasterPeerInfo // See https://docs.splunk.com/Documentation/Splunk/8.0.2/Indexer/Removepeerfrommasterlist func (c *SplunkClient) RemoveIndexerClusterPeer(id string) error { // sent request to remove from search head cluster consensus - endpoint := fmt.Sprintf("%s/services/cluster/master/control/control/remove_peers?peers=%s", c.managementURI, id) + endpoint := fmt.Sprintf("%s/services/cluster/master/control/control/remove_peers?peers=%s", c.ManagementURI, id) request, err := http.NewRequest("POST", endpoint, nil) if err != nil { return err @@ -601,7 +601,7 @@ func (c *SplunkClient) DecommissionIndexerClusterPeer(enforceCounts bool) error if enforceCounts { enforceCountsAsInt = 1 } - endpoint := fmt.Sprintf("%s/services/cluster/slave/control/control/decommission?enforce_counts=%d", c.managementURI, enforceCountsAsInt) + endpoint := fmt.Sprintf("%s/services/cluster/slave/control/control/decommission?enforce_counts=%d", c.ManagementURI, enforceCountsAsInt) request, err := http.NewRequest("POST", endpoint, nil) if err != nil { return err diff --git a/pkg/splunk/enterprise/restapi_test.go b/pkg/splunk/client/enterprise_test.go similarity index 93% rename from pkg/splunk/enterprise/restapi_test.go rename to pkg/splunk/client/enterprise_test.go index fd2dde457..2aacad891 100644 --- a/pkg/splunk/enterprise/restapi_test.go +++ b/pkg/splunk/client/enterprise_test.go @@ -12,57 +12,29 @@ // See the License for the specific language governing permissions and // limitations under the License. -package enterprise +package client import ( - "io/ioutil" "net/http" - "reflect" - "strings" "testing" -) - -// MockHTTPClient is used to replicate an http.Client -type MockHTTPClient struct { - Requests []http.Request - Response http.Response - Err error -} -// Do method for MockHTTPClient is called by all methods of SplunkClient -func (c *MockHTTPClient) Do(req *http.Request) (*http.Response, error) { - c.Requests = append(c.Requests, *req) - return &c.Response, c.Err -} + spltest "github.com/splunk/splunk-operator/pkg/splunk/test" +) -func splunkClientTester(t *testing.T, testMethod string, status int, body string, wantRequests []http.Request, test func(SplunkClient) error) { - mockClient := MockHTTPClient{ - Err: nil, - Response: http.Response{ - StatusCode: status, - Body: ioutil.NopCloser(strings.NewReader(body)), - }, - } +func splunkClientTester(t *testing.T, testMethod string, status int, body string, wantRequest *http.Request, test func(SplunkClient) error) { + mockSplunkClient := &spltest.MockHTTPClient{} + mockSplunkClient.AddHandler(wantRequest, status, body, nil) c := NewSplunkClient("https://localhost:8089", "admin", "p@ssw0rd") - c.client = &mockClient + c.Client = mockSplunkClient err := test(*c) if err != nil { t.Errorf("%s err = %v", testMethod, err) } - if len(mockClient.Requests) != len(wantRequests) { - t.Fatalf("%s got %d Requests; want %d", testMethod, len(mockClient.Requests), len(wantRequests)) - } - for n := range mockClient.Requests { - wantRequests[n].SetBasicAuth(c.username, c.password) - if !reflect.DeepEqual(mockClient.Requests[n], wantRequests[n]) { - t.Errorf("%s Requests[%d]=%v; want %v", testMethod, n, mockClient.Requests[n], wantRequests[n]) - } - } + mockSplunkClient.CheckRequests(t, testMethod) } func TestGetSearchHeadCaptainInfo(t *testing.T) { wantRequest, _ := http.NewRequest("GET", "https://localhost:8089/services/shcluster/captain/info?count=0&output_mode=json", nil) - want := []http.Request{*wantRequest} wantCaptainLabel := "splunk-s2-search-head-0" test := func(c SplunkClient) error { captainInfo, err := c.GetSearchHeadCaptainInfo() @@ -75,7 +47,7 @@ func TestGetSearchHeadCaptainInfo(t *testing.T) { return nil } body := `{"links":{},"origin":"https://localhost:8089/services/shcluster/captain/info","updated":"2020-03-15T16:36:42+00:00","generator":{"build":"a7f645ddaf91","version":"8.0.2"},"entry":[{"name":"captain","id":"https://localhost:8089/services/shcluster/captain/info/captain","updated":"1970-01-01T00:00:00+00:00","links":{"alternate":"/services/shcluster/captain/info/captain","list":"/services/shcluster/captain/info/captain"},"author":"system","acl":{"app":"","can_list":true,"can_write":true,"modifiable":false,"owner":"system","perms":{"read":["admin","splunk-system-role"],"write":["admin","splunk-system-role"]},"removable":false,"sharing":"system"},"content":{"eai:acl":null,"elected_captain":1584139352,"id":"A9D5FCCF-EB93-4E0A-93E1-45B56483EA7A","initialized_flag":true,"label":"splunk-s2-search-head-0","maintenance_mode":false,"mgmt_uri":"https://splunk-s2-search-head-0.splunk-s2-search-head-headless.splunk.svc.cluster.local:8089","min_peers_joined_flag":true,"peer_scheme_host_port":"https://splunk-s2-search-head-0.splunk-s2-search-head-headless.splunk.svc.cluster.local:8089","rolling_restart_flag":false,"service_ready_flag":true,"start_time":1584139291}}],"paging":{"total":1,"perPage":30,"offset":0},"messages":[]}` - splunkClientTester(t, "TestGetSearchHeadCaptainInfo", 200, body, want, test) + splunkClientTester(t, "TestGetSearchHeadCaptainInfo", 200, body, wantRequest, test) // test body with no entries test = func(c SplunkClient) error { @@ -86,18 +58,17 @@ func TestGetSearchHeadCaptainInfo(t *testing.T) { return nil } body = `{"links":{},"origin":"https://localhost:8089/services/shcluster/captain/info","updated":"2020-03-15T16:36:42+00:00","generator":{"build":"a7f645ddaf91","version":"8.0.2"},"entry":[]}` - splunkClientTester(t, "TestGetSearchHeadCaptainInfo", 200, body, want, test) + splunkClientTester(t, "TestGetSearchHeadCaptainInfo", 200, body, wantRequest, test) // test empty body - splunkClientTester(t, "TestGetSearchHeadCaptainInfo", 200, "", want, test) + splunkClientTester(t, "TestGetSearchHeadCaptainInfo", 200, "", wantRequest, test) // test error code - splunkClientTester(t, "TestGetSearchHeadCaptainInfo", 500, "", want, test) + splunkClientTester(t, "TestGetSearchHeadCaptainInfo", 500, "", wantRequest, test) } func TestGetSearchHeadClusterMemberInfo(t *testing.T) { wantRequest, _ := http.NewRequest("GET", "https://localhost:8089/services/shcluster/member/info?count=0&output_mode=json", nil) - want := []http.Request{*wantRequest} wantMemberStatus := "Up" test := func(c SplunkClient) error { memberInfo, err := c.GetSearchHeadClusterMemberInfo() @@ -110,7 +81,7 @@ func TestGetSearchHeadClusterMemberInfo(t *testing.T) { return nil } body := `{"links":{},"origin":"https://localhost:8089/services/shcluster/member/info","updated":"2020-03-15T16:30:38+00:00","generator":{"build":"a7f645ddaf91","version":"8.0.2"},"entry":[{"name":"member","id":"https://localhost:8089/services/shcluster/member/info/member","updated":"1970-01-01T00:00:00+00:00","links":{"alternate":"/services/shcluster/member/info/member","list":"/services/shcluster/member/info/member"},"author":"system","acl":{"app":"","can_list":true,"can_write":true,"modifiable":false,"owner":"system","perms":{"read":["admin","splunk-system-role"],"write":["admin","splunk-system-role"]},"removable":false,"sharing":"system"},"content":{"active_historical_search_count":0,"active_realtime_search_count":0,"adhoc_searchhead":false,"eai:acl":null,"is_registered":true,"last_heartbeat_attempt":1584289836,"maintenance_mode":false,"no_artifact_replications":false,"peer_load_stats_gla_15m":0,"peer_load_stats_gla_1m":0,"peer_load_stats_gla_5m":0,"peer_load_stats_max_runtime":0,"peer_load_stats_num_autosummary":0,"peer_load_stats_num_historical":0,"peer_load_stats_num_realtime":0,"peer_load_stats_num_running":0,"peer_load_stats_total_runtime":0,"restart_state":"NoRestart","status":"Up"}}],"paging":{"total":1,"perPage":30,"offset":0},"messages":[]}` - splunkClientTester(t, "TestGetSearchHeadClusterMemberInfo", 200, body, want, test) + splunkClientTester(t, "TestGetSearchHeadClusterMemberInfo", 200, body, wantRequest, test) // test body with no entries test = func(c SplunkClient) error { @@ -121,18 +92,17 @@ func TestGetSearchHeadClusterMemberInfo(t *testing.T) { return nil } body = `{"links":{},"origin":"https://localhost:8089/services/shcluster/captain/info","updated":"2020-03-15T16:36:42+00:00","generator":{"build":"a7f645ddaf91","version":"8.0.2"},"entry":[]}` - splunkClientTester(t, "TestGetSearchHeadCaptainInfo", 200, body, want, test) + splunkClientTester(t, "TestGetSearchHeadCaptainInfo", 200, body, wantRequest, test) // test empty body - splunkClientTester(t, "TestGetSearchHeadClusterMemberInfo", 200, "", want, test) + splunkClientTester(t, "TestGetSearchHeadClusterMemberInfo", 200, "", wantRequest, test) // test error code - splunkClientTester(t, "TestGetSearchHeadClusterMemberInfo", 500, "", want, test) + splunkClientTester(t, "TestGetSearchHeadClusterMemberInfo", 500, "", wantRequest, test) } func TestGetSearchHeadCaptainMembers(t *testing.T) { wantRequest, _ := http.NewRequest("GET", "https://localhost:8089/services/shcluster/captain/members?count=0&output_mode=json", nil) - want := []http.Request{*wantRequest} wantMembers := []string{ "splunk-s2-search-head-0", "splunk-s2-search-head-1", "splunk-s2-search-head-2", "splunk-s2-search-head-3", "splunk-s2-search-head-4", } @@ -167,7 +137,7 @@ func TestGetSearchHeadCaptainMembers(t *testing.T) { return nil } body := `{"links":{"create":"/services/shcluster/captain/members/_new"},"origin":"https://localhost:8089/services/shcluster/captain/members","updated":"2020-03-15T16:40:20+00:00","generator":{"build":"a7f645ddaf91","version":"8.0.2"},"entry":[{"name":"7D571849-CD52-48F4-B76A-E83C4E86E300","id":"https://localhost:8089/services/shcluster/captain/members/7D571849-CD52-48F4-B76A-E83C4E86E300","updated":"1970-01-01T00:00:00+00:00","links":{"alternate":"/services/shcluster/captain/members/7D571849-CD52-48F4-B76A-E83C4E86E300","list":"/services/shcluster/captain/members/7D571849-CD52-48F4-B76A-E83C4E86E300","edit":"/services/shcluster/captain/members/7D571849-CD52-48F4-B76A-E83C4E86E300"},"author":"system","acl":{"app":"","can_list":true,"can_write":true,"modifiable":false,"owner":"system","perms":{"read":["admin","splunk-system-role"],"write":["admin","splunk-system-role"]},"removable":false,"sharing":"system"},"content":{"adhoc_searchhead":false,"advertise_restart_required":false,"artifact_count":2,"delayed_artifacts_to_discard":[],"eai:acl":null,"fixup_set":[],"host_port_pair":"10.42.0.3:8089","is_captain":false,"kv_store_host_port":"splunk-s2-search-head-2.splunk-s2-search-head-headless.splunk.svc.cluster.local:8191","label":"splunk-s2-search-head-2","last_heartbeat":1584290418,"mgmt_uri":"https://splunk-s2-search-head-2.splunk-s2-search-head-headless.splunk.svc.cluster.local:8089","no_artifact_replications":false,"peer_scheme_host_port":"https://10.42.0.3:8089","pending_job_count":0,"preferred_captain":false,"replication_count":0,"replication_port":9887,"replication_use_ssl":false,"site":"default","status":"Up","status_counter":{"Complete":2,"NonStreamingTarget":0,"PendingDiscard":0}}},{"name":"90D7E074-9880-4867-BAA1-31A74EC28DC0","id":"https://localhost:8089/services/shcluster/captain/members/90D7E074-9880-4867-BAA1-31A74EC28DC0","updated":"1970-01-01T00:00:00+00:00","links":{"alternate":"/services/shcluster/captain/members/90D7E074-9880-4867-BAA1-31A74EC28DC0","list":"/services/shcluster/captain/members/90D7E074-9880-4867-BAA1-31A74EC28DC0","edit":"/services/shcluster/captain/members/90D7E074-9880-4867-BAA1-31A74EC28DC0"},"author":"system","acl":{"app":"","can_list":true,"can_write":true,"modifiable":false,"owner":"system","perms":{"read":["admin","splunk-system-role"],"write":["admin","splunk-system-role"]},"removable":false,"sharing":"system"},"content":{"adhoc_searchhead":false,"advertise_restart_required":false,"artifact_count":0,"delayed_artifacts_to_discard":[],"eai:acl":null,"fixup_set":[],"host_port_pair":"10.42.0.2:8089","is_captain":true,"kv_store_host_port":"splunk-s2-search-head-0.splunk-s2-search-head-headless.splunk.svc.cluster.local:8191","label":"splunk-s2-search-head-0","last_heartbeat":1584290416,"mgmt_uri":"https://splunk-s2-search-head-0.splunk-s2-search-head-headless.splunk.svc.cluster.local:8089","no_artifact_replications":false,"peer_scheme_host_port":"https://10.42.0.2:8089","pending_job_count":0,"preferred_captain":true,"replication_count":0,"replication_port":9887,"replication_use_ssl":false,"site":"default","status":"Up","status_counter":{"Complete":0,"NonStreamingTarget":0,"PendingDiscard":0}}},{"name":"97B56FAE-E9C9-4B12-8B1E-A428E7859417","id":"https://localhost:8089/services/shcluster/captain/members/97B56FAE-E9C9-4B12-8B1E-A428E7859417","updated":"1970-01-01T00:00:00+00:00","links":{"alternate":"/services/shcluster/captain/members/97B56FAE-E9C9-4B12-8B1E-A428E7859417","list":"/services/shcluster/captain/members/97B56FAE-E9C9-4B12-8B1E-A428E7859417","edit":"/services/shcluster/captain/members/97B56FAE-E9C9-4B12-8B1E-A428E7859417"},"author":"system","acl":{"app":"","can_list":true,"can_write":true,"modifiable":false,"owner":"system","perms":{"read":["admin","splunk-system-role"],"write":["admin","splunk-system-role"]},"removable":false,"sharing":"system"},"content":{"adhoc_searchhead":false,"advertise_restart_required":false,"artifact_count":1,"delayed_artifacts_to_discard":[],"eai:acl":null,"fixup_set":[],"host_port_pair":"10.36.0.7:8089","is_captain":false,"kv_store_host_port":"splunk-s2-search-head-1.splunk-s2-search-head-headless.splunk.svc.cluster.local:8191","label":"splunk-s2-search-head-1","last_heartbeat":1584290418,"mgmt_uri":"https://splunk-s2-search-head-1.splunk-s2-search-head-headless.splunk.svc.cluster.local:8089","no_artifact_replications":false,"peer_scheme_host_port":"https://10.36.0.7:8089","pending_job_count":0,"preferred_captain":false,"replication_count":0,"replication_port":9887,"replication_use_ssl":false,"site":"default","status":"Up","status_counter":{"Complete":1,"NonStreamingTarget":0,"PendingDiscard":0}}},{"name":"AA55C39A-5A3A-47CC-BF2C-2B60F0F6C561","id":"https://localhost:8089/services/shcluster/captain/members/AA55C39A-5A3A-47CC-BF2C-2B60F0F6C561","updated":"1970-01-01T00:00:00+00:00","links":{"alternate":"/services/shcluster/captain/members/AA55C39A-5A3A-47CC-BF2C-2B60F0F6C561","list":"/services/shcluster/captain/members/AA55C39A-5A3A-47CC-BF2C-2B60F0F6C561","edit":"/services/shcluster/captain/members/AA55C39A-5A3A-47CC-BF2C-2B60F0F6C561"},"author":"system","acl":{"app":"","can_list":true,"can_write":true,"modifiable":false,"owner":"system","perms":{"read":["admin","splunk-system-role"],"write":["admin","splunk-system-role"]},"removable":false,"sharing":"system"},"content":{"adhoc_searchhead":false,"advertise_restart_required":false,"artifact_count":1,"delayed_artifacts_to_discard":[],"eai:acl":null,"fixup_set":[],"host_port_pair":"10.42.0.5:8089","is_captain":false,"kv_store_host_port":"splunk-s2-search-head-4.splunk-s2-search-head-headless.splunk.svc.cluster.local:8191","label":"splunk-s2-search-head-4","last_heartbeat":1584290417,"mgmt_uri":"https://splunk-s2-search-head-4.splunk-s2-search-head-headless.splunk.svc.cluster.local:8089","no_artifact_replications":false,"peer_scheme_host_port":"https://10.42.0.5:8089","pending_job_count":0,"preferred_captain":false,"replication_count":0,"replication_port":9887,"replication_use_ssl":false,"site":"default","status":"Up","status_counter":{"Complete":1,"NonStreamingTarget":0,"PendingDiscard":0}}},{"name":"E271B238-921F-4F6E-BD99-E110EB7B0FDA","id":"https://localhost:8089/services/shcluster/captain/members/E271B238-921F-4F6E-BD99-E110EB7B0FDA","updated":"1970-01-01T00:00:00+00:00","links":{"alternate":"/services/shcluster/captain/members/E271B238-921F-4F6E-BD99-E110EB7B0FDA","list":"/services/shcluster/captain/members/E271B238-921F-4F6E-BD99-E110EB7B0FDA","edit":"/services/shcluster/captain/members/E271B238-921F-4F6E-BD99-E110EB7B0FDA"},"author":"system","acl":{"app":"","can_list":true,"can_write":true,"modifiable":false,"owner":"system","perms":{"read":["admin","splunk-system-role"],"write":["admin","splunk-system-role"]},"removable":false,"sharing":"system"},"content":{"adhoc_searchhead":false,"advertise_restart_required":false,"artifact_count":2,"delayed_artifacts_to_discard":[],"eai:acl":null,"fixup_set":[],"host_port_pair":"10.40.0.4:8089","is_captain":false,"kv_store_host_port":"splunk-s2-search-head-3.splunk-s2-search-head-headless.splunk.svc.cluster.local:8191","label":"splunk-s2-search-head-3","last_heartbeat":1584290420,"mgmt_uri":"https://splunk-s2-search-head-3.splunk-s2-search-head-headless.splunk.svc.cluster.local:8089","no_artifact_replications":false,"peer_scheme_host_port":"https://10.40.0.4:8089","pending_job_count":0,"preferred_captain":false,"replication_count":0,"replication_port":9887,"replication_use_ssl":false,"site":"default","status":"Up","status_counter":{"Complete":2,"NonStreamingTarget":0,"PendingDiscard":0}}}],"paging":{"total":5,"perPage":30,"offset":0},"messages":[]}` - splunkClientTester(t, "TestGetSearchHeadCaptainMembers", 200, body, want, test) + splunkClientTester(t, "TestGetSearchHeadCaptainMembers", 200, body, wantRequest, test) // test error response test = func(c SplunkClient) error { @@ -177,34 +147,32 @@ func TestGetSearchHeadCaptainMembers(t *testing.T) { } return nil } - splunkClientTester(t, "TestGetSearchHeadCaptainMembers", 503, "", want, test) + splunkClientTester(t, "TestGetSearchHeadCaptainMembers", 503, "", wantRequest, test) } func TestSetSearchHeadDetention(t *testing.T) { wantRequest, _ := http.NewRequest("POST", "https://localhost:8089/services/shcluster/member/control/control/set_manual_detention?manual_detention=on", nil) - want := []http.Request{*wantRequest} test := func(c SplunkClient) error { return c.SetSearchHeadDetention(true) } - splunkClientTester(t, "TestSetSearchHeadDetention", 200, "", want, test) + splunkClientTester(t, "TestSetSearchHeadDetention", 200, "", wantRequest, test) } func TestRemoveSearchHeadClusterMember(t *testing.T) { // test for 200 response first (sent on first removal request) wantRequest, _ := http.NewRequest("POST", "https://localhost:8089/services/shcluster/member/consensus/default/remove_server?output_mode=json", nil) - want := []http.Request{*wantRequest} test := func(c SplunkClient) error { return c.RemoveSearchHeadClusterMember() } - splunkClientTester(t, "TestRemoveSearchHeadClusterMember", 200, "", want, test) + splunkClientTester(t, "TestRemoveSearchHeadClusterMember", 200, "", wantRequest, test) // next test 503 error message (sent for short period after removal, while SH is updating itself) body := `{"messages":[{"type":"ERROR","text":"Failed to proxy call to member https://splunk-s2-search-head-0.splunk-s2-search-head-headless.splunk.svc.cluster.local:8089. ERROR: Server https://splunk-s2-search-head-3.splunk-s2-search-head-headless.splunk.svc.cluster.local:8089 is not part of configuration, hence cannot be removed. Check configuration by making GET request onto /services/shcluster/member/consensus"}]}` - splunkClientTester(t, "TestRemoveSearchHeadClusterMember", 503, body, want, test) + splunkClientTester(t, "TestRemoveSearchHeadClusterMember", 503, body, wantRequest, test) // check alternate 503 message (sent after SH has completed removal) body = `{"messages":[{"type":"ERROR","text":"This node is not part of any cluster configuration, please re-run the command from an active cluster member. Also see \"splunk add shcluster-member\" to add this member to an existing cluster or see \"splunk bootstrap shcluster-captain\" to bootstrap a new cluster with this member."}]}` - splunkClientTester(t, "TestRemoveSearchHeadClusterMember", 503, body, want, test) + splunkClientTester(t, "TestRemoveSearchHeadClusterMember", 503, body, wantRequest, test) // test unrecognized response message test = func(c SplunkClient) error { @@ -215,26 +183,25 @@ func TestRemoveSearchHeadClusterMember(t *testing.T) { return nil } body = `{"messages":[{"type":"ERROR","text":"Nothing that we are expecting."}]}` - splunkClientTester(t, "TestRemoveSearchHeadClusterMember", 503, body, want, test) + splunkClientTester(t, "TestRemoveSearchHeadClusterMember", 503, body, wantRequest, test) // test empty messages array in response body = `{"messages":[]}` - splunkClientTester(t, "TestRemoveSearchHeadClusterMember", 503, body, want, test) + splunkClientTester(t, "TestRemoveSearchHeadClusterMember", 503, body, wantRequest, test) // test unmarshal failure body = `` - splunkClientTester(t, "TestRemoveSearchHeadClusterMember", 503, body, want, test) + splunkClientTester(t, "TestRemoveSearchHeadClusterMember", 503, body, wantRequest, test) // test empty response - splunkClientTester(t, "TestRemoveSearchHeadClusterMember", 503, "", want, test) + splunkClientTester(t, "TestRemoveSearchHeadClusterMember", 503, "", wantRequest, test) // test bad response code - splunkClientTester(t, "TestRemoveSearchHeadClusterMember", 404, "", want, test) + splunkClientTester(t, "TestRemoveSearchHeadClusterMember", 404, "", wantRequest, test) } func TestGetClusterMasterInfo(t *testing.T) { wantRequest, _ := http.NewRequest("GET", "https://localhost:8089/services/cluster/master/info?count=0&output_mode=json", nil) - want := []http.Request{*wantRequest} wantInfo := ClusterMasterInfo{ Initialized: true, IndexingReady: true, @@ -265,7 +232,7 @@ func TestGetClusterMasterInfo(t *testing.T) { return nil } body := `{"links":{},"origin":"https://localhost:8089/services/cluster/master/info","updated":"2020-03-18T01:04:53+00:00","generator":{"build":"a7f645ddaf91","version":"8.0.2"},"entry":[{"name":"master","id":"https://localhost:8089/services/cluster/master/info/master","updated":"1970-01-01T00:00:00+00:00","links":{"alternate":"/services/cluster/master/info/master","list":"/services/cluster/master/info/master"},"author":"system","acl":{"app":"","can_list":true,"can_write":true,"modifiable":false,"owner":"system","perms":{"read":["admin","splunk-system-role"],"write":["admin","splunk-system-role"]},"removable":false,"sharing":"system"},"content":{"active_bundle":{"bundle_path":"/opt/splunk/var/run/splunk/cluster/remote-bundle/506c58d5aeda1dd6017889e3186e7337-1583870198.bundle","checksum":"14310A4AABD23E85BBD4559C4A3B59F8","timestamp":1583870198},"apply_bundle_status":{"invalid_bundle":{"bundle_path":"","bundle_validation_errors_on_master":[],"checksum":"","timestamp":0},"reload_bundle_issued":false,"status":"None"},"backup_and_restore_primaries":false,"controlled_rolling_restart_flag":false,"eai:acl":null,"indexing_ready_flag":true,"initialized_flag":true,"label":"splunk-s1-cluster-master-0","last_check_restart_bundle_result":false,"last_dry_run_bundle":{"bundle_path":"","checksum":"","timestamp":0},"last_validated_bundle":{"bundle_path":"/opt/splunk/var/run/splunk/cluster/remote-bundle/0af7c0e95f313f7be3b0cb1d878df9a1-1583948640.bundle","checksum":"14310A4AABD23E85BBD4559C4A3B59F8","is_valid_bundle":true,"timestamp":1583948640},"latest_bundle":{"bundle_path":"/opt/splunk/var/run/splunk/cluster/remote-bundle/506c58d5aeda1dd6017889e3186e7337-1583870198.bundle","checksum":"14310A4AABD23E85BBD4559C4A3B59F8","timestamp":1583870198},"maintenance_mode":false,"multisite":false,"previous_active_bundle":{"bundle_path":"","checksum":"","timestamp":0},"primaries_backup_status":"No on-going (or) completed primaries backup yet. Check back again in few minutes if you expect a backup.","quiet_period_flag":false,"rolling_restart_flag":false,"rolling_restart_or_upgrade":false,"service_ready_flag":true,"start_time":1583948636,"summary_replication":"false"}}],"paging":{"total":1,"perPage":30,"offset":0},"messages":[]}` - splunkClientTester(t, "TestGetClusterMasterInfo", 200, body, want, test) + splunkClientTester(t, "TestGetClusterMasterInfo", 200, body, wantRequest, test) // test body with no entries test = func(c SplunkClient) error { @@ -276,15 +243,14 @@ func TestGetClusterMasterInfo(t *testing.T) { return nil } body = `{"links":{},"origin":"https://localhost:8089/services/cluster/master/info","updated":"2020-03-18T01:04:53+00:00","generator":{"build":"a7f645ddaf91","version":"8.0.2"},"entry":[],"paging":{"total":1,"perPage":30,"offset":0},"messages":[]}` - splunkClientTester(t, "TestGetClusterMasterInfo", 200, body, want, test) + splunkClientTester(t, "TestGetClusterMasterInfo", 200, body, wantRequest, test) // test error code - splunkClientTester(t, "TestGetClusterMasterInfo", 500, "", want, test) + splunkClientTester(t, "TestGetClusterMasterInfo", 500, "", wantRequest, test) } func TestGetIndexerClusterPeerInfo(t *testing.T) { wantRequest, _ := http.NewRequest("GET", "https://localhost:8089/services/cluster/slave/info?count=0&output_mode=json", nil) - want := []http.Request{*wantRequest} wantMemberStatus := "Up" test := func(c SplunkClient) error { info, err := c.GetIndexerClusterPeerInfo() @@ -297,7 +263,7 @@ func TestGetIndexerClusterPeerInfo(t *testing.T) { return nil } body := `{"links":{},"origin":"https://localhost:8089/services/cluster/slave/info","updated":"2020-03-18T01:28:18+00:00","generator":{"build":"a7f645ddaf91","version":"8.0.2"},"entry":[{"name":"slave","id":"https://localhost:8089/services/cluster/slave/info/slave","updated":"1970-01-01T00:00:00+00:00","links":{"alternate":"/services/cluster/slave/info/slave","list":"/services/cluster/slave/info/slave"},"author":"system","acl":{"app":"","can_list":true,"can_write":true,"modifiable":false,"owner":"system","perms":{"read":["admin","splunk-system-role"],"write":["admin","splunk-system-role"]},"removable":false,"sharing":"system"},"content":{"active_bundle":{"bundle_path":"/opt/splunk/var/run/splunk/cluster/remote-bundle/87c8c24e7fabc3ff9683c26652cb5890-1583870244.bundle","checksum":"14310A4AABD23E85BBD4559C4A3B59F8","timestamp":1583870244},"base_generation_id":26,"eai:acl":null,"is_registered":true,"last_dry_run_bundle":{"bundle_path":"","checksum":"","timestamp":0},"last_heartbeat_attempt":0,"latest_bundle":{"bundle_path":"/opt/splunk/var/run/splunk/cluster/remote-bundle/87c8c24e7fabc3ff9683c26652cb5890-1583870244.bundle","checksum":"14310A4AABD23E85BBD4559C4A3B59F8","timestamp":1583870244},"maintenance_mode":false,"registered_summary_state":3,"restart_state":"NoRestart","site":"default","status":"Up"}}],"paging":{"total":1,"perPage":30,"offset":0},"messages":[]}` - splunkClientTester(t, "TestGetIndexerClusterPeerInfo", 200, body, want, test) + splunkClientTester(t, "TestGetIndexerClusterPeerInfo", 200, body, wantRequest, test) // test body with no entries test = func(c SplunkClient) error { @@ -308,15 +274,14 @@ func TestGetIndexerClusterPeerInfo(t *testing.T) { return nil } body = `{"links":{},"origin":"https://localhost:8089/services/cluster/slave/info","updated":"2020-03-18T01:28:18+00:00","generator":{"build":"a7f645ddaf91","version":"8.0.2"},"entry":[],"paging":{"total":1,"perPage":30,"offset":0},"messages":[]}` - splunkClientTester(t, "TestGetIndexerClusterPeerInfo", 200, body, want, test) + splunkClientTester(t, "TestGetIndexerClusterPeerInfo", 200, body, wantRequest, test) // test error code - splunkClientTester(t, "TestGetIndexerClusterPeerInfo", 500, "", want, test) + splunkClientTester(t, "TestGetIndexerClusterPeerInfo", 500, "", wantRequest, test) } func TestGetClusterMasterPeers(t *testing.T) { wantRequest, _ := http.NewRequest("GET", "https://localhost:8089/services/cluster/master/peers?count=0&output_mode=json", nil) - want := []http.Request{*wantRequest} var wantPeers = []struct { ID string Label string @@ -350,7 +315,7 @@ func TestGetClusterMasterPeers(t *testing.T) { return nil } body := `{"links":{"create":"/services/cluster/master/peers/_new"},"origin":"https://localhost:8089/services/cluster/master/peers","updated":"2020-03-18T01:08:53+00:00","generator":{"build":"a7f645ddaf91","version":"8.0.2"},"entry":[{"name":"D39B1729-E2C5-4273-B9B2-534DA7C2F866","id":"https://localhost:8089/services/cluster/master/peers/D39B1729-E2C5-4273-B9B2-534DA7C2F866","updated":"1970-01-01T00:00:00+00:00","links":{"alternate":"/services/cluster/master/peers/D39B1729-E2C5-4273-B9B2-534DA7C2F866","list":"/services/cluster/master/peers/D39B1729-E2C5-4273-B9B2-534DA7C2F866","edit":"/services/cluster/master/peers/D39B1729-E2C5-4273-B9B2-534DA7C2F866"},"author":"system","acl":{"app":"","can_list":true,"can_write":true,"modifiable":false,"owner":"system","perms":{"read":["admin","splunk-system-role"],"write":["admin","splunk-system-role"]},"removable":false,"sharing":"system"},"content":{"active_bundle_id":"14310A4AABD23E85BBD4559C4A3B59F8","apply_bundle_status":{"invalid_bundle":{"bundle_validation_errors":[],"invalid_bundle_id":""},"reasons_for_restart":[],"restart_required_for_apply_bundle":false,"status":"None"},"base_generation_id":26,"bucket_count":73,"bucket_count_by_index":{"_audit":24,"_internal":45,"_telemetry":4},"buckets_rf_by_origin_site":{"default":73},"buckets_sf_by_origin_site":{"default":73},"delayed_buckets_to_discard":[],"eai:acl":null,"fixup_set":[],"heartbeat_started":true,"host_port_pair":"10.36.0.6:8089","indexing_disk_space":210707374080,"is_searchable":true,"is_valid_bundle":true,"label":"splunk-s1-indexer-0","last_dry_run_bundle":"","last_heartbeat":1584493732,"last_validated_bundle":"14310A4AABD23E85BBD4559C4A3B59F8","latest_bundle_id":"14310A4AABD23E85BBD4559C4A3B59F8","peer_registered_summaries":true,"pending_builds":[],"pending_job_count":0,"primary_count":73,"primary_count_remote":0,"register_search_address":"10.36.0.6:8089","replication_count":0,"replication_port":9887,"replication_use_ssl":false,"restart_required_for_applying_dry_run_bundle":false,"search_state_counter":{"PendingSearchable":0,"Searchable":73,"SearchablePendingMask":0,"Unsearchable":0},"site":"default","splunk_version":"8.0.2","status":"Up","status_counter":{"Complete":69,"NonStreamingTarget":0,"StreamingSource":4,"StreamingTarget":0},"summary_replication_count":0}}],"paging":{"total":1,"perPage":30,"offset":0},"messages":[]}` - splunkClientTester(t, "TestGetClusterMasterPeers", 200, body, want, test) + splunkClientTester(t, "TestGetClusterMasterPeers", 200, body, wantRequest, test) // test error response test = func(c SplunkClient) error { @@ -360,23 +325,21 @@ func TestGetClusterMasterPeers(t *testing.T) { } return nil } - splunkClientTester(t, "TestGetClusterMasterPeers", 503, "", want, test) + splunkClientTester(t, "TestGetClusterMasterPeers", 503, "", wantRequest, test) } func TestRemoveIndexerClusterPeer(t *testing.T) { wantRequest, _ := http.NewRequest("POST", "https://localhost:8089/services/cluster/master/control/control/remove_peers?peers=D39B1729-E2C5-4273-B9B2-534DA7C2F866", nil) - want := []http.Request{*wantRequest} test := func(c SplunkClient) error { return c.RemoveIndexerClusterPeer("D39B1729-E2C5-4273-B9B2-534DA7C2F866") } - splunkClientTester(t, "TestRemoveIndexerClusterPeer", 200, "", want, test) + splunkClientTester(t, "TestRemoveIndexerClusterPeer", 200, "", wantRequest, test) } func TestDecommissionIndexerClusterPeer(t *testing.T) { wantRequest, _ := http.NewRequest("POST", "https://localhost:8089/services/cluster/slave/control/control/decommission?enforce_counts=1", nil) - want := []http.Request{*wantRequest} test := func(c SplunkClient) error { return c.DecommissionIndexerClusterPeer(true) } - splunkClientTester(t, "TestDecommissionIndexerClusterPeer", 200, "", want, test) + splunkClientTester(t, "TestDecommissionIndexerClusterPeer", 200, "", wantRequest, test) } diff --git a/pkg/splunk/reconcile/indexercluster.go b/pkg/splunk/reconcile/indexercluster.go index 318884a90..a3d84adde 100644 --- a/pkg/splunk/reconcile/indexercluster.go +++ b/pkg/splunk/reconcile/indexercluster.go @@ -25,6 +25,7 @@ import ( "github.com/go-logr/logr" enterprisev1 "github.com/splunk/splunk-operator/pkg/apis/enterprise/v1alpha2" + splclient "github.com/splunk/splunk-operator/pkg/splunk/client" "github.com/splunk/splunk-operator/pkg/splunk/enterprise" "github.com/splunk/splunk-operator/pkg/splunk/resources" ) @@ -47,6 +48,7 @@ func ApplyIndexerCluster(client ControllerClient, cr *enterprisev1.IndexerCluste // updates status after function completes cr.Status.Phase = enterprisev1.PhaseError + cr.Status.ClusterMasterPhase = enterprisev1.PhaseError cr.Status.Replicas = cr.Spec.Replicas cr.Status.Selector = fmt.Sprintf("app.kubernetes.io/instance=splunk-%s-indexer", cr.GetIdentifier()) defer func() { @@ -94,41 +96,24 @@ func ApplyIndexerCluster(client ControllerClient, cr *enterprisev1.IndexerCluste if err != nil { return result, err } - cr.Status.ClusterMasterPhase, err = ApplyStatefulSet(client, statefulSet) - if err == nil && cr.Status.Phase == enterprisev1.PhaseReady { - mgr := DefaultStatefulSetPodManager{} - cr.Status.ClusterMasterPhase, err = UpdateStatefulSetPods(client, statefulSet, &mgr, 1) - } + clusterMasterManager := DefaultStatefulSetPodManager{} + phase, err := clusterMasterManager.Update(client, statefulSet, 1) if err != nil { - cr.Status.ClusterMasterPhase = enterprisev1.PhaseError return result, err } + cr.Status.ClusterMasterPhase = phase // create or update statefulset for the indexers statefulSet, err = enterprise.GetIndexerStatefulSet(cr) if err != nil { return result, err } - cr.Status.Phase, err = ApplyStatefulSet(client, statefulSet) - if err != nil { - return result, err - } - - // update CR status with SHC information - mgr := IndexerClusterPodManager{client: client, log: scopedLog, cr: cr, secrets: secrets} - err = mgr.updateStatus(statefulSet) - if err != nil || cr.Status.ReadyReplicas == 0 || !cr.Status.Initialized || !cr.Status.IndexingReady || !cr.Status.ServiceReady { - scopedLog.Error(err, "Indexer cluster is not ready") - cr.Status.Phase = enterprisev1.PhasePending - return result, nil - } - - // manage scaling and updates - cr.Status.Phase, err = UpdateStatefulSetPods(client, statefulSet, &mgr, cr.Spec.Replicas) + mgr := IndexerClusterPodManager{log: scopedLog, cr: cr, secrets: secrets, newSplunkClient: splclient.NewSplunkClient} + phase, err = mgr.Update(client, statefulSet, cr.Spec.Replicas) if err != nil { - cr.Status.Phase = enterprisev1.PhaseError return result, err } + cr.Status.Phase = phase // no need to requeue if everything is ready if cr.Status.Phase == enterprisev1.PhaseReady { @@ -139,10 +124,29 @@ func ApplyIndexerCluster(client ControllerClient, cr *enterprisev1.IndexerCluste // IndexerClusterPodManager is used to manage the pods within a search head cluster type IndexerClusterPodManager struct { - client ControllerClient - log logr.Logger - cr *enterprisev1.IndexerCluster - secrets *corev1.Secret + log logr.Logger + cr *enterprisev1.IndexerCluster + secrets *corev1.Secret + newSplunkClient func(managementURI, username, password string) *splclient.SplunkClient +} + +// Update for IndexerClusterPodManager handles all updates for a statefulset of indexers +func (mgr *IndexerClusterPodManager) Update(c ControllerClient, statefulSet *appsv1.StatefulSet, desiredReplicas int32) (enterprisev1.ResourcePhase, error) { + // update statefulset, if necessary + _, err := ApplyStatefulSet(c, statefulSet) + if err != nil { + return enterprisev1.PhaseError, err + } + + // update CR status with SHC information + err = mgr.updateStatus(statefulSet) + if err != nil || mgr.cr.Status.ReadyReplicas == 0 || !mgr.cr.Status.Initialized || !mgr.cr.Status.IndexingReady || !mgr.cr.Status.ServiceReady { + mgr.log.Error(err, "Indexer cluster is not ready") + return enterprisev1.PhasePending, nil + } + + // manage scaling and updates + return UpdateStatefulSetPods(c, statefulSet, mgr, desiredReplicas) } // PrepareScaleDown for IndexerClusterPodManager prepares indexer pod to be removed via scale down event; it returns true when ready @@ -207,17 +211,17 @@ func (mgr *IndexerClusterPodManager) decommission(n int32, enforceCounts bool) ( } // getClient for IndexerClusterPodManager returns a SplunkClient for the member n -func (mgr *IndexerClusterPodManager) getClient(n int32) *enterprise.SplunkClient { +func (mgr *IndexerClusterPodManager) getClient(n int32) *splclient.SplunkClient { memberName := enterprise.GetSplunkStatefulsetPodName(enterprise.SplunkIndexer, mgr.cr.GetIdentifier(), n) fqdnName := resources.GetServiceFQDN(mgr.cr.GetNamespace(), fmt.Sprintf("%s.%s", memberName, enterprise.GetSplunkServiceName(enterprise.SplunkIndexer, mgr.cr.GetIdentifier(), true))) - return enterprise.NewSplunkClient(fmt.Sprintf("https://%s:8089", fqdnName), "admin", string(mgr.secrets.Data["password"])) + return mgr.newSplunkClient(fmt.Sprintf("https://%s:8089", fqdnName), "admin", string(mgr.secrets.Data["password"])) } // getClusterMasterClient for IndexerClusterPodManager returns a SplunkClient for cluster master -func (mgr *IndexerClusterPodManager) getClusterMasterClient() *enterprise.SplunkClient { +func (mgr *IndexerClusterPodManager) getClusterMasterClient() *splclient.SplunkClient { fqdnName := resources.GetServiceFQDN(mgr.cr.GetNamespace(), enterprise.GetSplunkServiceName(enterprise.SplunkClusterMaster, mgr.cr.GetIdentifier(), false)) - return enterprise.NewSplunkClient(fmt.Sprintf("https://%s:8089", fqdnName), "admin", string(mgr.secrets.Data["password"])) + return mgr.newSplunkClient(fmt.Sprintf("https://%s:8089", fqdnName), "admin", string(mgr.secrets.Data["password"])) } // updateStatus for IndexerClusterPodManager uses the REST API to update the status for a SearcHead custom resource @@ -226,13 +230,11 @@ func (mgr *IndexerClusterPodManager) updateStatus(statefulSet *appsv1.StatefulSe mgr.cr.Status.Peers = []enterprisev1.IndexerClusterMemberStatus{} if mgr.cr.Status.ClusterMasterPhase != enterprisev1.PhaseReady { - mgr.cr.Status.Phase = enterprisev1.PhasePending mgr.cr.Status.Initialized = false mgr.cr.Status.IndexingReady = false mgr.cr.Status.ServiceReady = false mgr.cr.Status.MaintenanceMode = false - mgr.log.Info("Waiting for cluster master to become ready") - return nil + return fmt.Errorf("Waiting for cluster master to become ready") } // get indexer cluster info from cluster master if it's ready diff --git a/pkg/splunk/reconcile/indexercluster_test.go b/pkg/splunk/reconcile/indexercluster_test.go index 6838e2e60..1c3f49f08 100644 --- a/pkg/splunk/reconcile/indexercluster_test.go +++ b/pkg/splunk/reconcile/indexercluster_test.go @@ -15,12 +15,18 @@ package reconcile import ( + "strings" "testing" "time" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" enterprisev1 "github.com/splunk/splunk-operator/pkg/apis/enterprise/v1alpha2" + splclient "github.com/splunk/splunk-operator/pkg/splunk/client" + spltest "github.com/splunk/splunk-operator/pkg/splunk/test" ) func TestApplyIndexerCluster(t *testing.T) { @@ -62,3 +68,144 @@ func TestApplyIndexerCluster(t *testing.T) { } splunkDeletionTester(t, revised, deleteFunc) } + +func indexerClusterPodManagerTester(t *testing.T, method string, mockHandlers []spltest.MockHTTPHandler, + desiredReplicas int32, wantPhase enterprisev1.ResourcePhase, statefulSet *appsv1.StatefulSet, + wantCalls map[string][]mockFuncCall, wantError error, initObjects ...runtime.Object) { + + // test for updating + scopedLog := log.WithName(method) + cr := enterprisev1.IndexerCluster{ + TypeMeta: metav1.TypeMeta{ + Kind: "IndexerCluster", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "stack1", + Namespace: "test", + }, + Status: enterprisev1.IndexerClusterStatus{ + ClusterMasterPhase: enterprisev1.PhaseReady, + }, + } + secrets := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "stack1-secrets", + Namespace: "test", + }, + Data: map[string][]byte{ + "password": []byte{'1', '2', '3'}, + }, + } + mockSplunkClient := &spltest.MockHTTPClient{} + mockSplunkClient.AddHandlers(mockHandlers...) + mgr := &IndexerClusterPodManager{ + log: scopedLog, + cr: &cr, + secrets: secrets, + newSplunkClient: func(managementURI, username, password string) *splclient.SplunkClient { + c := splclient.NewSplunkClient(managementURI, username, password) + c.Client = mockSplunkClient + return c + }, + } + podManagerUpdateTester(t, method, mgr, desiredReplicas, wantPhase, statefulSet, wantCalls, wantError, initObjects...) + mockSplunkClient.CheckRequests(t, method) +} + +func TestIndexerClusterPodManager(t *testing.T) { + var replicas int32 = 1 + statefulSet := &appsv1.StatefulSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: "splunk-stack1", + Namespace: "test", + }, + Spec: appsv1.StatefulSetSpec{ + Replicas: &replicas, + }, + Status: appsv1.StatefulSetStatus{ + Replicas: replicas, + ReadyReplicas: replicas, + UpdatedReplicas: replicas, + UpdateRevision: "v1", + }, + } + funcCalls := []mockFuncCall{ + {metaName: "*v1.StatefulSet-test-splunk-stack1"}, + {metaName: "*v1.Pod-test-splunk-stack1-0"}, + } + wantCalls := map[string][]mockFuncCall{"Get": {funcCalls[0]}} + + // test 1 ready pod + mockHandlers := []spltest.MockHTTPHandler{ + {"GET", "https://splunk-stack1-cluster-master-service.test.svc.cluster.local:8089/services/cluster/master/info?count=0&output_mode=json", 200, nil, + `{"links":{},"origin":"https://localhost:8089/services/cluster/master/info","updated":"2020-03-18T01:04:53+00:00","generator":{"build":"a7f645ddaf91","version":"8.0.2"},"entry":[{"name":"master","id":"https://localhost:8089/services/cluster/master/info/master","updated":"1970-01-01T00:00:00+00:00","links":{"alternate":"/services/cluster/master/info/master","list":"/services/cluster/master/info/master"},"author":"system","acl":{"app":"","can_list":true,"can_write":true,"modifiable":false,"owner":"system","perms":{"read":["admin","splunk-system-role"],"write":["admin","splunk-system-role"]},"removable":false,"sharing":"system"},"content":{"active_bundle":{"bundle_path":"/opt/splunk/var/run/splunk/cluster/remote-bundle/506c58d5aeda1dd6017889e3186e7337-1583870198.bundle","checksum":"14310A4AABD23E85BBD4559C4A3B59F8","timestamp":1583870198},"apply_bundle_status":{"invalid_bundle":{"bundle_path":"","bundle_validation_errors_on_master":[],"checksum":"","timestamp":0},"reload_bundle_issued":false,"status":"None"},"backup_and_restore_primaries":false,"controlled_rolling_restart_flag":false,"eai:acl":null,"indexing_ready_flag":true,"initialized_flag":true,"label":"splunk-stack1-cluster-master-0","last_check_restart_bundle_result":false,"last_dry_run_bundle":{"bundle_path":"","checksum":"","timestamp":0},"last_validated_bundle":{"bundle_path":"/opt/splunk/var/run/splunk/cluster/remote-bundle/0af7c0e95f313f7be3b0cb1d878df9a1-1583948640.bundle","checksum":"14310A4AABD23E85BBD4559C4A3B59F8","is_valid_bundle":true,"timestamp":1583948640},"latest_bundle":{"bundle_path":"/opt/splunk/var/run/splunk/cluster/remote-bundle/506c58d5aeda1dd6017889e3186e7337-1583870198.bundle","checksum":"14310A4AABD23E85BBD4559C4A3B59F8","timestamp":1583870198},"maintenance_mode":false,"multisite":false,"previous_active_bundle":{"bundle_path":"","checksum":"","timestamp":0},"primaries_backup_status":"No on-going (or) completed primaries backup yet. Check back again in few minutes if you expect a backup.","quiet_period_flag":false,"rolling_restart_flag":false,"rolling_restart_or_upgrade":false,"service_ready_flag":true,"start_time":1583948636,"summary_replication":"false"}}],"paging":{"total":1,"perPage":30,"offset":0},"messages":[]}`}, + {"GET", "https://splunk-stack1-cluster-master-service.test.svc.cluster.local:8089/services/cluster/master/peers?count=0&output_mode=json", 200, nil, + `{"links":{"create":"/services/cluster/master/peers/_new"},"origin":"https://localhost:8089/services/cluster/master/peers","updated":"2020-03-18T01:08:53+00:00","generator":{"build":"a7f645ddaf91","version":"8.0.2"},"entry":[{"name":"D39B1729-E2C5-4273-B9B2-534DA7C2F866","id":"https://localhost:8089/services/cluster/master/peers/D39B1729-E2C5-4273-B9B2-534DA7C2F866","updated":"1970-01-01T00:00:00+00:00","links":{"alternate":"/services/cluster/master/peers/D39B1729-E2C5-4273-B9B2-534DA7C2F866","list":"/services/cluster/master/peers/D39B1729-E2C5-4273-B9B2-534DA7C2F866","edit":"/services/cluster/master/peers/D39B1729-E2C5-4273-B9B2-534DA7C2F866"},"author":"system","acl":{"app":"","can_list":true,"can_write":true,"modifiable":false,"owner":"system","perms":{"read":["admin","splunk-system-role"],"write":["admin","splunk-system-role"]},"removable":false,"sharing":"system"},"content":{"active_bundle_id":"14310A4AABD23E85BBD4559C4A3B59F8","apply_bundle_status":{"invalid_bundle":{"bundle_validation_errors":[],"invalid_bundle_id":""},"reasons_for_restart":[],"restart_required_for_apply_bundle":false,"status":"None"},"base_generation_id":26,"bucket_count":73,"bucket_count_by_index":{"_audit":24,"_internal":45,"_telemetry":4},"buckets_rf_by_origin_site":{"default":73},"buckets_sf_by_origin_site":{"default":73},"delayed_buckets_to_discard":[],"eai:acl":null,"fixup_set":[],"heartbeat_started":true,"host_port_pair":"10.36.0.6:8089","indexing_disk_space":210707374080,"is_searchable":true,"is_valid_bundle":true,"label":"splunk-stack1-indexer-0","last_dry_run_bundle":"","last_heartbeat":1584493732,"last_validated_bundle":"14310A4AABD23E85BBD4559C4A3B59F8","latest_bundle_id":"14310A4AABD23E85BBD4559C4A3B59F8","peer_registered_summaries":true,"pending_builds":[],"pending_job_count":0,"primary_count":73,"primary_count_remote":0,"register_search_address":"10.36.0.6:8089","replication_count":0,"replication_port":9887,"replication_use_ssl":false,"restart_required_for_applying_dry_run_bundle":false,"search_state_counter":{"PendingSearchable":0,"Searchable":73,"SearchablePendingMask":0,"Unsearchable":0},"site":"default","splunk_version":"8.0.2","status":"Up","status_counter":{"Complete":69,"NonStreamingTarget":0,"StreamingSource":4,"StreamingTarget":0},"summary_replication_count":0}}],"paging":{"total":1,"perPage":30,"offset":0},"messages":[]}`}, + } + wantCalls = map[string][]mockFuncCall{"Get": funcCalls} + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "splunk-stack1-0", + Namespace: "test", + Labels: map[string]string{ + "controller-revision-hash": "v1", + }, + }, + } + method := "IndexerClusterPodManager.Update(All pods ready)" + indexerClusterPodManagerTester(t, method, mockHandlers, 1, enterprisev1.PhaseReady, statefulSet, wantCalls, nil, statefulSet, pod) + + // test pod needs update => decommission + mockHandlers = append(mockHandlers, + spltest.MockHTTPHandler{"POST", "https://splunk-stack1-indexer-0.splunk-stack1-indexer-headless.test.svc.cluster.local:8089/services/cluster/slave/control/control/decommission?enforce_counts=0", 200, nil, ``}, + ) + pod.ObjectMeta.Labels["controller-revision-hash"] = "v0" + method = "IndexerClusterPodManager.Update(Decommission Pod)" + indexerClusterPodManagerTester(t, method, mockHandlers, 1, enterprisev1.PhaseUpdating, statefulSet, wantCalls, nil, statefulSet, pod) + + // test pod needs update => wait for decommission to complete + mockHandlers = []spltest.MockHTTPHandler{mockHandlers[0], mockHandlers[1]} + mockHandlers[1].Body = strings.Replace(mockHandlers[1].Body, `"status":"Up"`, `"status":"ReassigningPrimaries"`, 1) + method = "IndexerClusterPodManager.Update(ReassigningPrimaries)" + indexerClusterPodManagerTester(t, method, mockHandlers, 1, enterprisev1.PhaseUpdating, statefulSet, wantCalls, nil, statefulSet, pod) + + // test pod needs update => wait for decommission to complete + mockHandlers[1].Body = strings.Replace(mockHandlers[1].Body, `"status":"ReassigningPrimaries"`, `"status":"Decommissioning"`, 1) + method = "IndexerClusterPodManager.Update(Decommissioning)" + indexerClusterPodManagerTester(t, method, mockHandlers, 1, enterprisev1.PhaseUpdating, statefulSet, wantCalls, nil, statefulSet, pod) + + // test pod needs update => delete pod + wantCalls = map[string][]mockFuncCall{"Get": funcCalls, "Delete": {funcCalls[1]}} + mockHandlers[1].Body = strings.Replace(mockHandlers[1].Body, `"status":"Decommissioning"`, `"status":"Down"`, 1) + method = "IndexerClusterPodManager.Update(Delete Pod)" + indexerClusterPodManagerTester(t, method, mockHandlers, 1, enterprisev1.PhaseUpdating, statefulSet, wantCalls, nil, statefulSet, pod) + + // test scale down => pod not found + pod.ObjectMeta.Name = "splunk-stack1-2" + replicas = 2 + statefulSet.Status.Replicas = 2 + statefulSet.Status.ReadyReplicas = 2 + statefulSet.Status.UpdatedReplicas = 2 + wantCalls = map[string][]mockFuncCall{"Get": {funcCalls[0]}} + method = "IndexerClusterPodManager.Update(Pod Not Found)" + indexerClusterPodManagerTester(t, method, mockHandlers, 1, enterprisev1.PhaseScalingDown, statefulSet, wantCalls, nil, statefulSet, pod) + + // test scale down => decommission pod + mockHandlers[1].Body = `{"entry":[{"name":"aa45bf46-7f46-47af-a760-590d5c606d10","content":{"status":"Up","label":"splunk-stack1-indexer-0"}},{"name":"D39B1729-E2C5-4273-B9B2-534DA7C2F866","content":{"status":"GracefulShutdown","label":"splunk-stack1-indexer-1"}}]}` + mockHandlers = append(mockHandlers, + spltest.MockHTTPHandler{"POST", "https://splunk-stack1-cluster-master-service.test.svc.cluster.local:8089/services/cluster/master/control/control/remove_peers?peers=D39B1729-E2C5-4273-B9B2-534DA7C2F866", 200, nil, ``}, + ) + pvcCalls := []mockFuncCall{ + {metaName: "*v1.PersistentVolumeClaim-test-pvc-etc-splunk-stack1-1"}, + {metaName: "*v1.PersistentVolumeClaim-test-pvc-var-splunk-stack1-1"}, + } + funcCalls[1] = mockFuncCall{metaName: "*v1.Pod-test-splunk-stack1-0"} + wantCalls = map[string][]mockFuncCall{"Get": {funcCalls[0]}, "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"}}, + {ObjectMeta: metav1.ObjectMeta{Name: "pvc-var-splunk-stack1-1", Namespace: "test"}}, + } + method = "IndexerClusterPodManager.Update(Decommission)" + indexerClusterPodManagerTester(t, method, mockHandlers, 1, enterprisev1.PhaseScalingDown, statefulSet, wantCalls, nil, statefulSet, pod, pvcList[0], pvcList[1]) +} diff --git a/pkg/splunk/reconcile/licensemaster.go b/pkg/splunk/reconcile/licensemaster.go index af51c7ae3..8794ab6d9 100644 --- a/pkg/splunk/reconcile/licensemaster.go +++ b/pkg/splunk/reconcile/licensemaster.go @@ -73,15 +73,16 @@ func ApplyLicenseMaster(client ControllerClient, cr *enterprisev1.LicenseMaster) if err != nil { return result, err } - cr.Status.Phase, err = ApplyStatefulSet(client, statefulSet) - if err == nil && cr.Status.Phase == enterprisev1.PhaseReady { - mgr := DefaultStatefulSetPodManager{} - cr.Status.Phase, err = UpdateStatefulSetPods(client, statefulSet, &mgr, 1) - } + mgr := DefaultStatefulSetPodManager{} + phase, err := mgr.Update(client, statefulSet, 1) if err != nil { - cr.Status.Phase = enterprisev1.PhaseError - } else if cr.Status.Phase == enterprisev1.PhaseReady { + return result, err + } + cr.Status.Phase = phase + + // no need to requeue if everything is ready + if cr.Status.Phase == enterprisev1.PhaseReady { result.Requeue = false } - return result, err + return result, nil } diff --git a/pkg/splunk/reconcile/searchheadcluster.go b/pkg/splunk/reconcile/searchheadcluster.go index db893a593..a8386ce72 100644 --- a/pkg/splunk/reconcile/searchheadcluster.go +++ b/pkg/splunk/reconcile/searchheadcluster.go @@ -22,10 +22,10 @@ import ( "github.com/go-logr/logr" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/reconcile" enterprisev1 "github.com/splunk/splunk-operator/pkg/apis/enterprise/v1alpha2" + splclient "github.com/splunk/splunk-operator/pkg/splunk/client" "github.com/splunk/splunk-operator/pkg/splunk/enterprise" "github.com/splunk/splunk-operator/pkg/splunk/resources" ) @@ -47,6 +47,7 @@ func ApplySearchHeadCluster(client ControllerClient, cr *enterprisev1.SearchHead // updates status after function completes cr.Status.Phase = enterprisev1.PhaseError + cr.Status.DeployerPhase = enterprisev1.PhaseError cr.Status.Replicas = cr.Spec.Replicas cr.Status.Selector = fmt.Sprintf("app.kubernetes.io/instance=splunk-%s-search-head", cr.GetIdentifier()) defer func() { @@ -94,41 +95,24 @@ func ApplySearchHeadCluster(client ControllerClient, cr *enterprisev1.SearchHead if err != nil { return result, err } - cr.Status.DeployerPhase, err = ApplyStatefulSet(client, statefulSet) - if err == nil && cr.Status.DeployerPhase == enterprisev1.PhaseReady { - mgr := DefaultStatefulSetPodManager{} - cr.Status.DeployerPhase, err = UpdateStatefulSetPods(client, statefulSet, &mgr, 1) - } + deployerManager := DefaultStatefulSetPodManager{} + phase, err := deployerManager.Update(client, statefulSet, 1) if err != nil { - cr.Status.DeployerPhase = enterprisev1.PhaseError return result, err } + cr.Status.DeployerPhase = phase // create or update statefulset for the search heads statefulSet, err = enterprise.GetSearchHeadStatefulSet(cr) if err != nil { return result, err } - cr.Status.Phase, err = ApplyStatefulSet(client, statefulSet) + mgr := SearchHeadClusterPodManager{log: scopedLog, cr: cr, secrets: secrets, newSplunkClient: splclient.NewSplunkClient} + phase, err = mgr.Update(client, statefulSet, cr.Spec.Replicas) if err != nil { return result, err } - - // update CR status with SHC information - mgr := SearchHeadClusterPodManager{client: client, log: scopedLog, cr: cr, secrets: secrets} - err = mgr.updateStatus(statefulSet) - if err != nil || cr.Status.ReadyReplicas == 0 || !cr.Status.Initialized || !cr.Status.CaptainReady { - scopedLog.Error(err, "Search head cluster is not ready") - cr.Status.Phase = enterprisev1.PhasePending - return result, nil - } - - // manage scaling and updates - cr.Status.Phase, err = UpdateStatefulSetPods(client, statefulSet, &mgr, cr.Spec.Replicas) - if err != nil { - cr.Status.Phase = enterprisev1.PhaseError - return result, err - } + cr.Status.Phase = phase // no need to requeue if everything is ready if cr.Status.Phase == enterprisev1.PhaseReady { @@ -139,10 +123,29 @@ func ApplySearchHeadCluster(client ControllerClient, cr *enterprisev1.SearchHead // SearchHeadClusterPodManager is used to manage the pods within a search head cluster type SearchHeadClusterPodManager struct { - client ControllerClient - log logr.Logger - cr *enterprisev1.SearchHeadCluster - secrets *corev1.Secret + log logr.Logger + cr *enterprisev1.SearchHeadCluster + secrets *corev1.Secret + newSplunkClient func(managementURI, username, password string) *splclient.SplunkClient +} + +// Update for SearchHeadClusterPodManager handles all updates for a statefulset of search heads +func (mgr *SearchHeadClusterPodManager) Update(c ControllerClient, statefulSet *appsv1.StatefulSet, desiredReplicas int32) (enterprisev1.ResourcePhase, error) { + // update statefulset, if necessary + _, err := ApplyStatefulSet(c, statefulSet) + if err != nil { + return enterprisev1.PhaseError, err + } + + // update CR status with SHC information + err = mgr.updateStatus(statefulSet) + if err != nil || mgr.cr.Status.ReadyReplicas == 0 || !mgr.cr.Status.Initialized || !mgr.cr.Status.CaptainReady { + mgr.log.Error(err, "Search head cluster is not ready") + return enterprisev1.PhasePending, nil + } + + // manage scaling and updates + return UpdateStatefulSetPods(c, statefulSet, mgr, desiredReplicas) } // PrepareScaleDown for SearchHeadClusterPodManager prepares search head pod to be removed via scale down event; it returns true when ready @@ -162,25 +165,6 @@ func (mgr *SearchHeadClusterPodManager) PrepareScaleDown(n int32) (bool, error) return false, err } - // delete PVCs used by the pod so that a future scale up will have clean state - for _, vol := range []string{"pvc-etc", "pvc-var"} { - namespacedName := types.NamespacedName{ - Namespace: mgr.cr.GetNamespace(), - Name: fmt.Sprintf("%s-%s", vol, memberName), - } - var pvc corev1.PersistentVolumeClaim - err := mgr.client.Get(context.TODO(), namespacedName, &pvc) - if err != nil { - return false, err - } - - log.Info("Deleting PVC", "name", pvc.ObjectMeta.Name) - err = mgr.client.Delete(context.Background(), &pvc) - if err != nil { - return false, err - } - } - // all done -> ok to scale down the statefulset return true, nil } @@ -232,11 +216,11 @@ func (mgr *SearchHeadClusterPodManager) FinishRecycle(n int32) (bool, error) { } // getClient for SearchHeadClusterPodManager returns a SplunkClient for the member n -func (mgr *SearchHeadClusterPodManager) getClient(n int32) *enterprise.SplunkClient { +func (mgr *SearchHeadClusterPodManager) getClient(n int32) *splclient.SplunkClient { memberName := enterprise.GetSplunkStatefulsetPodName(enterprise.SplunkSearchHead, mgr.cr.GetIdentifier(), n) fqdnName := resources.GetServiceFQDN(mgr.cr.GetNamespace(), fmt.Sprintf("%s.%s", memberName, enterprise.GetSplunkServiceName(enterprise.SplunkSearchHead, mgr.cr.GetIdentifier(), true))) - return enterprise.NewSplunkClient(fmt.Sprintf("https://%s:8089", fqdnName), "admin", string(mgr.secrets.Data["password"])) + return mgr.newSplunkClient(fmt.Sprintf("https://%s:8089", fqdnName), "admin", string(mgr.secrets.Data["password"])) } // updateStatus for SearchHeadClusterPodManager uses the REST API to update the status for a SearcHead custom resource diff --git a/pkg/splunk/reconcile/searchheadcluster_test.go b/pkg/splunk/reconcile/searchheadcluster_test.go index 37aa7fb6e..ffe69eef2 100644 --- a/pkg/splunk/reconcile/searchheadcluster_test.go +++ b/pkg/splunk/reconcile/searchheadcluster_test.go @@ -15,12 +15,18 @@ package reconcile import ( + "strings" "testing" "time" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" enterprisev1 "github.com/splunk/splunk-operator/pkg/apis/enterprise/v1alpha2" + splclient "github.com/splunk/splunk-operator/pkg/splunk/client" + spltest "github.com/splunk/splunk-operator/pkg/splunk/test" ) func TestApplySearchHeadCluster(t *testing.T) { @@ -34,7 +40,7 @@ func TestApplySearchHeadCluster(t *testing.T) { } createCalls := map[string][]mockFuncCall{"Get": funcCalls, "Create": funcCalls} updateCalls := map[string][]mockFuncCall{"Get": funcCalls, "Update": []mockFuncCall{funcCalls[4], funcCalls[5]}} - current := enterprisev1.SearchHeadCluster{ + statefulSet := enterprisev1.SearchHeadCluster{ TypeMeta: metav1.TypeMeta{ Kind: "SearchHeadCluster", }, @@ -43,13 +49,13 @@ func TestApplySearchHeadCluster(t *testing.T) { Namespace: "test", }, } - revised := current.DeepCopy() + revised := statefulSet.DeepCopy() revised.Spec.Image = "splunk/test" reconcile := func(c *mockClient, cr interface{}) error { _, err := ApplySearchHeadCluster(c, cr.(*enterprisev1.SearchHeadCluster)) return err } - reconcileTester(t, "TestApplySearchHeadCluster", ¤t, revised, createCalls, updateCalls, reconcile) + reconcileTester(t, "TestApplySearchHeadCluster", &statefulSet, revised, createCalls, updateCalls, reconcile) // test deletion currentTime := metav1.NewTime(time.Now()) @@ -61,3 +67,149 @@ func TestApplySearchHeadCluster(t *testing.T) { } splunkDeletionTester(t, revised, deleteFunc) } + +func searchHeadClusterPodManagerTester(t *testing.T, method string, mockHandlers []spltest.MockHTTPHandler, + desiredReplicas int32, wantPhase enterprisev1.ResourcePhase, statefulSet *appsv1.StatefulSet, + wantCalls map[string][]mockFuncCall, wantError error, initObjects ...runtime.Object) { + + // test for updating + scopedLog := log.WithName(method) + cr := enterprisev1.SearchHeadCluster{ + TypeMeta: metav1.TypeMeta{ + Kind: "SearchHeadCluster", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "stack1", + Namespace: "test", + }, + } + secrets := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "stack1-secrets", + Namespace: "test", + }, + Data: map[string][]byte{ + "password": []byte{'1', '2', '3'}, + }, + } + mockSplunkClient := &spltest.MockHTTPClient{} + mockSplunkClient.AddHandlers(mockHandlers...) + mgr := &SearchHeadClusterPodManager{ + log: scopedLog, + cr: &cr, + secrets: secrets, + newSplunkClient: func(managementURI, username, password string) *splclient.SplunkClient { + c := splclient.NewSplunkClient(managementURI, username, password) + c.Client = mockSplunkClient + return c + }, + } + podManagerUpdateTester(t, method, mgr, desiredReplicas, wantPhase, statefulSet, wantCalls, wantError, initObjects...) + mockSplunkClient.CheckRequests(t, method) +} + +func TestSearchHeadClusterPodManager(t *testing.T) { + var replicas int32 = 1 + statefulSet := &appsv1.StatefulSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: "splunk-stack1", + Namespace: "test", + }, + Spec: appsv1.StatefulSetSpec{ + Replicas: &replicas, + }, + Status: appsv1.StatefulSetStatus{ + Replicas: replicas, + ReadyReplicas: replicas, + UpdatedReplicas: replicas, + UpdateRevision: "v1", + }, + } + mockHandlers := []spltest.MockHTTPHandler{ + {"GET", "https://splunk-stack1-search-head-0.splunk-stack1-search-head-headless.test.svc.cluster.local:8089/services/shcluster/member/info?count=0&output_mode=json", 500, nil, ``}, + } + funcCalls := []mockFuncCall{ + {metaName: "*v1.StatefulSet-test-splunk-stack1"}, + {metaName: "*v1.Pod-test-splunk-stack1-0"}, + } + wantCalls := map[string][]mockFuncCall{"Get": {funcCalls[0]}} + + // test API failure + method := "SearchHeadClusterPodManager.Update(API failure)" + searchHeadClusterPodManagerTester(t, method, mockHandlers, 1, enterprisev1.PhasePending, statefulSet, wantCalls, nil, statefulSet) + + // test 1 ready pod + mockHandlers = []spltest.MockHTTPHandler{ + {"GET", "https://splunk-stack1-search-head-0.splunk-stack1-search-head-headless.test.svc.cluster.local:8089/services/shcluster/member/info?count=0&output_mode=json", 200, nil, + `{"links":{},"origin":"https://localhost:8089/services/shcluster/member/info","updated":"2020-03-15T16:30:38+00:00","generator":{"build":"a7f645ddaf91","version":"8.0.2"},"entry":[{"name":"member","id":"https://localhost:8089/services/shcluster/member/info/member","updated":"1970-01-01T00:00:00+00:00","links":{"alternate":"/services/shcluster/member/info/member","list":"/services/shcluster/member/info/member"},"author":"system","acl":{"app":"","can_list":true,"can_write":true,"modifiable":false,"owner":"system","perms":{"read":["admin","splunk-system-role"],"write":["admin","splunk-system-role"]},"removable":false,"sharing":"system"},"content":{"active_historical_search_count":0,"active_realtime_search_count":0,"adhoc_searchhead":false,"eai:acl":null,"is_registered":true,"last_heartbeat_attempt":1584289836,"maintenance_mode":false,"no_artifact_replications":false,"peer_load_stats_gla_15m":0,"peer_load_stats_gla_1m":0,"peer_load_stats_gla_5m":0,"peer_load_stats_max_runtime":0,"peer_load_stats_num_autosummary":0,"peer_load_stats_num_historical":0,"peer_load_stats_num_realtime":0,"peer_load_stats_num_running":0,"peer_load_stats_total_runtime":0,"restart_state":"NoRestart","status":"Up"}}],"paging":{"total":1,"perPage":30,"offset":0},"messages":[]}`}, + {"GET", "https://splunk-stack1-search-head-0.splunk-stack1-search-head-headless.test.svc.cluster.local:8089/services/shcluster/captain/info?count=0&output_mode=json", 200, nil, + `{"links":{},"origin":"https://localhost:8089/services/shcluster/captain/info","updated":"2020-03-15T16:36:42+00:00","generator":{"build":"a7f645ddaf91","version":"8.0.2"},"entry":[{"name":"captain","id":"https://localhost:8089/services/shcluster/captain/info/captain","updated":"1970-01-01T00:00:00+00:00","links":{"alternate":"/services/shcluster/captain/info/captain","list":"/services/shcluster/captain/info/captain"},"author":"system","acl":{"app":"","can_list":true,"can_write":true,"modifiable":false,"owner":"system","perms":{"read":["admin","splunk-system-role"],"write":["admin","splunk-system-role"]},"removable":false,"sharing":"system"},"content":{"eai:acl":null,"elected_captain":1584139352,"id":"A9D5FCCF-EB93-4E0A-93E1-45B56483EA7A","initialized_flag":true,"label":"splunk-s2-search-head-0","maintenance_mode":false,"mgmt_uri":"https://splunk-s2-search-head-0.splunk-s2-search-head-headless.splunk.svc.cluster.local:8089","min_peers_joined_flag":true,"peer_scheme_host_port":"https://splunk-s2-search-head-0.splunk-s2-search-head-headless.splunk.svc.cluster.local:8089","rolling_restart_flag":false,"service_ready_flag":true,"start_time":1584139291}}],"paging":{"total":1,"perPage":30,"offset":0},"messages":[]}`}, + } + wantCalls = map[string][]mockFuncCall{"Get": funcCalls} + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "splunk-stack1-0", + Namespace: "test", + Labels: map[string]string{ + "controller-revision-hash": "v1", + }, + }, + } + method = "SearchHeadClusterPodManager.Update(All pods ready)" + searchHeadClusterPodManagerTester(t, method, mockHandlers, 1, enterprisev1.PhaseReady, statefulSet, wantCalls, nil, statefulSet, pod) + + // test pod needs update => transition to detention + mockHandlers = append(mockHandlers, + spltest.MockHTTPHandler{"POST", "https://splunk-stack1-search-head-0.splunk-stack1-search-head-headless.test.svc.cluster.local:8089/services/shcluster/member/control/control/set_manual_detention?manual_detention=on", 200, nil, ``}, + ) + pod.ObjectMeta.Labels["controller-revision-hash"] = "v0" + method = "SearchHeadClusterPodManager.Update(Quarantine Pod)" + searchHeadClusterPodManagerTester(t, method, mockHandlers, 1, enterprisev1.PhaseUpdating, statefulSet, wantCalls, nil, statefulSet, pod) + + // test pod needs update => wait for searches to drain + mockHandlers = []spltest.MockHTTPHandler{mockHandlers[0], mockHandlers[1]} + mockHandlers[0].Body = strings.Replace(mockHandlers[0].Body, `"status":"Up"`, `"status":"ManualDetention"`, 1) + mockHandlers[0].Body = strings.Replace(mockHandlers[0].Body, `"active_historical_search_count":0`, `"active_historical_search_count":1`, 1) + method = "SearchHeadClusterPodManager.Update(Draining Searches)" + searchHeadClusterPodManagerTester(t, method, mockHandlers, 1, enterprisev1.PhaseUpdating, statefulSet, wantCalls, nil, statefulSet, pod) + + // test pod needs update => delete pod + wantCalls = map[string][]mockFuncCall{"Get": funcCalls, "Delete": {funcCalls[1]}} + mockHandlers[0].Body = strings.Replace(mockHandlers[0].Body, `"active_historical_search_count":1`, `"active_historical_search_count":0`, 1) + method = "SearchHeadClusterPodManager.Update(Delete Pod)" + searchHeadClusterPodManagerTester(t, method, mockHandlers, 1, enterprisev1.PhaseUpdating, statefulSet, wantCalls, nil, statefulSet, pod) + + // test pod update finished => release from detention + wantCalls = map[string][]mockFuncCall{"Get": funcCalls} + pod.ObjectMeta.Labels["controller-revision-hash"] = "v1" + mockHandlers = append(mockHandlers, + spltest.MockHTTPHandler{"POST", "https://splunk-stack1-search-head-0.splunk-stack1-search-head-headless.test.svc.cluster.local:8089/services/shcluster/member/control/control/set_manual_detention?manual_detention=off", 200, nil, ``}, + ) + method = "SearchHeadClusterPodManager.Update(Release Quarantine)" + searchHeadClusterPodManagerTester(t, method, mockHandlers, 1, enterprisev1.PhaseUpdating, statefulSet, wantCalls, nil, statefulSet, pod) + + // test scale down => remove member + mockHandlers[2] = spltest.MockHTTPHandler{"GET", "https://splunk-stack1-search-head-1.splunk-stack1-search-head-headless.test.svc.cluster.local:8089/services/shcluster/member/info?count=0&output_mode=json", 200, nil, + `{"links":{},"origin":"https://localhost:8089/services/shcluster/member/info","updated":"2020-03-15T16:30:38+00:00","generator":{"build":"a7f645ddaf91","version":"8.0.2"},"entry":[{"name":"member","id":"https://localhost:8089/services/shcluster/member/info/member","updated":"1970-01-01T00:00:00+00:00","links":{"alternate":"/services/shcluster/member/info/member","list":"/services/shcluster/member/info/member"},"author":"system","acl":{"app":"","can_list":true,"can_write":true,"modifiable":false,"owner":"system","perms":{"read":["admin","splunk-system-role"],"write":["admin","splunk-system-role"]},"removable":false,"sharing":"system"},"content":{"active_historical_search_count":0,"active_realtime_search_count":0,"adhoc_searchhead":false,"eai:acl":null,"is_registered":true,"last_heartbeat_attempt":1584289836,"maintenance_mode":false,"no_artifact_replications":false,"peer_load_stats_gla_15m":0,"peer_load_stats_gla_1m":0,"peer_load_stats_gla_5m":0,"peer_load_stats_max_runtime":0,"peer_load_stats_num_autosummary":0,"peer_load_stats_num_historical":0,"peer_load_stats_num_realtime":0,"peer_load_stats_num_running":0,"peer_load_stats_total_runtime":0,"restart_state":"NoRestart","status":"ManualDetention"}}],"paging":{"total":1,"perPage":30,"offset":0},"messages":[]}`} + mockHandlers = append(mockHandlers, + spltest.MockHTTPHandler{"POST", "https://splunk-stack1-search-head-1.splunk-stack1-search-head-headless.test.svc.cluster.local:8089/services/shcluster/member/consensus/default/remove_server?output_mode=json", 200, nil, ``}, + ) + pvcCalls := []mockFuncCall{ + {metaName: "*v1.PersistentVolumeClaim-test-pvc-etc-splunk-stack1-1"}, + {metaName: "*v1.PersistentVolumeClaim-test-pvc-var-splunk-stack1-1"}, + } + funcCalls[1] = mockFuncCall{metaName: "*v1.Pod-test-splunk-stack1-0"} + wantCalls = map[string][]mockFuncCall{"Get": {funcCalls[0]}, "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"}}, + {ObjectMeta: metav1.ObjectMeta{Name: "pvc-var-splunk-stack1-1", Namespace: "test"}}, + } + pod.ObjectMeta.Name = "splunk-stack1-2" + replicas = 2 + statefulSet.Status.Replicas = 2 + statefulSet.Status.ReadyReplicas = 2 + statefulSet.Status.UpdatedReplicas = 2 + method = "SearchHeadClusterPodManager.Update(Remove Member)" + searchHeadClusterPodManagerTester(t, method, mockHandlers, 1, enterprisev1.PhaseScalingDown, statefulSet, wantCalls, nil, statefulSet, pod, pvcList[0], pvcList[1]) +} diff --git a/pkg/splunk/reconcile/standalone.go b/pkg/splunk/reconcile/standalone.go index ed0cdac94..cbe7ca118 100644 --- a/pkg/splunk/reconcile/standalone.go +++ b/pkg/splunk/reconcile/standalone.go @@ -76,16 +76,17 @@ func ApplyStandalone(client ControllerClient, cr *enterprisev1.Standalone) (reco if err != nil { return result, err } - cr.Status.Phase, err = ApplyStatefulSet(client, statefulSet) + mgr := DefaultStatefulSetPodManager{} + phase, err := mgr.Update(client, statefulSet, cr.Spec.Replicas) cr.Status.ReadyReplicas = statefulSet.Status.ReadyReplicas - if err == nil && cr.Status.Phase == enterprisev1.PhaseReady { - mgr := DefaultStatefulSetPodManager{} - cr.Status.Phase, err = UpdateStatefulSetPods(client, statefulSet, &mgr, cr.Spec.Replicas) - } if err != nil { - cr.Status.Phase = enterprisev1.PhaseError - } else if cr.Status.Phase == enterprisev1.PhaseReady { + return result, err + } + cr.Status.Phase = phase + + // no need to requeue if everything is ready + if cr.Status.Phase == enterprisev1.PhaseReady { result.Requeue = false } - return result, err + return result, nil } diff --git a/pkg/splunk/reconcile/statefulset.go b/pkg/splunk/reconcile/statefulset.go index 569d4cb4a..ebec6026a 100644 --- a/pkg/splunk/reconcile/statefulset.go +++ b/pkg/splunk/reconcile/statefulset.go @@ -28,19 +28,31 @@ import ( // StatefulSetPodManager is used to manage the pods within a StatefulSet type StatefulSetPodManager interface { + // Update handles all updates for a statefulset and all of its pods + Update(ControllerClient, *appsv1.StatefulSet, int32) (enterprisev1.ResourcePhase, error) + // PrepareScaleDown prepares pod to be removed via scale down event; it returns true when ready - PrepareScaleDown(n int32) (bool, error) + PrepareScaleDown(int32) (bool, error) // PrepareRecycle prepares pod to be recycled for updates; it returns true when ready - PrepareRecycle(n int32) (bool, error) + PrepareRecycle(int32) (bool, error) // FinishRecycle completes recycle event for pod and returns true, or returns false if nothing to do - FinishRecycle(n int32) (bool, error) + FinishRecycle(int32) (bool, error) } // DefaultStatefulSetPodManager is a simple StatefulSetPodManager that does nothing type DefaultStatefulSetPodManager struct{} +// Update for DefaultStatefulSetPodManager handles all updates for a statefulset of standard pods +func (mgr *DefaultStatefulSetPodManager) Update(client ControllerClient, statefulSet *appsv1.StatefulSet, desiredReplicas int32) (enterprisev1.ResourcePhase, error) { + phase, err := ApplyStatefulSet(client, statefulSet) + if err == nil && phase == enterprisev1.PhaseReady { + phase, err = UpdateStatefulSetPods(client, statefulSet, mgr, desiredReplicas) + } + return phase, err +} + // PrepareScaleDown for DefaultStatefulSetPodManager does nothing and returns true func (mgr *DefaultStatefulSetPodManager) PrepareScaleDown(n int32) (bool, error) { return true, nil @@ -121,9 +133,9 @@ func UpdateStatefulSetPods(c ControllerClient, statefulSet *appsv1.StatefulSet, if readyReplicas > desiredReplicas { // prepare pod for removal via scale down n := readyReplicas - 1 + podName := fmt.Sprintf("%s-%d", statefulSet.GetName(), n) ready, err := mgr.PrepareScaleDown(n) if err != nil { - podName := fmt.Sprintf("%s-%d", statefulSet.GetName(), n) scopedLog.Error(err, "Unable to decommission Pod", "podName", podName) return enterprisev1.PhaseError, err } @@ -135,7 +147,33 @@ func UpdateStatefulSetPods(c ControllerClient, statefulSet *appsv1.StatefulSet, // scale down statefulset to terminate pod scopedLog.Info("Scaling replicas down", "replicas", n) *statefulSet.Spec.Replicas = n - return enterprisev1.PhaseScalingDown, UpdateResource(c, statefulSet) + err = UpdateResource(c, statefulSet) + if err != nil { + scopedLog.Error(err, "Scale down update failed for StatefulSet") + return enterprisev1.PhaseError, err + } + + // delete PVCs used by the pod so that a future scale up will have clean state + for _, vol := range []string{"pvc-etc", "pvc-var"} { + namespacedName := types.NamespacedName{ + Namespace: statefulSet.GetNamespace(), + Name: fmt.Sprintf("%s-%s", vol, podName), + } + var pvc corev1.PersistentVolumeClaim + err := c.Get(context.TODO(), namespacedName, &pvc) + if err != nil { + scopedLog.Error(err, "Unable to find PVC for deletion", "pvcName", pvc.ObjectMeta.Name) + return enterprisev1.PhaseError, err + } + log.Info("Deleting PVC", "pvcName", pvc.ObjectMeta.Name) + err = c.Delete(context.Background(), &pvc) + if err != nil { + scopedLog.Error(err, "Unable to delete PVC", "pvcName", pvc.ObjectMeta.Name) + return enterprisev1.PhaseError, err + } + } + + return enterprisev1.PhaseScalingDown, nil } // ready and no StatefulSet scaling is required @@ -168,7 +206,7 @@ func UpdateStatefulSetPods(c ControllerClient, statefulSet *appsv1.StatefulSet, // deleting pod will cause StatefulSet controller to create a new one with latest template scopedLog.Info("Recycling Pod for updates", "podName", podName, - "statefulSetRevision", statefulSet.Status.CurrentRevision, + "statefulSetRevision", statefulSet.Status.UpdateRevision, "podRevision", pod.GetLabels()["controller-revision-hash"]) preconditions := client.Preconditions{UID: &pod.ObjectMeta.UID, ResourceVersion: &pod.ObjectMeta.ResourceVersion} err = c.Delete(context.Background(), &pod, preconditions) diff --git a/pkg/splunk/reconcile/statefulset_test.go b/pkg/splunk/reconcile/statefulset_test.go index e7dfa794a..e6d9a6889 100644 --- a/pkg/splunk/reconcile/statefulset_test.go +++ b/pkg/splunk/reconcile/statefulset_test.go @@ -15,10 +15,15 @@ package reconcile import ( + "errors" + "fmt" "testing" + enterprisev1 "github.com/splunk/splunk-operator/pkg/apis/enterprise/v1alpha2" appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" ) func TestApplyStatefulSet(t *testing.T) { @@ -26,7 +31,7 @@ func TestApplyStatefulSet(t *testing.T) { createCalls := map[string][]mockFuncCall{"Get": funcCalls, "Create": funcCalls} updateCalls := map[string][]mockFuncCall{"Get": funcCalls, "Update": funcCalls} var replicas int32 = 1 - current := appsv1.StatefulSet{ + current := &appsv1.StatefulSet{ ObjectMeta: metav1.ObjectMeta{ Name: "splunk-stack1-indexer", Namespace: "test", @@ -41,5 +46,143 @@ func TestApplyStatefulSet(t *testing.T) { _, err := ApplyStatefulSet(c, cr.(*appsv1.StatefulSet)) return err } - reconcileTester(t, "TestApplyStatefulSet", ¤t, revised, createCalls, updateCalls, reconcile) + reconcileTester(t, "TestApplyStatefulSet", current, revised, createCalls, updateCalls, reconcile) +} + +func podManagerUpdateTester(t *testing.T, method string, mgr StatefulSetPodManager, + desiredReplicas int32, wantPhase enterprisev1.ResourcePhase, statefulSet *appsv1.StatefulSet, + wantCalls map[string][]mockFuncCall, wantError error, initObjects ...runtime.Object) { + + // initialize client + c := newMockClient() + for _, obj := range initObjects { + c.state[getStateKey(obj)] = obj + } + + // test update + gotPhase, err := mgr.Update(c, statefulSet, desiredReplicas) + if (err == nil && wantError != nil) || + (err != nil && wantError == nil) || + (err != nil && wantError != nil && err.Error() != wantError.Error()) { + t.Errorf("%s returned error %v; want %v", method, err, wantError) + } + if gotPhase != wantPhase { + t.Errorf("%s returned phase=%s; want %s", method, gotPhase, wantPhase) + } + + // check calls + c.checkCalls(t, method, wantCalls) +} + +func podManagerTester(t *testing.T, method string, mgr StatefulSetPodManager) { + // test create + funcCalls := []mockFuncCall{{metaName: "*v1.StatefulSet-test-splunk-stack1"}} + createCalls := map[string][]mockFuncCall{"Get": funcCalls, "Create": funcCalls} + var replicas int32 = 1 + current := &appsv1.StatefulSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: "splunk-stack1", + Namespace: "test", + }, + Spec: appsv1.StatefulSetSpec{ + Replicas: &replicas, + }, + Status: appsv1.StatefulSetStatus{ + Replicas: replicas, + ReadyReplicas: replicas, + UpdatedReplicas: replicas, + UpdateRevision: "v1", + }, + } + podManagerUpdateTester(t, method, mgr, 1, enterprisev1.PhasePending, current, createCalls, nil) + + // test update + revised := current.DeepCopy() + revised.Spec.Template.ObjectMeta.Labels = map[string]string{"one": "two"} + updateCalls := map[string][]mockFuncCall{"Get": funcCalls, "Update": funcCalls} + methodPlus := fmt.Sprintf("%s(%s)", method, "Update StatefulSet") + podManagerUpdateTester(t, methodPlus, mgr, 1, enterprisev1.PhaseUpdating, revised, updateCalls, nil, current) + + // test scale up (zero ready so far; wait for ready) + revised = current.DeepCopy() + current.Status.ReadyReplicas = 0 + scaleUpCalls := map[string][]mockFuncCall{"Get": funcCalls} + methodPlus = fmt.Sprintf("%s(%s)", method, "ScalingUp, 0 ready") + podManagerUpdateTester(t, methodPlus, mgr, 1, enterprisev1.PhasePending, revised, scaleUpCalls, nil, current) + + // test scale up (1 ready scaling to 2; wait for ready) + replicas = 2 + current.Status.Replicas = 2 + current.Status.ReadyReplicas = 1 + methodPlus = fmt.Sprintf("%s(%s)", method, "ScalingUp, 1/2 ready") + podManagerUpdateTester(t, methodPlus, mgr, 2, enterprisev1.PhaseScalingUp, revised, scaleUpCalls, nil, current) + + // test scale up (1 ready scaling to 2) + replicas = 1 + current.Status.Replicas = 1 + current.Status.ReadyReplicas = 1 + methodPlus = fmt.Sprintf("%s(%s)", method, "ScalingUp, Update Replicas 1=>2") + podManagerUpdateTester(t, methodPlus, mgr, 2, enterprisev1.PhaseScalingUp, revised, updateCalls, nil, current) + + // test scale down (2 ready, 1 desired) + replicas = 1 + current.Status.Replicas = 1 + current.Status.ReadyReplicas = 2 + methodPlus = fmt.Sprintf("%s(%s)", method, "ScalingDown, Ready > Replicas") + podManagerUpdateTester(t, methodPlus, mgr, 1, enterprisev1.PhaseScalingDown, revised, scaleUpCalls, nil, current) + + // test scale down (2 ready scaling down to 1) + pvcCalls := []mockFuncCall{ + {metaName: "*v1.PersistentVolumeClaim-test-pvc-etc-splunk-stack1-1"}, + {metaName: "*v1.PersistentVolumeClaim-test-pvc-var-splunk-stack1-1"}, + } + scaleDownCalls := map[string][]mockFuncCall{ + "Get": {funcCalls[0], pvcCalls[0], pvcCalls[1]}, + "Update": {funcCalls[0]}, + "Delete": pvcCalls, + } + pvcList := []*corev1.PersistentVolumeClaim{ + {ObjectMeta: metav1.ObjectMeta{Name: "pvc-etc-splunk-stack1-1", Namespace: "test"}}, + {ObjectMeta: metav1.ObjectMeta{Name: "pvc-var-splunk-stack1-1", Namespace: "test"}}, + } + replicas = 2 + current.Status.Replicas = 2 + current.Status.ReadyReplicas = 2 + methodPlus = fmt.Sprintf("%s(%s)", method, "ScalingDown, Update Replicas 2=>1") + podManagerUpdateTester(t, methodPlus, mgr, 1, enterprisev1.PhaseScalingDown, revised, scaleDownCalls, nil, current, pvcList[0], pvcList[1]) + + // test pod not found + replicas = 1 + current.Status.Replicas = 1 + current.Status.ReadyReplicas = 1 + podCalls := []mockFuncCall{funcCalls[0], {metaName: "*v1.Pod-test-splunk-stack1-0"}} + getPodCalls := map[string][]mockFuncCall{"Get": podCalls} + methodPlus = fmt.Sprintf("%s(%s)", method, "Pod not found") + podManagerUpdateTester(t, methodPlus, mgr, 1, enterprisev1.PhaseError, revised, getPodCalls, errors.New("NotFound"), current) + + // test pod updated + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "splunk-stack1-0", + Namespace: "test", + Labels: map[string]string{ + "controller-revision-hash": "v0", + }, + }, + } + updatePodCalls := map[string][]mockFuncCall{"Get": podCalls, "Delete": {podCalls[1]}} + methodPlus = fmt.Sprintf("%s(%s)", method, "Recycle pod") + podManagerUpdateTester(t, methodPlus, mgr, 1, enterprisev1.PhaseUpdating, revised, updatePodCalls, nil, current, pod) + + // test all pods ready + pod.ObjectMeta.Labels["controller-revision-hash"] = "v1" + methodPlus = fmt.Sprintf("%s(%s)", method, "All pods ready") + podManagerUpdateTester(t, methodPlus, mgr, 1, enterprisev1.PhaseReady, revised, getPodCalls, nil, current, pod) +} + +func TestDefaultStatefulSetPodManager(t *testing.T) { + // test for updating + mgr := DefaultStatefulSetPodManager{} + method := "DefaultStatefulSetPodManager.Update" + podManagerTester(t, method, &mgr) } diff --git a/pkg/splunk/reconcile/util_test.go b/pkg/splunk/reconcile/util_test.go index 9e194de0b..96076ac15 100644 --- a/pkg/splunk/reconcile/util_test.go +++ b/pkg/splunk/reconcile/util_test.go @@ -16,6 +16,7 @@ package reconcile import ( "context" + "errors" "fmt" "reflect" "testing" @@ -36,10 +37,14 @@ func copyResource(dst runtime.Object, src runtime.Object) { *dst.(*corev1.ConfigMap) = *src.(*corev1.ConfigMap) case *corev1.Secret: *dst.(*corev1.Secret) = *src.(*corev1.Secret) + case *corev1.PersistentVolumeClaim: + *dst.(*corev1.PersistentVolumeClaim) = *src.(*corev1.PersistentVolumeClaim) case *corev1.PersistentVolumeClaimList: *dst.(*corev1.PersistentVolumeClaimList) = *src.(*corev1.PersistentVolumeClaimList) case *corev1.Service: *dst.(*corev1.Service) = *src.(*corev1.Service) + case *corev1.Pod: + *dst.(*corev1.Pod) = *src.(*corev1.Pod) case *appsv1.Deployment: *dst.(*appsv1.Deployment) = *src.(*appsv1.Deployment) case *appsv1.StatefulSet: @@ -124,6 +129,9 @@ type mockClient struct { // calls is a record of all mockClient function calls calls map[string][]mockFuncCall + + // error returned when an object is not found + notFoundError error } // Get returns mock client's err field @@ -138,7 +146,7 @@ func (c mockClient) Get(ctx context.Context, key client.ObjectKey, obj runtime.O copyResource(obj, getObj.(runtime.Object)) return nil } - return fmt.Errorf("NotFound") + return c.notFoundError } // List returns mock client's err field @@ -153,7 +161,7 @@ func (c mockClient) List(ctx context.Context, obj runtime.Object, opts ...client copyResource(obj, listObj.(runtime.Object)) return nil } - return fmt.Errorf("NotFound") + return c.notFoundError } // Create returns mock client's err field @@ -214,6 +222,11 @@ func (c *mockClient) resetCalls() { c.calls = make(map[string][]mockFuncCall) } +// resetState resets the state of the mockClient +func (c *mockClient) resetState() { + c.state = make(map[string]interface{}) +} + // checkCalls verifies that the wanted function calls were performed func (c *mockClient) checkCalls(t *testing.T, testname string, wantCalls map[string][]mockFuncCall) { notEmptyWantCalls := 0 @@ -265,8 +278,9 @@ func (c *mockClient) checkCalls(t *testing.T, testname string, wantCalls map[str // newMockClient is used to create and initialize a new mock client func newMockClient() *mockClient { c := &mockClient{ - state: make(map[string]interface{}), - calls: make(map[string][]mockFuncCall), + state: make(map[string]interface{}), + calls: make(map[string][]mockFuncCall), + notFoundError: errors.New("NotFound"), } return c } diff --git a/pkg/splunk/test/client.go b/pkg/splunk/test/client.go new file mode 100644 index 000000000..796650492 --- /dev/null +++ b/pkg/splunk/test/client.go @@ -0,0 +1,95 @@ +// 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 test + +import ( + "errors" + "fmt" + "io/ioutil" + "net/http" + "reflect" + "strings" + "testing" +) + +// MockHTTPHandler is used to handle an HTTP request for a given URL +type MockHTTPHandler struct { + Method string + URL string + Status int + Err error + Body string +} + +// MockHTTPClient is used to replicate an http.Client for unit tests +type MockHTTPClient struct { + WantRequests []*http.Request + GotRequests []*http.Request + Handlers map[string]MockHTTPHandler +} + +// getHandlerKey method for MockHTTPClient returns map key for a HTTP request +func (c *MockHTTPClient) getHandlerKey(req *http.Request) string { + return fmt.Sprintf("%s %s", req.Method, req.URL.String()) +} + +// Do method for MockHTTPClient just tracks the requests that it receives +func (c *MockHTTPClient) Do(req *http.Request) (*http.Response, error) { + c.GotRequests = append(c.GotRequests, req) + rsp, ok := c.Handlers[c.getHandlerKey(req)] + if !ok { + return nil, errors.New("NotFound") + } + httpResponse := http.Response{ + StatusCode: rsp.Status, + Body: ioutil.NopCloser(strings.NewReader(rsp.Body)), + } + return &httpResponse, rsp.Err +} + +// AddHandler method for MockHTTPClient adds a wanted request and response to use for it +func (c *MockHTTPClient) AddHandler(req *http.Request, status int, body string, err error) { + c.WantRequests = append(c.WantRequests, req) + if c.Handlers == nil { + c.Handlers = make(map[string]MockHTTPHandler) + } + c.Handlers[c.getHandlerKey(req)] = MockHTTPHandler{ + Method: req.Method, + URL: req.URL.String(), + Status: status, + Err: err, + Body: body, + } +} + +// AddHandlers method for MockHTTPClient adds a wanted requests and responses +func (c *MockHTTPClient) AddHandlers(handlers ...MockHTTPHandler) { + for n := range handlers { + req, _ := http.NewRequest(handlers[n].Method, handlers[n].URL, nil) + c.AddHandler(req, handlers[n].Status, handlers[n].Body, handlers[n].Err) + } +} + +// CheckRequests method for MockHTTPClient checks if requests received matches requests that we want +func (c *MockHTTPClient) CheckRequests(t *testing.T, testMethod string) { + if len(c.GotRequests) != len(c.WantRequests) { + t.Fatalf("%s got %d Requests; want %d", testMethod, len(c.GotRequests), len(c.WantRequests)) + } + for n := range c.GotRequests { + if !reflect.DeepEqual(c.GotRequests[n].URL.String(), c.WantRequests[n].URL.String()) { + t.Errorf("%s GotRequests[%d]=%v; want %v", testMethod, n, c.GotRequests[n].URL.String(), c.WantRequests[n].URL.String()) + } + } +} diff --git a/pkg/splunk/test/doc.go b/pkg/splunk/test/doc.go new file mode 100644 index 000000000..01dec0154 --- /dev/null +++ b/pkg/splunk/test/doc.go @@ -0,0 +1,18 @@ +// 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 test includes common code used for testing other modules. +*/ +package test From 1750eba2788e750e22fb569c3d7bf59b45a4bbb0 Mon Sep 17 00:00:00 2001 From: Mike Dickey Date: Thu, 19 Mar 2020 18:46:30 -0700 Subject: [PATCH 7/7] Updated ChangeLog for 0.1.0 release --- docs/ChangeLog.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/ChangeLog.md b/docs/ChangeLog.md index a76a7854e..3a0589dd1 100644 --- a/docs/ChangeLog.md +++ b/docs/ChangeLog.md @@ -1,6 +1,6 @@ # Splunk Operator for Kubernetes Change Log -## 0.1.0 Alpha (2020-??-??) +## 0.1.0 Alpha (2020-03-20) * This release depends upon changes made concurrently in the Splunk Enterprise container images. You must use the latest splunk/splunk:edge @@ -19,7 +19,7 @@ SearchHeadCluster and IndexerCluster resources. This helps protect against data loss and maximizes availability while changes are being made. You can now also use the "kubectl scale" command, and Horizontal Pod Autoscalers - with all resources, except LicenseMaster which always uses a single Pod. + with all resources (except LicenseMaster, which always uses a single Pod). * A new serviceTemplate spec parameter has been added for all Splunk Enterprise custom resources. This may be used to define a template the operator uses for @@ -36,7 +36,7 @@ cluster master warnings about using the default value. * Integrated with CircleCI and Coverall for CICD and code coverage, and - added a bunch of unit tests to bring coverage up to 93%. + added a bunch of unit tests to bring coverage up to over 90%. ## 0.0.6 Alpha (2019-12-12)