├── config ├── webhook │ ├── manifests.yaml │ ├── kustomization.yaml │ ├── service.yaml │ └── kustomizeconfig.yaml ├── certmanager │ ├── kustomization.yaml │ ├── kustomizeconfig.yaml │ └── certificate.yaml ├── samples │ ├── solr_v1beta1_solrcloud.yaml │ ├── solr_v1beta1_solrbackup.yaml │ ├── solr_v1beta1_solrcollection.yaml │ ├── solr_v1beta1_solrprometheusexporter.yaml │ └── solr_v1beta1_solrcollectionalias.yaml ├── manager │ ├── kustomization.yaml │ └── manager.yaml ├── rbac │ ├── auth_proxy_role_binding.yaml │ ├── role_binding.yaml │ ├── leader_election_role_binding.yaml │ ├── auth_proxy_role.yaml │ ├── kustomization.yaml │ ├── auth_proxy_service.yaml │ ├── leader_election_role.yaml │ └── role.yaml ├── crd │ ├── patches │ │ ├── cainjection_in_solrbackups.yaml │ │ ├── cainjection_in_solrclouds.yaml │ │ ├── cainjection_in_solrcollections.yaml │ │ ├── cainjection_in_solrcollectionaliases.yaml │ │ ├── cainjection_in_solrprometheusexporters.yaml │ │ ├── webhook_in_solrclouds.yaml │ │ ├── webhook_in_solrbackups.yaml │ │ ├── webhook_in_solrcollections.yaml │ │ ├── webhook_in_solrcollectionaliases.yaml │ │ └── webhook_in_solrprometheusexporters.yaml │ ├── kustomizeconfig.yaml │ ├── kustomization.yaml │ └── bases │ │ ├── solr.bloomberg.com_solrcollectionaliases.yaml │ │ └── solr.bloomberg.com_solrcollections.yaml ├── default │ ├── manager_prometheus_metrics_patch.yaml │ ├── manager_webhook_patch.yaml │ ├── webhookcainjection_patch.yaml │ ├── manager_auth_proxy_patch.yaml │ └── kustomization.yaml └── crds │ └── solr_v1beta1_solrcloud.yaml ├── docker_deploy.sh ├── example ├── test_solrbackup.yaml ├── test_solrcollection_alias.yaml ├── test_solrcloud_private_repo.yaml ├── rbac │ ├── cluster-role-binding-template.yaml │ └── cluster-role-template.yaml ├── test_solrprometheusexporter.yaml ├── test_solrcloud.yaml ├── test_solrcollection.yaml └── ext_ops.yaml ├── PROJECT ├── .gitignore ├── hack ├── install_dependencies.sh ├── check_license.sh ├── check_format.sh └── boilerplate.go.txt ├── go.mod ├── .travis.yml ├── Dockerfile ├── api └── v1beta1 │ ├── groupversion_info.go │ ├── solrcollectionalias_types.go │ ├── solrcollection_types.go │ ├── solrprometheusexporter_types.go │ └── solrbackup_types.go ├── Makefile ├── controllers ├── suite_test.go ├── solrcollectionalias_controller.go ├── solrcollection_controller.go ├── solrprometheusexporter_controller.go ├── util │ ├── prometheus_exporter_util.go │ ├── collection_util.go │ ├── zk_util.go │ └── backup_util.go └── solrbackup_controller.go ├── main.go ├── LICENSE └── README.md /config/webhook/manifests.yaml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docker_deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | docker login -u "$DOCKER_USER" -p "$DOCKER_PASSWORD" \ 4 | && make docker-push -------------------------------------------------------------------------------- /config/certmanager/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - certificate.yaml 3 | 4 | configurations: 5 | - kustomizeconfig.yaml 6 | -------------------------------------------------------------------------------- /config/webhook/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - manifests.yaml 3 | - service.yaml 4 | 5 | configurations: 6 | - kustomizeconfig.yaml 7 | -------------------------------------------------------------------------------- /config/samples/solr_v1beta1_solrcloud.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: solr.bloomberg.com/v1beta1 2 | kind: SolrCloud 3 | metadata: 4 | name: solrcloud-sample 5 | spec: 6 | # Add fields here 7 | foo: bar 8 | -------------------------------------------------------------------------------- /config/samples/solr_v1beta1_solrbackup.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: solr.bloomberg.com/v1beta1 2 | kind: SolrBackup 3 | metadata: 4 | name: solrbackup-sample 5 | spec: 6 | # Add fields here 7 | foo: bar 8 | -------------------------------------------------------------------------------- /config/samples/solr_v1beta1_solrcollection.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: solr.bloomberg.com/v1beta1 2 | kind: SolrCollection 3 | metadata: 4 | name: solrcollection-sample 5 | spec: 6 | # Add fields here 7 | foo: bar 8 | -------------------------------------------------------------------------------- /config/manager/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - manager.yaml 3 | 4 | apiVersion: kustomize.config.k8s.io/v1beta1 5 | kind: Kustomization 6 | images: 7 | - name: controller 8 | newName: sepulworld/solr-operator 9 | -------------------------------------------------------------------------------- /config/samples/solr_v1beta1_solrprometheusexporter.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: solr.bloomberg.com/v1beta1 2 | kind: SolrPrometheusExporter 3 | metadata: 4 | name: solrprometheusexporter-sample 5 | spec: 6 | # Add fields here 7 | foo: bar 8 | -------------------------------------------------------------------------------- /example/test_solrbackup.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: solr.bloomberg.com/v1beta1 2 | kind: SolrBackup 3 | metadata: 4 | labels: 5 | controller-tools.k8s.io: "1.0" 6 | name: solrbackup-sample 7 | spec: 8 | # Add fields here 9 | foo: bar 10 | -------------------------------------------------------------------------------- /config/webhook/service.yaml: -------------------------------------------------------------------------------- 1 | 2 | apiVersion: v1 3 | kind: Service 4 | metadata: 5 | name: webhook-service 6 | namespace: system 7 | spec: 8 | ports: 9 | - port: 443 10 | targetPort: 9443 11 | selector: 12 | control-plane: controller-manager 13 | -------------------------------------------------------------------------------- /example/test_solrcollection_alias.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: solr.bloomberg.com/v1beta1 2 | kind: SolrCollectionAlias 3 | metadata: 4 | name: collection-alias-example 5 | spec: 6 | solrCloud: example 7 | aliasType: standard 8 | collections: 9 | - example-collection-1 10 | -------------------------------------------------------------------------------- /config/samples/solr_v1beta1_solrcollectionalias.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: solr.bloomberg.com/v1beta1 2 | kind: SolrCollectionAlias 3 | metadata: 4 | labels: 5 | controller-tools.k8s.io: "1.0" 6 | name: solrcollectionalias-sample 7 | spec: 8 | # Add fields here 9 | foo: bar 10 | -------------------------------------------------------------------------------- /example/test_solrcloud_private_repo.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: solr.bloomberg.com/v1beta1 2 | kind: SolrCloud 3 | metadata: 4 | name: example-private-repo-solr-image 5 | spec: 6 | replicas: 3 7 | solrImage: 8 | repository: myprivate-repo.jfrog.io/solr 9 | tag: 8.2.0 10 | imagePullSecret: "k8s-docker-registry-secret" 11 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | name: proxy-rolebinding 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: ClusterRole 8 | name: proxy-role 9 | subjects: 10 | - kind: ServiceAccount 11 | name: default 12 | namespace: system 13 | -------------------------------------------------------------------------------- /config/rbac/role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | name: solr-operator-rolebinding 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: ClusterRole 8 | name: solr-operator-role 9 | subjects: 10 | - kind: ServiceAccount 11 | name: default 12 | namespace: system 13 | -------------------------------------------------------------------------------- /config/rbac/leader_election_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: RoleBinding 3 | metadata: 4 | name: leader-election-rolebinding 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: Role 8 | name: leader-election-role 9 | subjects: 10 | - kind: ServiceAccount 11 | name: default 12 | namespace: system 13 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_role.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: proxy-role 5 | rules: 6 | - apiGroups: ["authentication.k8s.io"] 7 | resources: 8 | - tokenreviews 9 | verbs: ["create"] 10 | - apiGroups: ["authorization.k8s.io"] 11 | resources: 12 | - subjectaccessreviews 13 | verbs: ["create"] 14 | -------------------------------------------------------------------------------- /example/rbac/cluster-role-binding-template.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1beta1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | name: 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: ClusterRole 8 | name: 9 | subjects: 10 | - kind: ServiceAccount 11 | name: default 12 | namespace: 13 | -------------------------------------------------------------------------------- /example/test_solrprometheusexporter.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: solr.bloomberg.com/v1beta1 2 | kind: SolrPrometheusExporter 3 | metadata: 4 | labels: 5 | controller-tools.k8s.io: "1.0" 6 | name: solrprometheusexporter-sample 7 | spec: 8 | solrReference: 9 | standalone: 10 | address: "http://example-solrcloud-common.default/solr" 11 | image: 12 | tag: 8.2.0 13 | -------------------------------------------------------------------------------- /config/crd/patches/cainjection_in_solrbackups.yaml: -------------------------------------------------------------------------------- 1 | # The following patch adds a directive for certmanager to inject CA into the CRD 2 | # CRD conversion requires k8s 1.13 or later. 3 | apiVersion: apiextensions.k8s.io/v1beta1 4 | kind: CustomResourceDefinition 5 | metadata: 6 | annotations: 7 | certmanager.k8s.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) 8 | name: solrbackups.solr.bloomberg.com 9 | -------------------------------------------------------------------------------- /config/crd/patches/cainjection_in_solrclouds.yaml: -------------------------------------------------------------------------------- 1 | # The following patch adds a directive for certmanager to inject CA into the CRD 2 | # CRD conversion requires k8s 1.13 or later. 3 | apiVersion: apiextensions.k8s.io/v1beta1 4 | kind: CustomResourceDefinition 5 | metadata: 6 | annotations: 7 | certmanager.k8s.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) 8 | name: solrclouds.solr.bloomberg.com 9 | -------------------------------------------------------------------------------- /config/rbac/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - role.yaml 3 | - role_binding.yaml 4 | - leader_election_role.yaml 5 | - leader_election_role_binding.yaml 6 | # Comment the following 3 lines if you want to disable 7 | # the auth proxy (https://github.com/brancz/kube-rbac-proxy) 8 | # which protects your /metrics endpoint. 9 | - auth_proxy_service.yaml 10 | - auth_proxy_role.yaml 11 | - auth_proxy_role_binding.yaml 12 | -------------------------------------------------------------------------------- /config/crd/patches/cainjection_in_solrcollections.yaml: -------------------------------------------------------------------------------- 1 | # The following patch adds a directive for certmanager to inject CA into the CRD 2 | # CRD conversion requires k8s 1.13 or later. 3 | apiVersion: apiextensions.k8s.io/v1beta1 4 | kind: CustomResourceDefinition 5 | metadata: 6 | annotations: 7 | certmanager.k8s.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) 8 | name: solrcollections.solr.bloomberg.com 9 | -------------------------------------------------------------------------------- /config/crd/patches/cainjection_in_solrcollectionaliases.yaml: -------------------------------------------------------------------------------- 1 | # The following patch adds a directive for certmanager to inject CA into the CRD 2 | # CRD conversion requires k8s 1.13 or later. 3 | apiVersion: apiextensions.k8s.io/v1beta1 4 | kind: CustomResourceDefinition 5 | metadata: 6 | annotations: 7 | certmanager.k8s.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) 8 | name: solrcollectionaliases.solr.bloomberg.com 9 | -------------------------------------------------------------------------------- /config/crd/patches/cainjection_in_solrprometheusexporters.yaml: -------------------------------------------------------------------------------- 1 | # The following patch adds a directive for certmanager to inject CA into the CRD 2 | # CRD conversion requires k8s 1.13 or later. 3 | apiVersion: apiextensions.k8s.io/v1beta1 4 | kind: CustomResourceDefinition 5 | metadata: 6 | annotations: 7 | certmanager.k8s.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) 8 | name: solrprometheusexporters.solr.bloomberg.com 9 | -------------------------------------------------------------------------------- /PROJECT: -------------------------------------------------------------------------------- 1 | version: "2" 2 | domain: bloomberg.com 3 | repo: github.com/bloomberg/solr-operator 4 | resources: 5 | - group: solr 6 | version: v1beta1 7 | kind: SolrCloud 8 | - group: solr 9 | version: v1beta1 10 | kind: SolrBackup 11 | - group: solr 12 | version: v1beta1 13 | kind: SolrCollection 14 | - group: solr 15 | version: v1beta1 16 | kind: SolrPrometheusExporter 17 | - group: solr 18 | version: v1beta1 19 | kind: SolrCollectionAlias 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Binaries for programs and plugins 3 | *.exe 4 | *.exe~ 5 | *.dll 6 | *.so 7 | *.dylib 8 | bin 9 | 10 | # Test binary, build with `go test -c` 11 | *.test 12 | 13 | # Output of the go coverage tool, specifically when used with LiteIDE 14 | *.out 15 | 16 | # Kubernetes Generated files - skip generated files, except for vendored files 17 | 18 | !vendor/**/zz_generated.* 19 | 20 | # editor and IDE paraphernalia 21 | .idea 22 | *.swp 23 | *.swo 24 | *~ 25 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | annotations: 5 | prometheus.io/port: "8443" 6 | prometheus.io/scheme: https 7 | prometheus.io/scrape: "true" 8 | labels: 9 | control-plane: controller-manager 10 | name: controller-manager-metrics-service 11 | namespace: system 12 | spec: 13 | ports: 14 | - name: https 15 | port: 8443 16 | targetPort: https 17 | selector: 18 | control-plane: controller-manager 19 | -------------------------------------------------------------------------------- /config/certmanager/kustomizeconfig.yaml: -------------------------------------------------------------------------------- 1 | # This configuration is for teaching kustomize how to update name ref and var substitution 2 | nameReference: 3 | - kind: Issuer 4 | group: certmanager.k8s.io 5 | fieldSpecs: 6 | - kind: Certificate 7 | group: certmanager.k8s.io 8 | path: spec/issuerRef/name 9 | 10 | varReference: 11 | - kind: Certificate 12 | group: certmanager.k8s.io 13 | path: spec/commonName 14 | - kind: Certificate 15 | group: certmanager.k8s.io 16 | path: spec/dnsNames 17 | -------------------------------------------------------------------------------- /hack/install_dependencies.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -o errexit 4 | set -o nounset 5 | set -o pipefail 6 | 7 | kubebuilder_version=2.1.0 8 | os=$(go env GOOS) 9 | arch=$(go env GOARCH) 10 | 11 | # Install go modules 12 | GO111MODULE=on go mod tidy 13 | 14 | # Install Kubebuilder 15 | curl -sL https://go.kubebuilder.io/dl/${kubebuilder_version}/${os}/${arch} | tar -xz -C /tmp/ 16 | sudo mv /tmp/kubebuilder_${kubebuilder_version}_${os}_${arch} /usr/local/kubebuilder 17 | export PATH=$PATH:/usr/local/kubebuilder/bin 18 | -------------------------------------------------------------------------------- /config/default/manager_prometheus_metrics_patch.yaml: -------------------------------------------------------------------------------- 1 | # This patch enables Prometheus scraping for the manager pod. 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | metadata: 5 | name: solr-operator 6 | spec: 7 | template: 8 | metadata: 9 | annotations: 10 | prometheus.io/scrape: 'true' 11 | spec: 12 | containers: 13 | # Expose the prometheus metrics on default port 14 | - name: solr-operator 15 | ports: 16 | - containerPort: 8080 17 | name: metrics 18 | protocol: TCP 19 | -------------------------------------------------------------------------------- /hack/check_license.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # exit immediately when a command fails 3 | set -e 4 | # only exit with zero if all commands of the pipeline exit successfully 5 | set -o pipefail 6 | # error on unset variables 7 | set -u 8 | 9 | licRes=$( 10 | find . -type f -iname '*.go' ! -path '*/vendor/*' -exec \ 11 | sh -c 'head -n3 $1 | grep -Eq "(Copyright|generated|GENERATED)" || echo -e $1' {} {} \; 12 | ) 13 | 14 | if [ -n "${licRes}" ]; then 15 | echo -e "license header checking failed:\\n${licRes}" 16 | exit 255 17 | fi 18 | -------------------------------------------------------------------------------- /config/crd/kustomizeconfig.yaml: -------------------------------------------------------------------------------- 1 | # This file is for teaching kustomize how to substitute name and namespace reference in CRD 2 | nameReference: 3 | - kind: Service 4 | version: v1 5 | fieldSpecs: 6 | - kind: CustomResourceDefinition 7 | group: apiextensions.k8s.io 8 | path: spec/conversion/webhookClientConfig/service/name 9 | 10 | namespace: 11 | - kind: CustomResourceDefinition 12 | group: apiextensions.k8s.io 13 | path: spec/conversion/webhookClientConfig/service/namespace 14 | create: false 15 | 16 | varReference: 17 | - path: metadata/annotations 18 | -------------------------------------------------------------------------------- /hack/check_format.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # exit immediately when a command fails 3 | set -e 4 | # only exit with zero if all commands of the pipeline exit successfully 5 | set -o pipefail 6 | # error on unset variables 7 | set -u 8 | 9 | goFiles=$(find . -name \*.go -not -path "./vendor/*" -print) 10 | invalidFiles=$(gofmt -l $goFiles) 11 | 12 | if [ "$invalidFiles" ]; then 13 | echo -e "These files did not pass the 'go fmt' check, please run 'go fmt' on them:" 14 | for file in $invalidFiles 15 | do 16 | echo "" 17 | gofmt -d $file 18 | done 19 | 20 | exit 1 21 | fi 22 | -------------------------------------------------------------------------------- /config/rbac/leader_election_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions to do leader election. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: Role 4 | metadata: 5 | name: leader-election-role 6 | rules: 7 | - apiGroups: 8 | - "" 9 | resources: 10 | - configmaps 11 | verbs: 12 | - get 13 | - list 14 | - watch 15 | - create 16 | - update 17 | - patch 18 | - delete 19 | - apiGroups: 20 | - "" 21 | resources: 22 | - configmaps/status 23 | verbs: 24 | - get 25 | - update 26 | - patch 27 | - apiGroups: 28 | - "" 29 | resources: 30 | - events 31 | verbs: 32 | - create 33 | -------------------------------------------------------------------------------- /config/default/manager_webhook_patch.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: solr-operator 5 | spec: 6 | template: 7 | spec: 8 | containers: 9 | - name: solr-operator 10 | ports: 11 | - containerPort: 9443 12 | name: webhook-server 13 | protocol: TCP 14 | volumeMounts: 15 | - mountPath: /tmp/k8s-webhook-server/serving-certs 16 | name: cert 17 | readOnly: true 18 | volumes: 19 | - name: cert 20 | secret: 21 | defaultMode: 420 22 | secretName: webhook-server-secret 23 | -------------------------------------------------------------------------------- /hack/boilerplate.go.txt: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Bloomberg Finance LP. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | -------------------------------------------------------------------------------- /config/default/webhookcainjection_patch.yaml: -------------------------------------------------------------------------------- 1 | # This patch add annotation to admission webhook config and 2 | # the variables $(CERTIFICATE_NAMESPACE) and $(CERTIFICATE_NAME) will be substituted by kustomize. 3 | apiVersion: admissionregistration.k8s.io/v1beta1 4 | kind: MutatingWebhookConfiguration 5 | metadata: 6 | name: mutating-webhook-configuration 7 | annotations: 8 | certmanager.k8s.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) 9 | --- 10 | apiVersion: admissionregistration.k8s.io/v1beta1 11 | kind: ValidatingWebhookConfiguration 12 | metadata: 13 | name: validating-webhook-configuration 14 | annotations: 15 | certmanager.k8s.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) 16 | -------------------------------------------------------------------------------- /config/crd/patches/webhook_in_solrclouds.yaml: -------------------------------------------------------------------------------- 1 | # The following patch enables conversion webhook for CRD 2 | # CRD conversion requires k8s 1.13 or later. 3 | apiVersion: apiextensions.k8s.io/v1beta1 4 | kind: CustomResourceDefinition 5 | metadata: 6 | name: solrclouds.solr.bloomberg.com 7 | spec: 8 | conversion: 9 | strategy: Webhook 10 | webhookClientConfig: 11 | # this is "\n" used as a placeholder, otherwise it will be rejected by the apiserver for being blank, 12 | # but we're going to set it later using the cert-manager (or potentially a patch if not using cert-manager) 13 | caBundle: Cg== 14 | service: 15 | namespace: system 16 | name: webhook-service 17 | path: /convert 18 | -------------------------------------------------------------------------------- /config/crd/patches/webhook_in_solrbackups.yaml: -------------------------------------------------------------------------------- 1 | # The following patch enables conversion webhook for CRD 2 | # CRD conversion requires k8s 1.13 or later. 3 | apiVersion: apiextensions.k8s.io/v1beta1 4 | kind: CustomResourceDefinition 5 | metadata: 6 | name: solrbackups.solr.bloomberg.com 7 | spec: 8 | conversion: 9 | strategy: Webhook 10 | webhookClientConfig: 11 | # this is "\n" used as a placeholder, otherwise it will be rejected by the apiserver for being blank, 12 | # but we're going to set it later using the cert-manager (or potentially a patch if not using cert-manager) 13 | caBundle: Cg== 14 | service: 15 | namespace: system 16 | name: webhook-service 17 | path: /convert 18 | -------------------------------------------------------------------------------- /config/crd/patches/webhook_in_solrcollections.yaml: -------------------------------------------------------------------------------- 1 | # The following patch enables conversion webhook for CRD 2 | # CRD conversion requires k8s 1.13 or later. 3 | apiVersion: apiextensions.k8s.io/v1beta1 4 | kind: CustomResourceDefinition 5 | metadata: 6 | name: solrcollections.solr.bloomberg.com 7 | spec: 8 | conversion: 9 | strategy: Webhook 10 | webhookClientConfig: 11 | # this is "\n" used as a placeholder, otherwise it will be rejected by the apiserver for being blank, 12 | # but we're going to set it later using the cert-manager (or potentially a patch if not using cert-manager) 13 | caBundle: Cg== 14 | service: 15 | namespace: system 16 | name: webhook-service 17 | path: /convert 18 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/bloomberg/solr-operator 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/coreos/etcd-operator v0.9.3 7 | github.com/docker/spdystream v0.0.0-20181023171402-6480d4af844c // indirect 8 | github.com/elazarl/goproxy v0.0.0-20191011121108-aa519ddbe484 // indirect 9 | github.com/go-logr/logr v0.1.0 10 | github.com/onsi/ginkgo v1.8.0 11 | github.com/onsi/gomega v1.5.0 12 | github.com/pravega/zookeeper-operator v0.0.0-20190125200339-95e80d5de229 13 | k8s.io/api v0.0.0-20191016110246-af539daaa43a 14 | k8s.io/apimachinery v0.0.0-20191004115701-31ade1b30762 15 | k8s.io/client-go v0.0.0-20191016110837-54936ba21026 16 | sigs.k8s.io/controller-runtime v0.3.0 17 | sigs.k8s.io/controller-tools v0.2.2 // indirect 18 | ) 19 | -------------------------------------------------------------------------------- /config/crd/patches/webhook_in_solrcollectionaliases.yaml: -------------------------------------------------------------------------------- 1 | # The following patch enables conversion webhook for CRD 2 | # CRD conversion requires k8s 1.13 or later. 3 | apiVersion: apiextensions.k8s.io/v1beta1 4 | kind: CustomResourceDefinition 5 | metadata: 6 | name: solrcollectionaliases.solr.bloomberg.com 7 | spec: 8 | conversion: 9 | strategy: Webhook 10 | webhookClientConfig: 11 | # this is "\n" used as a placeholder, otherwise it will be rejected by the apiserver for being blank, 12 | # but we're going to set it later using the cert-manager (or potentially a patch if not using cert-manager) 13 | caBundle: Cg== 14 | service: 15 | namespace: system 16 | name: webhook-service 17 | path: /convert 18 | -------------------------------------------------------------------------------- /config/crd/patches/webhook_in_solrprometheusexporters.yaml: -------------------------------------------------------------------------------- 1 | # The following patch enables conversion webhook for CRD 2 | # CRD conversion requires k8s 1.13 or later. 3 | apiVersion: apiextensions.k8s.io/v1beta1 4 | kind: CustomResourceDefinition 5 | metadata: 6 | name: solrprometheusexporters.solr.bloomberg.com 7 | spec: 8 | conversion: 9 | strategy: Webhook 10 | webhookClientConfig: 11 | # this is "\n" used as a placeholder, otherwise it will be rejected by the apiserver for being blank, 12 | # but we're going to set it later using the cert-manager (or potentially a patch if not using cert-manager) 13 | caBundle: Cg== 14 | service: 15 | namespace: system 16 | name: webhook-service 17 | path: /convert 18 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | services: 3 | - docker 4 | 5 | go: 6 | - "1.13" 7 | - master 8 | 9 | script: 10 | - docker --version 11 | - ./hack/install_dependencies.sh 12 | 13 | # build locally and run locally 14 | - make clean 15 | - make manifests 16 | - make build 17 | # - ./bin/manager --help 18 | - make test 19 | - make manifests-check 20 | 21 | jobs: 22 | include: 23 | - stage: "Docker" 24 | name: "Build (& Release if tagged)" 25 | script: 26 | - ./hack/install_dependencies.sh 27 | - make docker-build 28 | deploy: 29 | - provider: script 30 | script: bash docker_deploy.sh 31 | skip_cleanup: true 32 | on: 33 | tags: true 34 | condition: -n "$DOCKER_PASSWORD" 35 | -------------------------------------------------------------------------------- /example/rbac/cluster-role-template.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1beta1 2 | kind: ClusterRole 3 | metadata: 4 | name: 5 | rules: 6 | - apiGroups: 7 | - etcd.database.coreos.com 8 | resources: 9 | - etcdclusters 10 | - etcdbackups 11 | - etcdrestores 12 | verbs: 13 | - "*" 14 | - apiGroups: 15 | - apiextensions.k8s.io 16 | resources: 17 | - customresourcedefinitions 18 | verbs: 19 | - "*" 20 | - apiGroups: 21 | - "" 22 | resources: 23 | - pods 24 | - services 25 | - endpoints 26 | - persistentvolumeclaims 27 | - events 28 | verbs: 29 | - "*" 30 | - apiGroups: 31 | - apps 32 | resources: 33 | - deployments 34 | verbs: 35 | - "*" 36 | # The following permissions can be removed if not using S3 backup and TLS 37 | - apiGroups: 38 | - "" 39 | resources: 40 | - secrets 41 | verbs: 42 | - get 43 | -------------------------------------------------------------------------------- /config/default/manager_auth_proxy_patch.yaml: -------------------------------------------------------------------------------- 1 | # This patch inject a sidecar container which is a HTTP proxy for the controller manager, 2 | # it performs RBAC authorization against the Kubernetes API using SubjectAccessReviews. 3 | apiVersion: apps/v1 4 | kind: Deployment 5 | metadata: 6 | name: controller-manager 7 | namespace: system 8 | spec: 9 | template: 10 | spec: 11 | containers: 12 | - name: kube-rbac-proxy 13 | image: gcr.io/kubebuilder/kube-rbac-proxy:v0.4.0 14 | args: 15 | - "--secure-listen-address=0.0.0.0:8443" 16 | - "--upstream=http://127.0.0.1:8080/" 17 | - "--logtostderr=true" 18 | - "--v=10" 19 | ports: 20 | - containerPort: 8443 21 | name: https 22 | - name: manager 23 | args: 24 | - "--metrics-addr=127.0.0.1:8080" 25 | - "--enable-leader-election" 26 | -------------------------------------------------------------------------------- /config/webhook/kustomizeconfig.yaml: -------------------------------------------------------------------------------- 1 | # the following config is for teaching kustomize where to look at when substituting vars. 2 | # It requires kustomize v2.1.0 or newer to work properly. 3 | nameReference: 4 | - kind: Service 5 | version: v1 6 | fieldSpecs: 7 | - kind: MutatingWebhookConfiguration 8 | group: admissionregistration.k8s.io 9 | path: webhooks/clientConfig/service/name 10 | - kind: ValidatingWebhookConfiguration 11 | group: admissionregistration.k8s.io 12 | path: webhooks/clientConfig/service/name 13 | 14 | namespace: 15 | - kind: MutatingWebhookConfiguration 16 | group: admissionregistration.k8s.io 17 | path: webhooks/clientConfig/service/namespace 18 | create: true 19 | - kind: ValidatingWebhookConfiguration 20 | group: admissionregistration.k8s.io 21 | path: webhooks/clientConfig/service/namespace 22 | create: true 23 | 24 | varReference: 25 | - path: metadata/annotations 26 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build the manager binary 2 | FROM golang:1.13 as builder 3 | 4 | WORKDIR /workspace 5 | # Copy the Go Modules manifests 6 | COPY go.mod go.mod 7 | COPY go.sum go.sum 8 | # cache deps before building and copying source so that we don't need to re-download as much 9 | # and so that source changes don't invalidate our downloaded layer 10 | RUN go mod download 11 | 12 | # Copy the go source 13 | COPY main.go main.go 14 | COPY api/ api/ 15 | COPY controllers/ controllers/ 16 | 17 | # Build 18 | RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GO111MODULE=on go build -a -o bin/solr-operator main.go 19 | 20 | # Use distroless as minimal base image to package the manager binary 21 | # Refer to https://github.com/GoogleContainerTools/distroless for more details 22 | FROM gcr.io/distroless/static:nonroot 23 | WORKDIR / 24 | COPY --from=builder /workspace/bin/solr-operator . 25 | USER nonroot:nonroot 26 | 27 | ENTRYPOINT ["/solr-operator"] 28 | -------------------------------------------------------------------------------- /config/certmanager/certificate.yaml: -------------------------------------------------------------------------------- 1 | # The following manifests contain a self-signed issuer CR and a certificate CR. 2 | # More document can be found at https://docs.cert-manager.io 3 | apiVersion: certmanager.k8s.io/v1alpha1 4 | kind: Issuer 5 | metadata: 6 | name: selfsigned-issuer 7 | namespace: system 8 | spec: 9 | selfSigned: {} 10 | --- 11 | apiVersion: certmanager.k8s.io/v1alpha1 12 | kind: Certificate 13 | metadata: 14 | name: serving-cert # this name should match the one appeared in kustomizeconfig.yaml 15 | namespace: system 16 | spec: 17 | # $(SERVICE_NAME) and $(SERVICE_NAMESPACE) will be substituted by kustomize 18 | commonName: $(SERVICE_NAME).$(SERVICE_NAMESPACE).svc 19 | dnsNames: 20 | - $(SERVICE_NAME).$(SERVICE_NAMESPACE).svc.cluster.local 21 | issuerRef: 22 | kind: Issuer 23 | name: selfsigned-issuer 24 | secretName: webhook-server-cert # this secret will not be prefixed, since it's not managed by kustomize 25 | -------------------------------------------------------------------------------- /example/test_solrcloud.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: solr.bloomberg.com/v1beta1 2 | kind: SolrCloud 3 | metadata: 4 | name: example 5 | spec: 6 | replicas: 3 7 | solrImage: 8 | tag: 8.2.0 9 | solrJavaMem: "-Xms1g -Xmx3g" 10 | solrPodPolicy: 11 | resources: 12 | limits: 13 | memory: "1G" 14 | requests: 15 | cpu: "65m" 16 | memory: "156Mi" 17 | zookeeperRef: 18 | provided: 19 | zookeeper: 20 | persistentVolumeClaimSpec: 21 | storageClassName: "hostpath" 22 | resources: 23 | requests: 24 | storage: "5Gi" 25 | replicas: 1 26 | zookeeperPodPolicy: 27 | resources: 28 | limits: 29 | memory: "1G" 30 | requests: 31 | cpu: "65m" 32 | memory: "156Mi" 33 | solrOpts: "-Dsolr.autoSoftCommit.maxTime=10000" 34 | solrGCTune: "-XX:SurvivorRatio=4 -XX:TargetSurvivorRatio=90 -XX:MaxTenuringThreshold=8" 35 | -------------------------------------------------------------------------------- /config/manager/manager.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | metadata: 5 | name: solr-operator 6 | labels: 7 | control-plane: solr-operator 8 | spec: 9 | selector: 10 | matchLabels: 11 | control-plane: solr-operator 12 | replicas: 1 13 | template: 14 | metadata: 15 | labels: 16 | control-plane: solr-operator 17 | spec: 18 | containers: 19 | - args: 20 | - -zk-operator=true 21 | - -etcd-operator=false 22 | - -ingress-base-domain=ing.local.domain 23 | image: controller:0.1.5-3 24 | imagePullPolicy: Always 25 | name: solr-operator 26 | env: 27 | - name: POD_NAMESPACE 28 | valueFrom: 29 | fieldRef: 30 | fieldPath: metadata.namespace 31 | resources: 32 | limits: 33 | cpu: 200m 34 | memory: 30Mi 35 | requests: 36 | cpu: 100m 37 | memory: 20Mi 38 | terminationGracePeriodSeconds: 10 39 | -------------------------------------------------------------------------------- /example/test_solrcollection.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: solr.bloomberg.com/v1beta1 2 | kind: SolrCollection 3 | metadata: 4 | name: example-collection-1 5 | spec: 6 | solrCloud: example 7 | collection: example-collection 8 | routerName: compositeId 9 | autoAddReplicas: false 10 | numShards: 2 11 | replicationFactor: 1 12 | maxShardsPerNode: 1 13 | collectionConfigName: "_default" 14 | --- 15 | apiVersion: solr.bloomberg.com/v1beta1 16 | kind: SolrCollection 17 | metadata: 18 | name: example-collection-2-compositeid-autoadd 19 | spec: 20 | solrCloud: example 21 | collection: example-collection-2 22 | routerName: compositeId 23 | autoAddReplicas: true 24 | numShards: 2 25 | replicationFactor: 1 26 | maxShardsPerNode: 1 27 | collectionConfigName: "_default" 28 | --- 29 | apiVersion: solr.bloomberg.com/v1beta1 30 | kind: SolrCollection 31 | metadata: 32 | name: example-collection-3-implicit 33 | spec: 34 | solrCloud: example 35 | collection: example-collection-3-implicit 36 | routerName: implicit 37 | routerField: 'car' 38 | autoAddReplicas: true 39 | numShards: 2 40 | replicationFactor: 1 41 | maxShardsPerNode: 1 42 | shards: "fooshard1,fooshard2" 43 | collectionConfigName: "_default" -------------------------------------------------------------------------------- /api/v1beta1/groupversion_info.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Bloomberg Finance LP. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package v1beta1 contains API Schema definitions for the solr v1beta1 API group 18 | // +kubebuilder:object:generate=true 19 | // +groupName=solr.bloomberg.com 20 | package v1beta1 21 | 22 | import ( 23 | "k8s.io/apimachinery/pkg/runtime/schema" 24 | "sigs.k8s.io/controller-runtime/pkg/scheme" 25 | ) 26 | 27 | var ( 28 | // GroupVersion is group version used to register these objects 29 | GroupVersion = schema.GroupVersion{Group: "solr.bloomberg.com", Version: "v1beta1"} 30 | 31 | // SchemeBuilder is used to add go types to the GroupVersionKind scheme 32 | SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} 33 | 34 | // AddToScheme adds the types in this group-version to the given scheme. 35 | AddToScheme = SchemeBuilder.AddToScheme 36 | ) 37 | -------------------------------------------------------------------------------- /config/crd/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # This kustomization.yaml is not intended to be run by itself, 2 | # since it depends on service name and namespace that are out of this kustomize package. 3 | # It should be run by config/default 4 | resources: 5 | - bases/solr.bloomberg.com_solrclouds.yaml 6 | - bases/solr.bloomberg.com_solrbackups.yaml 7 | - bases/solr.bloomberg.com_solrcollections.yaml 8 | - bases/solr.bloomberg.com_solrprometheusexporters.yaml 9 | - bases/solr.bloomberg.com_solrcollectionaliases.yaml 10 | # +kubebuilder:scaffold:crdkustomizeresource 11 | 12 | patchesStrategicMerge: 13 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. 14 | # patches here are for enabling the conversion webhook for each CRD 15 | #- patches/webhook_in_solrclouds.yaml 16 | #- patches/webhook_in_solrbackups.yaml 17 | #- patches/webhook_in_solrcollections.yaml 18 | #- patches/webhook_in_solrprometheusexporters.yaml 19 | #- patches/webhook_in_solrcollectionaliases.yaml 20 | # +kubebuilder:scaffold:crdkustomizewebhookpatch 21 | 22 | # [CERTMANAGER] To enable webhook, uncomment all the sections with [CERTMANAGER] prefix. 23 | # patches here are for enabling the CA injection for each CRD 24 | #- patches/cainjection_in_solrclouds.yaml 25 | #- patches/cainjection_in_solrbackups.yaml 26 | #- patches/cainjection_in_solrcollections.yaml 27 | #- patches/cainjection_in_solrprometheusexporters.yaml 28 | #- patches/cainjection_in_solrcollectionaliases.yaml 29 | # +kubebuilder:scaffold:crdkustomizecainjectionpatch 30 | 31 | # the following config is for teaching kustomize how to do kustomization for CRDs. 32 | configurations: 33 | - kustomizeconfig.yaml 34 | -------------------------------------------------------------------------------- /api/v1beta1/solrcollectionalias_types.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Bloomberg Finance LP. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package v1beta1 18 | 19 | import ( 20 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 21 | ) 22 | 23 | // SolrCollectionAliasSpec defines the desired state of SolrCollectionAlias 24 | type SolrCollectionAliasSpec struct { 25 | // A reference to the SolrCloud to create alias on 26 | SolrCloud string `json:"solrCloud"` 27 | 28 | // Collections is a list of collections to apply alias to 29 | Collections []string `json:"collections"` 30 | 31 | // AliasType is a either standard or routed, right now we support standard 32 | AliasType string `json:"aliasType"` 33 | } 34 | 35 | // SolrCollectionAliasStatus defines the observed state of SolrCollectionAlias 36 | type SolrCollectionAliasStatus struct { 37 | // Time the alias was created 38 | // +optional 39 | CreatedTime *metav1.Time `json:"createdTime,omitempty"` 40 | 41 | // Created or not status 42 | // +optional 43 | Created bool `json:"created,omitempty"` 44 | 45 | // Associated collections to the alias 46 | // +optional 47 | Collections []string `json:"collections,omitempty"` 48 | } 49 | 50 | // +kubebuilder:object:root=true 51 | 52 | // SolrCollectionAlias is the Schema for the solrcollectionaliases API 53 | type SolrCollectionAlias struct { 54 | metav1.TypeMeta `json:",inline"` 55 | metav1.ObjectMeta `json:"metadata,omitempty"` 56 | 57 | Spec SolrCollectionAliasSpec `json:"spec,omitempty"` 58 | Status SolrCollectionAliasStatus `json:"status,omitempty"` 59 | } 60 | 61 | // +kubebuilder:object:root=true 62 | 63 | // SolrCollectionAliasList contains a list of SolrCollectionAlias 64 | type SolrCollectionAliasList struct { 65 | metav1.TypeMeta `json:",inline"` 66 | metav1.ListMeta `json:"metadata,omitempty"` 67 | Items []SolrCollectionAlias `json:"items"` 68 | } 69 | 70 | func init() { 71 | SchemeBuilder.Register(&SolrCollectionAlias{}, &SolrCollectionAliasList{}) 72 | } 73 | -------------------------------------------------------------------------------- /config/default/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # Adds namespace to all resources. 2 | namespace: default 3 | 4 | # Value of this field is prepended to the 5 | # names of all resources, e.g. a deployment named 6 | # "wordpress" becomes "alices-wordpress". 7 | # Note that it should also match with the prefix (text before '-') of the namespace 8 | # field above. 9 | # namePrefix: solr-operator- 10 | 11 | # Labels to add to all resources and selectors. 12 | #commonLabels: 13 | # someName: someValue 14 | 15 | bases: 16 | - ../crd 17 | - ../rbac 18 | - ../manager 19 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in crd/kustomization.yaml 20 | #- ../webhook 21 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required. 22 | #- ../certmanager 23 | 24 | patchesStrategicMerge: 25 | # Protect the /metrics endpoint by putting it behind auth. 26 | # Only one of manager_auth_proxy_patch.yaml and 27 | # manager_prometheus_metrics_patch.yaml should be enabled. 28 | #- manager_auth_proxy_patch.yaml 29 | # If you want your controller-manager to expose the /metrics 30 | # endpoint w/o any authn/z, uncomment the following line and 31 | # comment manager_auth_proxy_patch.yaml. 32 | # Only one of manager_auth_proxy_patch.yaml and 33 | # manager_prometheus_metrics_patch.yaml should be enabled. 34 | - manager_prometheus_metrics_patch.yaml 35 | 36 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in crd/kustomization.yaml 37 | #- manager_webhook_patch.yaml 38 | 39 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 40 | # Uncomment 'CERTMANAGER' sections in crd/kustomization.yaml to enable the CA injection in the admission webhooks. 41 | # 'CERTMANAGER' needs to be enabled to use ca injection 42 | #- webhookcainjection_patch.yaml 43 | 44 | # the following config is for teaching kustomize how to do var substitution 45 | vars: 46 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix. 47 | #- name: CERTIFICATE_NAMESPACE # namespace of the certificate CR 48 | # objref: 49 | # kind: Certificate 50 | # group: certmanager.k8s.io 51 | # version: v1alpha1 52 | # name: serving-cert # this name should match the one in certificate.yaml 53 | # fieldref: 54 | # fieldpath: metadata.namespace 55 | #- name: CERTIFICATE_NAME 56 | # objref: 57 | # kind: Certificate 58 | # group: certmanager.k8s.io 59 | # version: v1alpha1 60 | # name: serving-cert # this name should match the one in certificate.yaml 61 | #- name: SERVICE_NAMESPACE # namespace of the service 62 | # objref: 63 | # kind: Service 64 | # version: v1 65 | # name: webhook-service 66 | # fieldref: 67 | # fieldpath: metadata.namespace 68 | #- name: SERVICE_NAME 69 | # objref: 70 | # kind: Service 71 | # version: v1 72 | # name: webhook-service 73 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Produce CRDs that work back to Kubernetes 1.11 (no version conversion) 2 | CRD_OPTIONS ?= "crd" 3 | 4 | # Image URL to use all building/pushing image targets 5 | NAME ?= solr-operator 6 | NAMESPACE ?= bloomberg/ 7 | IMG = $(NAMESPACE)$(NAME) 8 | VERSION ?= 0.1.4 9 | GIT_SHA = $(shell git rev-parse --short HEAD) 10 | GOOS = $(shell go env GOOS) 11 | ARCH = $(shell go env GOARCH) 12 | GO111MODULE ?= on 13 | # Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) 14 | ifeq (,$(shell go env GOBIN)) 15 | GOBIN=$(shell go env GOPATH)/bin 16 | else 17 | GOBIN=$(shell go env GOBIN) 18 | endif 19 | 20 | all: generate 21 | 22 | version: 23 | @echo $(VERSION) 24 | 25 | ### 26 | # Setup 27 | ### 28 | 29 | clean: 30 | rm -rf ./bin 31 | 32 | vendor: 33 | export GO111MODULE=on; go mod tidy 34 | 35 | ### 36 | # Building 37 | ### 38 | 39 | # Build manager binary 40 | build: generate vet 41 | BIN=manager VERSION=${VERSION} GIT_SHA=${GIT_SHA} ARCH=${ARCH} GOOS=${GOOS} ./build/build.sh 42 | 43 | # Run tests 44 | test: check-format check-license generate fmt vet manifests 45 | go test ./... -coverprofile cover.out 46 | 47 | # Build manager binary 48 | manager: generate fmt vet 49 | go build -o bin/manager main.go 50 | 51 | # Run against the configured Kubernetes cluster in ~/.kube/config 52 | run: generate fmt vet manifests 53 | go run ./main.go 54 | 55 | # Install CRDs into a cluster 56 | install: manifests 57 | kustomize build config/crd | kubectl apply -f - 58 | 59 | # Deploy controller in the configured Kubernetes cluster in ~/.kube/config 60 | deploy: manifests 61 | cd config/manager && kustomize edit set image controller=${IMG} 62 | kustomize build config/default | kubectl apply -f - 63 | 64 | # Generate manifests e.g. CRD, RBAC etc. 65 | manifests: controller-gen 66 | $(CONTROLLER_GEN) $(CRD_OPTIONS) rbac:roleName=solr-operator-role webhook paths="./..." output:crd:artifacts:config=config/crd/bases 67 | 68 | # Run go fmt against code 69 | fmt: 70 | go fmt ./... 71 | 72 | # Run go vet against code 73 | vet: 74 | go vet ./... 75 | 76 | check-format: 77 | ./hack/check_format.sh 78 | 79 | check-license: 80 | ./hack/check_license.sh 81 | 82 | manifests-check: 83 | @echo "Check to make sure the manifests are up to date" 84 | git diff --exit-code -- config 85 | 86 | # Generate code 87 | generate: controller-gen 88 | $(CONTROLLER_GEN) object:headerFile=./hack/boilerplate.go.txt paths="./..." 89 | 90 | 91 | docker-build: test 92 | docker build . -t ${IMG}:${VERSION} 93 | 94 | # Push the docker image for the operator 95 | docker-push: 96 | docker push ${IMG}:${VERSION} 97 | docker push ${IMG}:latest 98 | 99 | # # find or download controller-gen 100 | # download controller-gen if necessary 101 | controller-gen: 102 | ifeq (, $(shell which controller-gen)) 103 | go get sigs.k8s.io/controller-tools/cmd/controller-gen@v0.2.2 104 | CONTROLLER_GEN=$(shell go env GOPATH)/bin/controller-gen 105 | else 106 | CONTROLLER_GEN=$(shell which controller-gen) 107 | endif 108 | -------------------------------------------------------------------------------- /config/crd/bases/solr.bloomberg.com_solrcollectionaliases.yaml: -------------------------------------------------------------------------------- 1 | 2 | --- 3 | apiVersion: apiextensions.k8s.io/v1beta1 4 | kind: CustomResourceDefinition 5 | metadata: 6 | annotations: 7 | controller-gen.kubebuilder.io/version: v0.2.2 8 | creationTimestamp: null 9 | name: solrcollectionaliases.solr.bloomberg.com 10 | spec: 11 | group: solr.bloomberg.com 12 | names: 13 | kind: SolrCollectionAlias 14 | listKind: SolrCollectionAliasList 15 | plural: solrcollectionaliases 16 | singular: solrcollectionalias 17 | scope: "" 18 | validation: 19 | openAPIV3Schema: 20 | description: SolrCollectionAlias is the Schema for the solrcollectionaliases 21 | API 22 | properties: 23 | apiVersion: 24 | description: 'APIVersion defines the versioned schema of this representation 25 | of an object. Servers should convert recognized schemas to the latest 26 | internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#resources' 27 | type: string 28 | kind: 29 | description: 'Kind is a string value representing the REST resource this 30 | object represents. Servers may infer this from the endpoint the client 31 | submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#types-kinds' 32 | type: string 33 | metadata: 34 | type: object 35 | spec: 36 | description: SolrCollectionAliasSpec defines the desired state of SolrCollectionAlias 37 | properties: 38 | aliasType: 39 | description: AliasType is a either standard or routed, right now we 40 | support standard 41 | type: string 42 | collections: 43 | description: Collections is a list of collections to apply alias to 44 | items: 45 | type: string 46 | type: array 47 | solrCloud: 48 | description: A reference to the SolrCloud to create alias on 49 | type: string 50 | required: 51 | - aliasType 52 | - collections 53 | - solrCloud 54 | type: object 55 | status: 56 | description: SolrCollectionAliasStatus defines the observed state of SolrCollectionAlias 57 | properties: 58 | collections: 59 | description: Associated collections to the alias 60 | items: 61 | type: string 62 | type: array 63 | created: 64 | description: Created or not status 65 | type: boolean 66 | createdTime: 67 | description: Time the alias was created 68 | format: date-time 69 | type: string 70 | type: object 71 | type: object 72 | version: v1beta1 73 | versions: 74 | - name: v1beta1 75 | served: true 76 | storage: true 77 | status: 78 | acceptedNames: 79 | kind: "" 80 | plural: "" 81 | conditions: [] 82 | storedVersions: [] 83 | -------------------------------------------------------------------------------- /controllers/suite_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Bloomberg Finance LP. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package controllers 18 | 19 | import ( 20 | "path/filepath" 21 | "testing" 22 | 23 | . "github.com/onsi/ginkgo" 24 | . "github.com/onsi/gomega" 25 | 26 | solrv1beta1 "github.com/bloomberg/solr-operator/api/v1beta1" 27 | "k8s.io/client-go/kubernetes/scheme" 28 | "k8s.io/client-go/rest" 29 | "sigs.k8s.io/controller-runtime/pkg/client" 30 | "sigs.k8s.io/controller-runtime/pkg/envtest" 31 | logf "sigs.k8s.io/controller-runtime/pkg/log" 32 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 33 | // +kubebuilder:scaffold:imports 34 | ) 35 | 36 | // These tests use Ginkgo (BDD-style Go testing framework). Refer to 37 | // http://onsi.github.io/ginkgo/ to learn more about Ginkgo. 38 | 39 | var cfg *rest.Config 40 | var k8sClient client.Client 41 | var testEnv *envtest.Environment 42 | 43 | func TestAPIs(t *testing.T) { 44 | RegisterFailHandler(Fail) 45 | 46 | RunSpecsWithDefaultAndCustomReporters(t, 47 | "Controller Suite", 48 | []Reporter{envtest.NewlineReporter{}}) 49 | } 50 | 51 | var _ = BeforeSuite(func(done Done) { 52 | logf.SetLogger(zap.LoggerTo(GinkgoWriter, true)) 53 | 54 | By("bootstrapping test environment") 55 | testEnv = &envtest.Environment{ 56 | CRDDirectoryPaths: []string{filepath.Join("..", "config", "crd", "bases")}, 57 | } 58 | 59 | var err error 60 | cfg, err = testEnv.Start() 61 | Expect(err).ToNot(HaveOccurred()) 62 | Expect(cfg).ToNot(BeNil()) 63 | 64 | err = solrv1beta1.AddToScheme(scheme.Scheme) 65 | Expect(err).NotTo(HaveOccurred()) 66 | 67 | err = solrv1beta1.AddToScheme(scheme.Scheme) 68 | Expect(err).NotTo(HaveOccurred()) 69 | 70 | err = solrv1beta1.AddToScheme(scheme.Scheme) 71 | Expect(err).NotTo(HaveOccurred()) 72 | 73 | err = solrv1beta1.AddToScheme(scheme.Scheme) 74 | Expect(err).NotTo(HaveOccurred()) 75 | 76 | err = solrv1beta1.AddToScheme(scheme.Scheme) 77 | Expect(err).NotTo(HaveOccurred()) 78 | 79 | err = solrv1beta1.AddToScheme(scheme.Scheme) 80 | Expect(err).NotTo(HaveOccurred()) 81 | 82 | err = solrv1beta1.AddToScheme(scheme.Scheme) 83 | Expect(err).NotTo(HaveOccurred()) 84 | 85 | err = solrv1beta1.AddToScheme(scheme.Scheme) 86 | Expect(err).NotTo(HaveOccurred()) 87 | 88 | err = solrv1beta1.AddToScheme(scheme.Scheme) 89 | Expect(err).NotTo(HaveOccurred()) 90 | 91 | err = solrv1beta1.AddToScheme(scheme.Scheme) 92 | Expect(err).NotTo(HaveOccurred()) 93 | 94 | err = solrv1beta1.AddToScheme(scheme.Scheme) 95 | Expect(err).NotTo(HaveOccurred()) 96 | 97 | // +kubebuilder:scaffold:scheme 98 | 99 | k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) 100 | Expect(err).ToNot(HaveOccurred()) 101 | Expect(k8sClient).ToNot(BeNil()) 102 | 103 | close(done) 104 | }, 60) 105 | 106 | var _ = AfterSuite(func() { 107 | By("tearing down the test environment") 108 | err := testEnv.Stop() 109 | Expect(err).ToNot(HaveOccurred()) 110 | }) 111 | -------------------------------------------------------------------------------- /api/v1beta1/solrcollection_types.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Bloomberg Finance LP. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package v1beta1 18 | 19 | import ( 20 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 21 | ) 22 | 23 | // SolrCollectionSpec defines the desired state of SolrCollection 24 | type SolrCollectionSpec struct { 25 | // A reference to the SolrCloud to create a collection for 26 | SolrCloud string `json:"solrCloud"` 27 | 28 | // The name of the collection to perform the action on 29 | Collection string `json:"collection"` 30 | 31 | // Define a configset to use for the collection. Use '_default' if you don't have a custom configset 32 | CollectionConfigName string `json:"collectionConfigName"` 33 | 34 | // The router name that will be used. The router defines how documents will be distributed 35 | // +optional 36 | RouterName string `json:"routerName,omitempty"` 37 | 38 | // If this parameter is specified, the router will look at the value of the field in an input document 39 | // to compute the hash and identify a shard instead of looking at the uniqueKey field. 40 | // If the field specified is null in the document, the document will be rejected. 41 | // +optional 42 | RouterField string `json:"routerField,omitempty"` 43 | 44 | // The num of shards to create, used if RouteName is compositeId 45 | // +optional 46 | NumShards int64 `json:"numShards,omitempty"` 47 | 48 | // The replication factor to be used 49 | // +optional 50 | ReplicationFactor int64 `json:"replicationFactor,omitempty"` 51 | 52 | // Max shards per node 53 | // +optional 54 | MaxShardsPerNode int64 `json:"maxShardsPerNode,omitempty"` 55 | 56 | // A comma separated list of shard names, e.g., shard-x,shard-y,shard-z. This is a required parameter when the router.name is implicit 57 | // +optional 58 | Shards string `json:"shards,omitempty"` 59 | 60 | // When set to true, enables automatic addition of replicas when the number of active replicas falls below the value set for replicationFactor 61 | // +optional 62 | AutoAddReplicas bool `json:"autoAddReplicas,omitempty"` 63 | } 64 | 65 | // SolrCollectionStatus defines the observed state of SolrCollection 66 | type SolrCollectionStatus struct { 67 | // Whether the collection has been created or not 68 | // +optional 69 | Created bool `json:"created,omitempty"` 70 | 71 | // Time the collection was created 72 | // +optional 73 | CreatedTime *metav1.Time `json:"createdTime,omitempty"` 74 | 75 | // Set the status of the collection creation process 76 | // +optional 77 | InProgressCreation bool `json:"inProgressCreation,omitempty"` 78 | } 79 | 80 | // +kubebuilder:object:root=true 81 | 82 | // SolrCollection is the Schema for the solrcollections API 83 | type SolrCollection struct { 84 | metav1.TypeMeta `json:",inline"` 85 | metav1.ObjectMeta `json:"metadata,omitempty"` 86 | 87 | Spec SolrCollectionSpec `json:"spec,omitempty"` 88 | Status SolrCollectionStatus `json:"status,omitempty"` 89 | } 90 | 91 | // +kubebuilder:object:root=true 92 | 93 | // SolrCollectionList contains a list of SolrCollection 94 | type SolrCollectionList struct { 95 | metav1.TypeMeta `json:",inline"` 96 | metav1.ListMeta `json:"metadata,omitempty"` 97 | Items []SolrCollection `json:"items"` 98 | } 99 | 100 | func init() { 101 | SchemeBuilder.Register(&SolrCollection{}, &SolrCollectionList{}) 102 | } 103 | -------------------------------------------------------------------------------- /example/ext_ops.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: etcd-operator 5 | spec: 6 | replicas: 1 7 | selector: 8 | matchLabels: 9 | name: etcd-operator 10 | template: 11 | metadata: 12 | labels: 13 | name: etcd-operator 14 | spec: 15 | containers: 16 | - name: etcd-operator 17 | image: quay.io/coreos/etcd-operator:v0.9.3 18 | command: 19 | - etcd-operator 20 | - -cluster-wide 21 | env: 22 | - name: MY_POD_NAMESPACE 23 | valueFrom: 24 | fieldRef: 25 | fieldPath: metadata.namespace 26 | - name: MY_POD_NAME 27 | valueFrom: 28 | fieldRef: 29 | fieldPath: metadata.name 30 | 31 | --- 32 | apiVersion: apiextensions.k8s.io/v1beta1 33 | kind: CustomResourceDefinition 34 | metadata: 35 | name: zookeeperclusters.zookeeper.pravega.io 36 | spec: 37 | group: zookeeper.pravega.io 38 | names: 39 | kind: ZookeeperCluster 40 | listKind: ZookeeperClusterList 41 | plural: zookeeperclusters 42 | singular: zookeepercluster 43 | shortNames: 44 | - zk 45 | additionalPrinterColumns: 46 | - name: Members 47 | type: integer 48 | description: The number zookeeper members running 49 | JSONPath: .status.replicas 50 | - name: Ready Members 51 | type: integer 52 | description: The number zookeeper members ready 53 | JSONPath: .status.readyReplicas 54 | - name: Internal Endpoint 55 | type: string 56 | description: Client endpoint internal to cluster network 57 | JSONPath: .status.internalClientEndpoint 58 | - name: External Endpoint 59 | type: string 60 | description: Client endpoint external to cluster network via LoadBalancer 61 | JSONPath: .status.externalClientEndpoint 62 | - name: Age 63 | type: date 64 | JSONPath: .metadata.creationTimestamp 65 | scope: Namespaced 66 | version: v1beta1 67 | subresources: 68 | status: {} 69 | 70 | --- 71 | apiVersion: apps/v1 72 | kind: Deployment 73 | metadata: 74 | name: zk-operator 75 | spec: 76 | replicas: 1 77 | selector: 78 | matchLabels: 79 | name: zk-operator 80 | template: 81 | metadata: 82 | labels: 83 | name: zk-operator 84 | spec: 85 | serviceAccountName: zookeeper-operator 86 | containers: 87 | - name: zk-operator 88 | # Replace this with the built image name 89 | image: pravega/zookeeper-operator:v0.2.0 90 | ports: 91 | - containerPort: 60000 92 | name: metrics 93 | imagePullPolicy: Always 94 | command: 95 | - zookeeper-operator 96 | readinessProbe: 97 | exec: 98 | command: 99 | - stat 100 | - /tmp/operator-sdk-ready 101 | initialDelaySeconds: 4 102 | periodSeconds: 10 103 | failureThreshold: 1 104 | env: 105 | - name: WATCH_NAMESPACE 106 | value: "" 107 | - name: POD_NAME 108 | valueFrom: 109 | fieldRef: 110 | fieldPath: metadata.name 111 | - name: OPERATOR_NAME 112 | value: "zk-operator" 113 | 114 | --- 115 | apiVersion: v1 116 | kind: ServiceAccount 117 | metadata: 118 | name: zookeeper-operator 119 | 120 | --- 121 | 122 | kind: ClusterRole 123 | apiVersion: rbac.authorization.k8s.io/v1beta1 124 | metadata: 125 | name: zookeeper-operator 126 | rules: 127 | - apiGroups: 128 | - zookeeper.pravega.io 129 | resources: 130 | - "*" 131 | verbs: 132 | - "*" 133 | - apiGroups: 134 | - "" 135 | resources: 136 | - pods 137 | - services 138 | - endpoints 139 | - persistentvolumeclaims 140 | - events 141 | - configmaps 142 | - secrets 143 | verbs: 144 | - "*" 145 | - apiGroups: 146 | - apps 147 | resources: 148 | - deployments 149 | - daemonsets 150 | - replicasets 151 | - statefulsets 152 | verbs: 153 | - "*" 154 | - apiGroups: 155 | - policy 156 | resources: 157 | - poddisruptionbudgets 158 | verbs: 159 | - "*" 160 | 161 | --- 162 | 163 | kind: ClusterRoleBinding 164 | apiVersion: rbac.authorization.k8s.io/v1beta1 165 | metadata: 166 | name: zookeeper-operator-cluster-role-binding 167 | subjects: 168 | - kind: ServiceAccount 169 | name: zookeeper-operator 170 | namespace: default 171 | roleRef: 172 | kind: ClusterRole 173 | name: zookeeper-operator 174 | apiGroup: rbac.authorization.k8s.io 175 | -------------------------------------------------------------------------------- /config/crd/bases/solr.bloomberg.com_solrcollections.yaml: -------------------------------------------------------------------------------- 1 | 2 | --- 3 | apiVersion: apiextensions.k8s.io/v1beta1 4 | kind: CustomResourceDefinition 5 | metadata: 6 | annotations: 7 | controller-gen.kubebuilder.io/version: v0.2.2 8 | creationTimestamp: null 9 | name: solrcollections.solr.bloomberg.com 10 | spec: 11 | group: solr.bloomberg.com 12 | names: 13 | kind: SolrCollection 14 | listKind: SolrCollectionList 15 | plural: solrcollections 16 | singular: solrcollection 17 | scope: "" 18 | validation: 19 | openAPIV3Schema: 20 | description: SolrCollection is the Schema for the solrcollections API 21 | properties: 22 | apiVersion: 23 | description: 'APIVersion defines the versioned schema of this representation 24 | of an object. Servers should convert recognized schemas to the latest 25 | internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#resources' 26 | type: string 27 | kind: 28 | description: 'Kind is a string value representing the REST resource this 29 | object represents. Servers may infer this from the endpoint the client 30 | submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#types-kinds' 31 | type: string 32 | metadata: 33 | type: object 34 | spec: 35 | description: SolrCollectionSpec defines the desired state of SolrCollection 36 | properties: 37 | autoAddReplicas: 38 | description: When set to true, enables automatic addition of replicas 39 | when the number of active replicas falls below the value set for replicationFactor 40 | type: boolean 41 | collection: 42 | description: The name of the collection to perform the action on 43 | type: string 44 | collectionConfigName: 45 | description: Define a configset to use for the collection. Use '_default' 46 | if you don't have a custom configset 47 | type: string 48 | maxShardsPerNode: 49 | description: Max shards per node 50 | format: int64 51 | type: integer 52 | numShards: 53 | description: The num of shards to create, used if RouteName is compositeId 54 | format: int64 55 | type: integer 56 | replicationFactor: 57 | description: The replication factor to be used 58 | format: int64 59 | type: integer 60 | routerField: 61 | description: If this parameter is specified, the router will look at 62 | the value of the field in an input document to compute the hash and 63 | identify a shard instead of looking at the uniqueKey field. If the 64 | field specified is null in the document, the document will be rejected. 65 | type: string 66 | routerName: 67 | description: The router name that will be used. The router defines how 68 | documents will be distributed 69 | type: string 70 | shards: 71 | description: A comma separated list of shard names, e.g., shard-x,shard-y,shard-z. 72 | This is a required parameter when the router.name is implicit 73 | type: string 74 | solrCloud: 75 | description: A reference to the SolrCloud to create a collection for 76 | type: string 77 | required: 78 | - collection 79 | - collectionConfigName 80 | - solrCloud 81 | type: object 82 | status: 83 | description: SolrCollectionStatus defines the observed state of SolrCollection 84 | properties: 85 | created: 86 | description: Whether the collection has been created or not 87 | type: boolean 88 | createdTime: 89 | description: Time the collection was created 90 | format: date-time 91 | type: string 92 | inProgressCreation: 93 | description: Set the status of the collection creation process 94 | type: boolean 95 | type: object 96 | type: object 97 | version: v1beta1 98 | versions: 99 | - name: v1beta1 100 | served: true 101 | storage: true 102 | status: 103 | acceptedNames: 104 | kind: "" 105 | plural: "" 106 | conditions: [] 107 | storedVersions: [] 108 | -------------------------------------------------------------------------------- /config/rbac/role.yaml: -------------------------------------------------------------------------------- 1 | 2 | --- 3 | apiVersion: rbac.authorization.k8s.io/v1 4 | kind: ClusterRole 5 | metadata: 6 | creationTimestamp: null 7 | name: solr-operator-role 8 | rules: 9 | - apiGroups: 10 | - "" 11 | resources: 12 | - configmaps 13 | verbs: 14 | - create 15 | - delete 16 | - get 17 | - list 18 | - patch 19 | - update 20 | - watch 21 | - apiGroups: 22 | - "" 23 | resources: 24 | - configmaps/status 25 | verbs: 26 | - get 27 | - patch 28 | - update 29 | - apiGroups: 30 | - "" 31 | resources: 32 | - pods 33 | verbs: 34 | - get 35 | - list 36 | - watch 37 | - apiGroups: 38 | - "" 39 | resources: 40 | - pods/exec 41 | verbs: 42 | - create 43 | - apiGroups: 44 | - "" 45 | resources: 46 | - pods/status 47 | verbs: 48 | - get 49 | - apiGroups: 50 | - "" 51 | resources: 52 | - services 53 | verbs: 54 | - create 55 | - delete 56 | - get 57 | - list 58 | - patch 59 | - update 60 | - watch 61 | - apiGroups: 62 | - "" 63 | resources: 64 | - services/status 65 | verbs: 66 | - get 67 | - patch 68 | - update 69 | - apiGroups: 70 | - apps 71 | resources: 72 | - deployments 73 | verbs: 74 | - create 75 | - delete 76 | - get 77 | - list 78 | - patch 79 | - update 80 | - watch 81 | - apiGroups: 82 | - apps 83 | resources: 84 | - deployments/status 85 | verbs: 86 | - get 87 | - patch 88 | - update 89 | - apiGroups: 90 | - apps 91 | resources: 92 | - statefulsets 93 | verbs: 94 | - create 95 | - delete 96 | - get 97 | - list 98 | - patch 99 | - update 100 | - watch 101 | - apiGroups: 102 | - apps 103 | resources: 104 | - statefulsets/status 105 | verbs: 106 | - get 107 | - patch 108 | - update 109 | - apiGroups: 110 | - batch 111 | resources: 112 | - jobs 113 | verbs: 114 | - create 115 | - delete 116 | - get 117 | - list 118 | - patch 119 | - update 120 | - watch 121 | - apiGroups: 122 | - batch 123 | resources: 124 | - jobs/status 125 | verbs: 126 | - get 127 | - patch 128 | - update 129 | - apiGroups: 130 | - etcd.database.coreos.com 131 | resources: 132 | - etcdclusters 133 | verbs: 134 | - create 135 | - delete 136 | - get 137 | - list 138 | - patch 139 | - update 140 | - watch 141 | - apiGroups: 142 | - etcd.database.coreos.com 143 | resources: 144 | - etcdclusters/status 145 | verbs: 146 | - get 147 | - patch 148 | - update 149 | - apiGroups: 150 | - extensions 151 | resources: 152 | - ingresses 153 | verbs: 154 | - create 155 | - delete 156 | - get 157 | - list 158 | - patch 159 | - update 160 | - watch 161 | - apiGroups: 162 | - extensions 163 | resources: 164 | - ingresses/status 165 | verbs: 166 | - get 167 | - patch 168 | - update 169 | - apiGroups: 170 | - solr.bloomberg.com 171 | resources: 172 | - solrbackups 173 | verbs: 174 | - create 175 | - delete 176 | - get 177 | - list 178 | - patch 179 | - update 180 | - watch 181 | - apiGroups: 182 | - solr.bloomberg.com 183 | resources: 184 | - solrbackups/status 185 | verbs: 186 | - get 187 | - patch 188 | - update 189 | - apiGroups: 190 | - solr.bloomberg.com 191 | resources: 192 | - solrclouds 193 | verbs: 194 | - create 195 | - delete 196 | - get 197 | - list 198 | - patch 199 | - update 200 | - watch 201 | - apiGroups: 202 | - solr.bloomberg.com 203 | resources: 204 | - solrclouds/status 205 | verbs: 206 | - get 207 | - patch 208 | - update 209 | - apiGroups: 210 | - solr.bloomberg.com 211 | resources: 212 | - solrcollectionaliases 213 | verbs: 214 | - create 215 | - delete 216 | - get 217 | - list 218 | - patch 219 | - update 220 | - watch 221 | - apiGroups: 222 | - solr.bloomberg.com 223 | resources: 224 | - solrcollectionaliases/status 225 | verbs: 226 | - get 227 | - patch 228 | - update 229 | - apiGroups: 230 | - solr.bloomberg.com 231 | resources: 232 | - solrcollections 233 | verbs: 234 | - create 235 | - delete 236 | - get 237 | - list 238 | - patch 239 | - update 240 | - watch 241 | - apiGroups: 242 | - solr.bloomberg.com 243 | resources: 244 | - solrcollections/status 245 | verbs: 246 | - get 247 | - patch 248 | - update 249 | - apiGroups: 250 | - solr.bloomberg.com 251 | resources: 252 | - solrprometheusexporters 253 | verbs: 254 | - create 255 | - delete 256 | - get 257 | - list 258 | - patch 259 | - update 260 | - watch 261 | - apiGroups: 262 | - solr.bloomberg.com 263 | resources: 264 | - solrprometheusexporters/status 265 | verbs: 266 | - get 267 | - patch 268 | - update 269 | - apiGroups: 270 | - zookeeper.pravega.io 271 | resources: 272 | - zookeeperclusters 273 | verbs: 274 | - create 275 | - delete 276 | - get 277 | - list 278 | - patch 279 | - update 280 | - watch 281 | - apiGroups: 282 | - zookeeper.pravega.io 283 | resources: 284 | - zookeeperclusters/status 285 | verbs: 286 | - get 287 | - patch 288 | - update 289 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Bloomberg Finance LP. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "flag" 21 | "os" 22 | 23 | solrv1beta1 "github.com/bloomberg/solr-operator/api/v1beta1" 24 | "github.com/bloomberg/solr-operator/controllers" 25 | etcdv1beta2 "github.com/coreos/etcd-operator/pkg/apis/etcd/v1beta2" 26 | "github.com/coreos/etcd-operator/pkg/util/constants" 27 | zkv1beta1 "github.com/pravega/zookeeper-operator/pkg/apis" 28 | "k8s.io/apimachinery/pkg/runtime" 29 | clientgoscheme "k8s.io/client-go/kubernetes/scheme" 30 | _ "k8s.io/client-go/plugin/pkg/client/auth/gcp" 31 | ctrl "sigs.k8s.io/controller-runtime" 32 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 33 | // +kubebuilder:scaffold:imports 34 | ) 35 | 36 | var ( 37 | scheme = runtime.NewScheme() 38 | setupLog = ctrl.Log.WithName("setup") 39 | namespace string 40 | name string 41 | 42 | useEtcdCRD bool 43 | useZookeeperCRD bool 44 | 45 | ingressBaseDomain string 46 | ) 47 | 48 | func init() { 49 | _ = clientgoscheme.AddToScheme(scheme) 50 | 51 | _ = solrv1beta1.AddToScheme(scheme) 52 | _ = zkv1beta1.AddToScheme(scheme) 53 | _ = etcdv1beta2.AddToScheme(scheme) 54 | 55 | // +kubebuilder:scaffold:scheme 56 | flag.BoolVar(&useEtcdCRD, "etcd-operator", true, "The operator will not use the etcd operator & crd when this flag is set to false.") 57 | flag.BoolVar(&useZookeeperCRD, "zk-operator", true, "The operator will not use the zk operator & crd when this flag is set to false.") 58 | flag.StringVar(&ingressBaseDomain, "ingress-base-domain", "", "The operator will use this base domain for host matching in an ingress for the cloud.") 59 | flag.Parse() 60 | } 61 | 62 | func main() { 63 | namespace = os.Getenv(constants.EnvOperatorPodNamespace) 64 | if len(namespace) == 0 { 65 | //log.Fatalf("must set env (%s)", constants.EnvOperatorPodNamespace) 66 | } 67 | name = os.Getenv(constants.EnvOperatorPodName) 68 | if len(name) == 0 { 69 | //log.Fatalf("must set env (%s)", constants.EnvOperatorPodName) 70 | } 71 | 72 | var metricsAddr string 73 | var enableLeaderElection bool 74 | flag.StringVar(&metricsAddr, "metrics-addr", ":8080", "The address the metric endpoint binds to.") 75 | flag.BoolVar(&enableLeaderElection, "enable-leader-election", false, 76 | "Enable leader election for controller manager. Enabling this will ensure there is only one active controller manager.") 77 | flag.Parse() 78 | 79 | ctrl.SetLogger(zap.Logger(true)) 80 | 81 | mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ 82 | Scheme: scheme, 83 | MetricsBindAddress: metricsAddr, 84 | LeaderElection: enableLeaderElection, 85 | Port: 9443, 86 | }) 87 | if err != nil { 88 | setupLog.Error(err, "unable to start manager") 89 | os.Exit(1) 90 | } 91 | 92 | controllers.SetIngressBaseUrl(ingressBaseDomain) 93 | controllers.UseEtcdCRD(useEtcdCRD) 94 | controllers.UseZkCRD(useZookeeperCRD) 95 | 96 | if err = (&controllers.SolrCloudReconciler{ 97 | Client: mgr.GetClient(), 98 | Log: ctrl.Log.WithName("controllers").WithName("SolrCloud"), 99 | }).SetupWithManager(mgr); err != nil { 100 | setupLog.Error(err, "unable to create controller", "controller", "SolrCloud") 101 | os.Exit(1) 102 | } 103 | if err = (&controllers.SolrBackupReconciler{ 104 | Client: mgr.GetClient(), 105 | Log: ctrl.Log.WithName("controllers").WithName("SolrBackup"), 106 | }).SetupWithManager(mgr); err != nil { 107 | setupLog.Error(err, "unable to create controller", "controller", "SolrBackup") 108 | os.Exit(1) 109 | } 110 | if err = (&controllers.SolrCollectionReconciler{ 111 | Client: mgr.GetClient(), 112 | Log: ctrl.Log.WithName("controllers").WithName("SolrCollection"), 113 | }).SetupWithManager(mgr); err != nil { 114 | setupLog.Error(err, "unable to create controller", "controller", "SolrCollection") 115 | os.Exit(1) 116 | } 117 | if err = (&controllers.SolrPrometheusExporterReconciler{ 118 | Client: mgr.GetClient(), 119 | Log: ctrl.Log.WithName("controllers").WithName("SolrPrometheusExporter"), 120 | }).SetupWithManager(mgr); err != nil { 121 | setupLog.Error(err, "unable to create controller", "controller", "SolrPrometheusExporter") 122 | os.Exit(1) 123 | } 124 | if err = (&controllers.SolrCollectionAliasReconciler{ 125 | Client: mgr.GetClient(), 126 | Log: ctrl.Log.WithName("controllers").WithName("SolrCollectionAlias"), 127 | }).SetupWithManager(mgr); err != nil { 128 | setupLog.Error(err, "unable to create controller", "controller", "SolrCollectionAlias") 129 | os.Exit(1) 130 | } 131 | // +kubebuilder:scaffold:builder 132 | 133 | setupLog.Info("starting manager") 134 | if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { 135 | setupLog.Error(err, "problem running manager") 136 | os.Exit(1) 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /controllers/solrcollectionalias_controller.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Bloomberg Finance LP. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package controllers 18 | 19 | import ( 20 | "context" 21 | "reflect" 22 | "time" 23 | 24 | "github.com/bloomberg/solr-operator/controllers/util" 25 | "github.com/go-logr/logr" 26 | "k8s.io/apimachinery/pkg/api/errors" 27 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 28 | "k8s.io/apimachinery/pkg/types" 29 | ctrl "sigs.k8s.io/controller-runtime" 30 | "sigs.k8s.io/controller-runtime/pkg/client" 31 | "sigs.k8s.io/controller-runtime/pkg/reconcile" 32 | 33 | solrv1beta1 "github.com/bloomberg/solr-operator/api/v1beta1" 34 | ) 35 | 36 | // SolrCollectionAliasReconciler reconciles a SolrCollectionAlias object 37 | type SolrCollectionAliasReconciler struct { 38 | client.Client 39 | Log logr.Logger 40 | } 41 | 42 | // +kubebuilder:rbac:groups=solr.bloomberg.com,resources=solrcollectionaliases,verbs=get;list;watch;create;update;patch;delete 43 | // +kubebuilder:rbac:groups=solr.bloomberg.com,resources=solrcollectionaliases/status,verbs=get;update;patch 44 | 45 | func (r *SolrCollectionAliasReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { 46 | _ = context.Background() 47 | _ = r.Log.WithValues("solrcollectionalias", req.NamespacedName) 48 | 49 | alias := &solrv1beta1.SolrCollectionAlias{} 50 | aliasFinalizer := "alias.finalizers.bloomberg.com" 51 | 52 | err := r.Get(context.TODO(), req.NamespacedName, alias) 53 | if err != nil { 54 | if errors.IsNotFound(err) { 55 | // Object not found, return. Created objects are automatically garbage collected. 56 | // For additional cleanup logic use finalizers. 57 | return reconcile.Result{}, nil 58 | } 59 | // Error reading the object - requeue the req. 60 | return reconcile.Result{}, err 61 | } 62 | 63 | oldStatus := alias.Status.DeepCopy() 64 | requeueOrNot := reconcile.Result{Requeue: true, RequeueAfter: time.Second * 5} 65 | 66 | aliasCreationStatus := reconcileSolrCollectionAlias(r, alias, alias.Spec.SolrCloud, alias.Name, alias.Spec.AliasType, alias.Spec.Collections, alias.Namespace) 67 | 68 | if err != nil { 69 | r.Log.Error(err, "Error while creating SolrCloud alias") 70 | } 71 | 72 | if alias.ObjectMeta.DeletionTimestamp.IsZero() { 73 | // The object is not being deleted, so if it does not have our finalizer, 74 | // then lets add the finalizer and update the object 75 | if !util.ContainsString(alias.ObjectMeta.Finalizers, aliasFinalizer) { 76 | alias.ObjectMeta.Finalizers = append(alias.ObjectMeta.Finalizers, aliasFinalizer) 77 | if err := r.Update(context.Background(), alias); err != nil { 78 | return reconcile.Result{}, err 79 | } 80 | } 81 | } else { 82 | // The object is being deleted, get associated SolrCloud 83 | solrCloud := &solrv1beta1.SolrCloud{} 84 | err = r.Get(context.TODO(), types.NamespacedName{Namespace: alias.Namespace, Name: alias.Spec.SolrCloud}, solrCloud) 85 | 86 | if util.ContainsString(alias.ObjectMeta.Finalizers, aliasFinalizer) && solrCloud != nil && aliasCreationStatus { 87 | r.Log.Info("Deleting Solr collection alias", "cloud", alias.Spec.SolrCloud, "namespace", alias.Namespace, "Collection Name", alias.Name) 88 | // our finalizer is present, along with the associated SolrCloud and alias lets delete alias 89 | delete, err := util.DeleteCollectionAlias(alias.Spec.SolrCloud, alias.Name, alias.Namespace) 90 | if err != nil { 91 | r.Log.Error(err, "Failed to delete Solr collection") 92 | return reconcile.Result{}, err 93 | } 94 | 95 | r.Log.Info("Deleted Solr collection", "cloud", alias.Spec.SolrCloud, "namespace", alias.Namespace, "Alias", alias.Name, "Deleted", delete) 96 | 97 | } 98 | 99 | // remove our finalizer from the list and update it. 100 | alias.ObjectMeta.Finalizers = util.RemoveString(alias.ObjectMeta.Finalizers, aliasFinalizer) 101 | if err := r.Update(context.Background(), alias); err != nil { 102 | return reconcile.Result{}, err 103 | } 104 | } 105 | 106 | if alias.Status.CreatedTime == nil { 107 | now := metav1.Now() 108 | alias.Status.CreatedTime = &now 109 | alias.Status.Created = aliasCreationStatus 110 | } 111 | 112 | if !reflect.DeepEqual(oldStatus, alias.Status) { 113 | r.Log.Info("Updating status for collection alias", "alias", alias, "namespace", alias.Namespace, "name", alias.Name) 114 | err = r.Status().Update(context.TODO(), alias) 115 | } 116 | 117 | if aliasCreationStatus { 118 | requeueOrNot = reconcile.Result{} 119 | } 120 | 121 | return requeueOrNot, nil 122 | } 123 | 124 | func reconcileSolrCollectionAlias(r *SolrCollectionAliasReconciler, alias *solrv1beta1.SolrCollectionAlias, solrCloudName string, aliasName string, aliasType string, collections []string, namespace string) (aliasCreationStatus bool) { 125 | success, aliasCollections := util.CurrentCollectionAliasDetails(solrCloudName, aliasName, namespace) 126 | 127 | // If not created, or if alias status differs from spec requirements create alias 128 | if !success || !reflect.DeepEqual(alias.Status.Collections, aliasCollections) { 129 | r.Log.Info("Applying collection alias", "alias", alias) 130 | err := util.CreateCollecttionAlias(solrCloudName, aliasName, aliasType, collections, namespace) 131 | if err == nil { 132 | alias.Status.Created = true 133 | alias.Status.Collections = collections 134 | return alias.Status.Created 135 | } 136 | } 137 | 138 | return alias.Status.Created 139 | } 140 | 141 | func (r *SolrCollectionAliasReconciler) SetupWithManager(mgr ctrl.Manager) error { 142 | return ctrl.NewControllerManagedBy(mgr). 143 | For(&solrv1beta1.SolrCollectionAlias{}). 144 | Complete(r) 145 | } 146 | -------------------------------------------------------------------------------- /api/v1beta1/solrprometheusexporter_types.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Bloomberg Finance LP. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package v1beta1 18 | 19 | import ( 20 | "fmt" 21 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 22 | ) 23 | 24 | const ( 25 | SolrPrometheusExporterTechnologyLabel = "solr-prometheus-exporter" 26 | ) 27 | 28 | // SolrPrometheusExporterSpec defines the desired state of SolrPrometheusExporter 29 | type SolrPrometheusExporterSpec struct { 30 | // Reference of the Solr instance to collect metrics for 31 | SolrReference `json:"solrReference"` 32 | 33 | // Image of Solr Prometheus Exporter to run. 34 | // +optional 35 | Image *ContainerImage `json:"image,omitempty"` 36 | 37 | // Pod defines the policy to create pod for the SolrCloud. 38 | // Updating the Pod does not take effect on any existing pods. 39 | // +optional 40 | PodPolicy SolrPodPolicy `json:"podPolicy,omitempty"` 41 | 42 | // The entrypoint into the exporter. Defaults to the official docker-solr location. 43 | // +optional 44 | ExporterEntrypoint string `json:"exporterEntrypoint,omitempty"` 45 | 46 | // Number of threads to use for the prometheus exporter 47 | // Defaults to 1 48 | // +optional 49 | NumThreads int32 `json:"numThreads,omitempty"` 50 | 51 | // The interval to scrape Solr at (in seconds) 52 | // Defaults to 60 seconds 53 | // +optional 54 | ScrapeInterval int32 `json:"scrapeInterval,omitempty"` 55 | 56 | // The xml config for the metrics 57 | // +optional 58 | Config string `json:"metricsConfig,omitempty"` 59 | } 60 | 61 | func (ps *SolrPrometheusExporterSpec) withDefaults(namespace string) (changed bool) { 62 | changed = ps.SolrReference.withDefaults(namespace) || changed 63 | 64 | if ps.Image == nil { 65 | ps.Image = &ContainerImage{} 66 | } 67 | changed = ps.Image.withDefaults(DefaultSolrRepo, DefaultSolrVersion, DefaultPullPolicy) || changed 68 | 69 | if ps.NumThreads == 0 { 70 | ps.NumThreads = 1 71 | changed = true 72 | } 73 | 74 | return changed 75 | } 76 | 77 | // SolrReference defines a reference to an internal or external solrCloud or standalone solr 78 | // One, and only one, of Cloud or Standalone must be provided. 79 | type SolrReference struct { 80 | // Reference of a solrCloud instance 81 | // +optional 82 | Cloud *SolrCloudReference `json:"cloud,omitempty"` 83 | 84 | // Reference of a standalone solr instance 85 | // +optional 86 | Standalone *StandaloneSolrReference `json:"standalone,omitempty"` 87 | } 88 | 89 | func (sr *SolrReference) withDefaults(namespace string) (changed bool) { 90 | if sr.Cloud != nil { 91 | changed = sr.Cloud.withDefaults(namespace) || changed 92 | } 93 | return changed 94 | } 95 | 96 | // SolrCloudReference defines a reference to an internal or external solrCloud. 97 | // Internal (to the kube cluster) clouds should be specified via the Name and Namespace options. 98 | // External clouds should be specified by their Zookeeper connection information. 99 | type SolrCloudReference struct { 100 | // The name of a solr cloud running within the kubernetes cluster 101 | // +optional 102 | Name string `json:"name,omitempty"` 103 | 104 | // The namespace of a solr cloud running within the kubernetes cluster 105 | // +optional 106 | Namespace string `json:"namespace,omitempty"` 107 | 108 | // The ZK Connection information for a cloud, could be used for solr's outside of the kube cluster 109 | // +optional 110 | ZookeeperConnectionInfo *ZookeeperConnectionInfo `json:"zkConnectionInfo,omitempty"` 111 | } 112 | 113 | func (scr *SolrCloudReference) withDefaults(namespace string) (changed bool) { 114 | if scr.Name != "" { 115 | if scr.Namespace == "" { 116 | scr.Namespace = namespace 117 | changed = true 118 | } 119 | } 120 | 121 | if scr.ZookeeperConnectionInfo != nil { 122 | changed = scr.ZookeeperConnectionInfo.withDefaults() || changed 123 | } 124 | return changed 125 | } 126 | 127 | // SolrPrometheusExporterStatus defines the observed state of SolrPrometheusExporter 128 | type StandaloneSolrReference struct { 129 | // The address of the standalone solr 130 | Address string `json:"address"` 131 | } 132 | 133 | // SolrPrometheusExporterStatus defines the observed state of SolrPrometheusExporter 134 | type SolrPrometheusExporterStatus struct { 135 | // An address the prometheus exporter can be connected to from within the Kube cluster 136 | // InternalAddress string `json:"internalAddress"` 137 | 138 | // An address the prometheus exporter can be connected to from outside of the Kube cluster 139 | // Will only be provided when an ingressUrl is provided for the cloud 140 | // +optional 141 | // ExternalAddress string `json:"externalAddress,omitempty"` 142 | 143 | // Is the prometheus exporter up and running 144 | Ready bool `json:"ready"` 145 | } 146 | 147 | // +kubebuilder:object:root=true 148 | 149 | // SolrPrometheusExporter is the Schema for the solrprometheusexporters API 150 | // +kubebuilder:resource:shortName=solrmetrics 151 | // +kubebuilder:subresource:status 152 | // +kubebuilder:printcolumn:name="Ready",type="boolean",JSONPath=".status.ready",description="Whether the prometheus exporter is ready" 153 | // +kubebuilder:printcolumn:name="Scrape Interval",type="integer",JSONPath=".spec.scrapeInterval",description="Scrape interval for metrics (in ms)" 154 | // +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" 155 | type SolrPrometheusExporter struct { 156 | metav1.TypeMeta `json:",inline"` 157 | metav1.ObjectMeta `json:"metadata,omitempty"` 158 | 159 | Spec SolrPrometheusExporterSpec `json:"spec,omitempty"` 160 | Status SolrPrometheusExporterStatus `json:"status,omitempty"` 161 | } 162 | 163 | // WithDefaults set default values when not defined in the spec. 164 | func (spe *SolrPrometheusExporter) WithDefaults() bool { 165 | return spe.Spec.withDefaults(spe.Namespace) 166 | } 167 | 168 | func (spe *SolrPrometheusExporter) SharedLabels() map[string]string { 169 | return spe.SharedLabelsWith(map[string]string{}) 170 | } 171 | 172 | func (spe *SolrPrometheusExporter) SharedLabelsWith(labels map[string]string) map[string]string { 173 | newLabels := map[string]string{} 174 | 175 | if labels != nil { 176 | for k, v := range labels { 177 | newLabels[k] = v 178 | } 179 | } 180 | 181 | newLabels[SolrPrometheusExporterTechnologyLabel] = spe.Name 182 | return newLabels 183 | } 184 | 185 | // MetricsDeploymentName returns the name of the metrics deployment for the cloud 186 | func (sc *SolrPrometheusExporter) MetricsDeploymentName() string { 187 | return fmt.Sprintf("%s-solr-metrics", sc.GetName()) 188 | } 189 | 190 | // MetricsConfigMapName returns the name of the metrics service for the cloud 191 | func (sc *SolrPrometheusExporter) MetricsConfigMapName() string { 192 | return fmt.Sprintf("%s-solr-metrics", sc.GetName()) 193 | } 194 | 195 | // MetricsServiceName returns the name of the metrics service for the cloud 196 | func (sc *SolrPrometheusExporter) MetricsServiceName() string { 197 | return fmt.Sprintf("%s-solr-metrics", sc.GetName()) 198 | } 199 | 200 | func (sc *SolrPrometheusExporter) MetricsIngressPrefix() string { 201 | return fmt.Sprintf("%s-%s-solr-metrics", sc.Namespace, sc.Name) 202 | } 203 | 204 | func (sc *SolrPrometheusExporter) MetricsIngressUrl(ingressBaseUrl string) string { 205 | return fmt.Sprintf("%s.%s", sc.MetricsIngressPrefix(), ingressBaseUrl) 206 | } 207 | 208 | // +kubebuilder:object:root=true 209 | 210 | // SolrPrometheusExporterList contains a list of SolrPrometheusExporter 211 | type SolrPrometheusExporterList struct { 212 | metav1.TypeMeta `json:",inline"` 213 | metav1.ListMeta `json:"metadata,omitempty"` 214 | Items []SolrPrometheusExporter `json:"items"` 215 | } 216 | 217 | func init() { 218 | SchemeBuilder.Register(&SolrPrometheusExporter{}, &SolrPrometheusExporterList{}) 219 | } 220 | -------------------------------------------------------------------------------- /controllers/solrcollection_controller.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Bloomberg Finance LP. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package controllers 18 | 19 | import ( 20 | "context" 21 | "reflect" 22 | "time" 23 | 24 | "github.com/bloomberg/solr-operator/controllers/util" 25 | "github.com/go-logr/logr" 26 | "k8s.io/apimachinery/pkg/api/errors" 27 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 28 | "k8s.io/apimachinery/pkg/types" 29 | ctrl "sigs.k8s.io/controller-runtime" 30 | "sigs.k8s.io/controller-runtime/pkg/client" 31 | "sigs.k8s.io/controller-runtime/pkg/reconcile" 32 | 33 | solrv1beta1 "github.com/bloomberg/solr-operator/api/v1beta1" 34 | ) 35 | 36 | // SolrCollectionReconciler reconciles a SolrCollection object 37 | type SolrCollectionReconciler struct { 38 | client.Client 39 | Log logr.Logger 40 | } 41 | 42 | // +kubebuilder:rbac:groups=solr.bloomberg.com,resources=solrcollections,verbs=get;list;watch;create;update;patch;delete 43 | // +kubebuilder:rbac:groups=solr.bloomberg.com,resources=solrcollections/status,verbs=get;update;patch 44 | 45 | func (r *SolrCollectionReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { 46 | _ = context.Background() 47 | _ = r.Log.WithValues("solrcloeection", req.NamespacedName) 48 | 49 | // Fetch the SolrCollection collection 50 | collection := &solrv1beta1.SolrCollection{} 51 | collectionFinalizer := "collection.finalizers.bloomberg.com" 52 | 53 | err := r.Get(context.TODO(), req.NamespacedName, collection) 54 | if err != nil { 55 | if errors.IsNotFound(err) { 56 | // Object not found, return. Created objects are automatically garbage collected. 57 | // For additional cleanup logic use finalizers. 58 | return reconcile.Result{}, nil 59 | } 60 | // Error reading the object - requeue the req. 61 | return reconcile.Result{}, err 62 | } 63 | 64 | oldStatus := collection.Status.DeepCopy() 65 | requeueOrNot := reconcile.Result{Requeue: true, RequeueAfter: time.Second * 5} 66 | 67 | solrCloud, collectionCreationStatus, err := reconcileSolrCollection(r, collection, collection.Spec.NumShards, collection.Spec.ReplicationFactor, collection.Spec.AutoAddReplicas, collection.Spec.MaxShardsPerNode, collection.Spec.RouterName, collection.Spec.RouterField, collection.Spec.Shards, collection.Spec.CollectionConfigName, collection.Namespace) 68 | 69 | if err != nil { 70 | r.Log.Error(err, "Error while creating SolrCloud collection") 71 | } 72 | 73 | if collection.Status.CreatedTime == nil { 74 | now := metav1.Now() 75 | collection.Status.CreatedTime = &now 76 | } 77 | 78 | if solrCloud != nil && collectionCreationStatus == false { 79 | r.Log.Info("Collections update failed") 80 | } 81 | 82 | if collection.ObjectMeta.DeletionTimestamp.IsZero() { 83 | // The object is not being deleted, so if it does not have our finalizer, 84 | // then lets add the finalizer and update the object 85 | if !util.ContainsString(collection.ObjectMeta.Finalizers, collectionFinalizer) { 86 | collection.ObjectMeta.Finalizers = append(collection.ObjectMeta.Finalizers, collectionFinalizer) 87 | if err := r.Update(context.Background(), collection); err != nil { 88 | return reconcile.Result{}, err 89 | } 90 | } 91 | } else { 92 | // The object is being deleted 93 | if util.ContainsString(collection.ObjectMeta.Finalizers, collectionFinalizer) { 94 | r.Log.Info("Deleting Solr collection", "cloud", collection.Spec.SolrCloud, "namespace", collection.Namespace, "Collection Name", collection.Name) 95 | // our finalizer is present, so lets handle our external dependency 96 | delete, err := util.DeleteCollection(collection.Spec.SolrCloud, collection.Name, collection.Namespace) 97 | if err != nil { 98 | r.Log.Error(err, "Failed to delete Solr collection") 99 | return reconcile.Result{}, err 100 | } 101 | 102 | r.Log.Info("Deleted Solr collection", "cloud", collection.Spec.SolrCloud, "namespace", collection.Namespace, "Collection Name", collection.Name, "Deleted", delete) 103 | 104 | } 105 | 106 | // remove our finalizer from the list and update it. 107 | collection.ObjectMeta.Finalizers = util.RemoveString(collection.ObjectMeta.Finalizers, collectionFinalizer) 108 | if err := r.Update(context.Background(), collection); err != nil { 109 | return reconcile.Result{}, err 110 | } 111 | } 112 | 113 | if !reflect.DeepEqual(oldStatus, collection.Status) { 114 | r.Log.Info("Updating status for solr-collection", "collection", collection, "namespace", collection.Namespace, "name", collection.Name) 115 | err = r.Status().Update(context.TODO(), collection) 116 | } 117 | 118 | if collection.Status.InProgressCreation { 119 | if util.CheckIfCollectionExists(collection.Spec.SolrCloud, collection.Spec.Collection, collection.Namespace) { 120 | r.Log.Info("Collection exists, creation complete", "collection", collection, "namespace", collection.Namespace, "name", collection.Name) 121 | collection.Status.InProgressCreation = false 122 | requeueOrNot = reconcile.Result{} 123 | } else { 124 | r.Log.Info("Collection creation still in progress", "collection", collection, "namespace", collection.Namespace, "name", collection.Name) 125 | requeueOrNot = reconcile.Result{Requeue: true} 126 | } 127 | } 128 | if collection.Status.Created { 129 | requeueOrNot = reconcile.Result{} 130 | } 131 | 132 | return requeueOrNot, nil 133 | } 134 | 135 | func reconcileSolrCollection(r *SolrCollectionReconciler, collection *solrv1beta1.SolrCollection, numShards int64, replicationFactor int64, autoAddReplicas bool, maxShardsPerNode int64, routerName string, routerField string, shards string, collectionConfigName string, namespace string) (solrCloud *solrv1beta1.SolrCloud, collectionCreationStatus bool, err error) { 136 | // Get the solrCloud that this collection is for. 137 | solrCloud = &solrv1beta1.SolrCloud{} 138 | err = r.Get(context.TODO(), types.NamespacedName{Namespace: collection.Namespace, Name: collection.Spec.SolrCloud}, solrCloud) 139 | 140 | // If the collection has already been created already and requires modification 141 | if collection.Status.Created { 142 | modificationRequired, err := util.CheckIfCollectionModificationRequired(solrCloud.Name, collection.Name, replicationFactor, autoAddReplicas, maxShardsPerNode, collectionConfigName, namespace) 143 | 144 | if err != nil { 145 | return nil, false, err 146 | } 147 | 148 | if modificationRequired { 149 | modify, err := util.ModifyCollection(solrCloud.Name, collection.Name, replicationFactor, autoAddReplicas, maxShardsPerNode, collectionConfigName, namespace) 150 | 151 | if err != nil { 152 | return nil, false, err 153 | } 154 | 155 | r.Log.Info("Modified Solr collection", "SolrCloud", collection.Spec.SolrCloud, "namespace", collection.Namespace, "Collection Name", collection.Name, "Modified", modify) 156 | } else { 157 | r.Log.Info("Modification on Solr collection not required", "SolrCloud", collection.Spec.SolrCloud, "namespace", collection.Namespace, "Collection Name", collection.Name, "Modified", nil) 158 | } 159 | 160 | } 161 | 162 | // If the collection collection hasn't been created or is in progress, start it creation 163 | if !collection.Status.Created && !collection.Status.InProgressCreation { 164 | 165 | // Request the creation of collection by calling solr 166 | collection.Status.InProgressCreation = true 167 | create, err := util.CreateCollection(solrCloud.Name, collection.Name, numShards, replicationFactor, autoAddReplicas, routerName, routerField, shards, collectionConfigName, namespace) 168 | if err != nil { 169 | collection.Status.InProgressCreation = false 170 | return nil, false, err 171 | } 172 | collection.Status.Created = create 173 | collection.Status.InProgressCreation = false 174 | } 175 | 176 | err = r.Get(context.TODO(), types.NamespacedName{Namespace: collection.Namespace, Name: collection.Spec.SolrCloud}, solrCloud) 177 | 178 | return solrCloud, collection.Status.Created, err 179 | } 180 | 181 | func (r *SolrCollectionReconciler) SetupWithManager(mgr ctrl.Manager) error { 182 | return ctrl.NewControllerManagedBy(mgr). 183 | For(&solrv1beta1.SolrCollection{}). 184 | Complete(r) 185 | } 186 | -------------------------------------------------------------------------------- /controllers/solrprometheusexporter_controller.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Bloomberg Finance LP. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package controllers 18 | 19 | import ( 20 | "context" 21 | solrv1beta1 "github.com/bloomberg/solr-operator/api/v1beta1" 22 | "github.com/bloomberg/solr-operator/controllers/util" 23 | "github.com/go-logr/logr" 24 | appsv1 "k8s.io/api/apps/v1" 25 | corev1 "k8s.io/api/core/v1" 26 | "k8s.io/apimachinery/pkg/api/errors" 27 | "k8s.io/apimachinery/pkg/runtime" 28 | "k8s.io/apimachinery/pkg/types" 29 | ctrl "sigs.k8s.io/controller-runtime" 30 | "sigs.k8s.io/controller-runtime/pkg/client" 31 | "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" 32 | ) 33 | 34 | // SolrPrometheusExporterReconciler reconciles a SolrPrometheusExporter object 35 | type SolrPrometheusExporterReconciler struct { 36 | client.Client 37 | Log logr.Logger 38 | scheme *runtime.Scheme 39 | } 40 | 41 | // +kubebuilder:rbac:groups=,resources=configmaps,verbs=get;list;watch;create;update;patch;delete 42 | // +kubebuilder:rbac:groups=,resources=configmaps/status,verbs=get 43 | // +kubebuilder:rbac:groups=,resources=services,verbs=get;list;watch;create;update;patch;delete 44 | // +kubebuilder:rbac:groups=,resources=services/status,verbs=get 45 | // +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete 46 | // +kubebuilder:rbac:groups=apps,resources=deployments/status,verbs=get 47 | // +kubebuilder:rbac:groups=solr.bloomberg.com,resources=solrclouds,verbs=get;list;watch 48 | // +kubebuilder:rbac:groups=solr.bloomberg.com,resources=solrclouds/status,verbs=get 49 | // +kubebuilder:rbac:groups=solr.bloomberg.com,resources=solrprometheusexporters,verbs=get;list;watch;create;update;patch;delete 50 | // +kubebuilder:rbac:groups=solr.bloomberg.com,resources=solrprometheusexporters/status,verbs=get;update;patch 51 | 52 | func (r *SolrPrometheusExporterReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { 53 | _ = context.Background() 54 | _ = r.Log.WithValues("solrprometheusexporter", req.NamespacedName) 55 | 56 | // Fetch the SolrPrometheusExporter instance 57 | prometheusExporter := &solrv1beta1.SolrPrometheusExporter{} 58 | err := r.Get(context.TODO(), req.NamespacedName, prometheusExporter) 59 | if err != nil { 60 | if errors.IsNotFound(err) { 61 | // Object not found, return. Created objects are automatically garbage collected. 62 | // For additional cleanup logic use finalizers. 63 | return ctrl.Result{}, nil 64 | } 65 | // Error reading the object - requeue the req. 66 | return ctrl.Result{}, err 67 | } 68 | 69 | changed := prometheusExporter.WithDefaults() 70 | if changed { 71 | r.Log.Info("Setting default settings for Solr PrometheusExporter", "namespace", prometheusExporter.Namespace, "name", prometheusExporter.Name) 72 | if err := r.Update(context.TODO(), prometheusExporter); err != nil { 73 | return ctrl.Result{}, err 74 | } 75 | return ctrl.Result{Requeue: true}, nil 76 | } 77 | 78 | if prometheusExporter.Spec.Config != "" { 79 | // Generate ConfigMap 80 | configMap := util.GenerateMetricsConfigMap(prometheusExporter) 81 | if err := controllerutil.SetControllerReference(prometheusExporter, configMap, r.scheme); err != nil { 82 | return ctrl.Result{}, err 83 | } 84 | 85 | // Check if the ConfigMap already exists 86 | foundConfigMap := &corev1.ConfigMap{} 87 | err = r.Get(context.TODO(), types.NamespacedName{Name: configMap.Name, Namespace: configMap.Namespace}, foundConfigMap) 88 | if err != nil && errors.IsNotFound(err) { 89 | r.Log.Info("Creating PrometheusExporter ConfigMap", "namespace", configMap.Namespace, "name", configMap.Name) 90 | err = r.Create(context.TODO(), configMap) 91 | } else if err == nil && util.CopyConfigMapFields(configMap, foundConfigMap) { 92 | // Update the found ConfigMap and write the result back if there are any changes 93 | r.Log.Info("Updating PrometheusExporter ConfigMap", "namespace", configMap.Namespace, "name", configMap.Name) 94 | err = r.Update(context.TODO(), foundConfigMap) 95 | } 96 | if err != nil { 97 | return ctrl.Result{}, err 98 | } 99 | } 100 | 101 | // Generate Metrics Service 102 | metricsService := util.GenerateSolrMetricsService(prometheusExporter) 103 | if err := controllerutil.SetControllerReference(prometheusExporter, metricsService, r.scheme); err != nil { 104 | return ctrl.Result{}, err 105 | } 106 | 107 | // Check if the Metrics Service already exists 108 | foundMetricsService := &corev1.Service{} 109 | err = r.Get(context.TODO(), types.NamespacedName{Name: metricsService.Name, Namespace: metricsService.Namespace}, foundMetricsService) 110 | if err != nil && errors.IsNotFound(err) { 111 | r.Log.Info("Creating PrometheusExporter Service", "namespace", metricsService.Namespace, "name", metricsService.Name) 112 | err = r.Create(context.TODO(), metricsService) 113 | } else if err == nil && util.CopyServiceFields(metricsService, foundMetricsService) { 114 | // Update the found Metrics Service and write the result back if there are any changes 115 | r.Log.Info("Updating PrometheusExporter Service", "namespace", metricsService.Namespace, "name", metricsService.Name) 116 | err = r.Update(context.TODO(), foundMetricsService) 117 | } 118 | if err != nil { 119 | return ctrl.Result{}, err 120 | } 121 | 122 | // Get the ZkConnectionString to connect to 123 | solrConnectionInfo := util.SolrConnectionInfo{} 124 | if solrConnectionInfo, err = getSolrConnectionInfo(r, prometheusExporter); err != nil { 125 | return ctrl.Result{}, err 126 | } 127 | 128 | deploy := util.GenerateSolrPrometheusExporterDeployment(prometheusExporter, solrConnectionInfo) 129 | if err := controllerutil.SetControllerReference(prometheusExporter, deploy, r.scheme); err != nil { 130 | return ctrl.Result{}, err 131 | } 132 | 133 | foundDeploy := &appsv1.Deployment{} 134 | err = r.Get(context.TODO(), types.NamespacedName{Name: deploy.Name, Namespace: deploy.Namespace}, foundDeploy) 135 | if err != nil && errors.IsNotFound(err) { 136 | r.Log.Info("Creating PrometheusExporter Deployment", "namespace", deploy.Namespace, "name", deploy.Name) 137 | err = r.Create(context.TODO(), deploy) 138 | } else if err == nil { 139 | if util.CopyDeploymentFields(deploy, foundDeploy) { 140 | r.Log.Info("Updating PrometheusExporter Deployment", "namespace", deploy.Namespace, "name", deploy.Name) 141 | err = r.Update(context.TODO(), foundDeploy) 142 | if err != nil { 143 | return ctrl.Result{}, err 144 | } 145 | } 146 | ready := foundDeploy.Status.ReadyReplicas > 0 147 | 148 | if ready != prometheusExporter.Status.Ready { 149 | prometheusExporter.Status.Ready = ready 150 | r.Log.Info("Updating status for solr-prometheus-exporter", "namespace", prometheusExporter.Namespace, "name", prometheusExporter.Name) 151 | err = r.Status().Update(context.TODO(), prometheusExporter) 152 | } 153 | } 154 | return ctrl.Result{}, err 155 | } 156 | 157 | func getSolrConnectionInfo(r *SolrPrometheusExporterReconciler, prometheusExporter *solrv1beta1.SolrPrometheusExporter) (solrConnectionInfo util.SolrConnectionInfo, err error) { 158 | solrConnectionInfo = util.SolrConnectionInfo{} 159 | 160 | if prometheusExporter.Spec.SolrReference.Standalone != nil { 161 | solrConnectionInfo.StandaloneAddress = prometheusExporter.Spec.SolrReference.Standalone.Address 162 | } 163 | if prometheusExporter.Spec.SolrReference.Cloud != nil { 164 | if prometheusExporter.Spec.SolrReference.Cloud.ZookeeperConnectionInfo != nil { 165 | solrConnectionInfo.CloudZkConnnectionString = prometheusExporter.Spec.SolrReference.Cloud.ZookeeperConnectionInfo.ZkConnectionString() 166 | } else if prometheusExporter.Spec.SolrReference.Cloud.Name != "" { 167 | solrCloud := &solrv1beta1.SolrCloud{} 168 | err = r.Get(context.TODO(), types.NamespacedName{Name: prometheusExporter.Spec.SolrReference.Cloud.Name, Namespace: prometheusExporter.Spec.SolrReference.Cloud.Namespace}, solrCloud) 169 | if err == nil { 170 | solrConnectionInfo.CloudZkConnnectionString = solrCloud.Status.ZookeeperConnectionInfo.ZkConnectionString() 171 | } 172 | } 173 | } 174 | return solrConnectionInfo, err 175 | } 176 | 177 | func (r *SolrPrometheusExporterReconciler) SetupWithManager(mgr ctrl.Manager) error { 178 | r.scheme = mgr.GetScheme() 179 | return ctrl.NewControllerManagedBy(mgr). 180 | For(&solrv1beta1.SolrPrometheusExporter{}). 181 | Complete(r) 182 | } 183 | -------------------------------------------------------------------------------- /controllers/util/prometheus_exporter_util.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Bloomberg Finance LP. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | */ 14 | 15 | package util 16 | 17 | import ( 18 | "reflect" 19 | "strconv" 20 | 21 | solr "github.com/bloomberg/solr-operator/api/v1beta1" 22 | appsv1 "k8s.io/api/apps/v1" 23 | corev1 "k8s.io/api/core/v1" 24 | extv1 "k8s.io/api/extensions/v1beta1" 25 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 26 | "k8s.io/apimachinery/pkg/util/intstr" 27 | ) 28 | 29 | const ( 30 | SolrMetricsPort = 8080 31 | SolrMetricsPortName = "solr-metrics" 32 | ExtSolrMetricsPort = 80 33 | ExtSolrMetricsPortName = "ext-solr-metrics" 34 | 35 | DefaultPrometheusExporterEntrypoint = "/opt/solr/contrib/prometheus-exporter/bin/solr-exporter" 36 | ) 37 | 38 | // SolrConnectionInfo defines how to connect to a cloud or standalone solr instance. 39 | // One, and only one, of Cloud or Standalone must be provided. 40 | type SolrConnectionInfo struct { 41 | CloudZkConnnectionString string 42 | StandaloneAddress string 43 | } 44 | 45 | // GenerateSolrPrometheusExporterDeployment returns a new appsv1.Deployment pointer generated for the SolrCloud Prometheus Exporter instance 46 | // solrPrometheusExporter: SolrPrometheusExporter instance 47 | func GenerateSolrPrometheusExporterDeployment(solrPrometheusExporter *solr.SolrPrometheusExporter, solrConnectionInfo SolrConnectionInfo) *appsv1.Deployment { 48 | gracePeriodTerm := int64(10) 49 | singleReplica := int32(1) 50 | fsGroup := int64(SolrMetricsPort) 51 | 52 | labels := solrPrometheusExporter.SharedLabelsWith(solrPrometheusExporter.GetLabels()) 53 | selectorLabels := solrPrometheusExporter.SharedLabels() 54 | 55 | labels["technology"] = solr.SolrPrometheusExporterTechnologyLabel 56 | selectorLabels["technology"] = solr.SolrPrometheusExporterTechnologyLabel 57 | 58 | var solrVolumes []corev1.Volume 59 | var volumeMounts []corev1.VolumeMount 60 | exporterArgs := []string{ 61 | "-p", strconv.Itoa(SolrMetricsPort), 62 | "-n", strconv.Itoa(int(solrPrometheusExporter.Spec.NumThreads)), 63 | } 64 | 65 | if solrPrometheusExporter.Spec.ScrapeInterval > 0 { 66 | exporterArgs = append(exporterArgs, "-s", strconv.Itoa(int(solrPrometheusExporter.Spec.ScrapeInterval))) 67 | } 68 | 69 | // Setup the solrConnectionInfo 70 | if solrConnectionInfo.CloudZkConnnectionString != "" { 71 | exporterArgs = append(exporterArgs, "-z", solrConnectionInfo.CloudZkConnnectionString) 72 | } else if solrConnectionInfo.StandaloneAddress != "" { 73 | exporterArgs = append(exporterArgs, "-b", solrConnectionInfo.StandaloneAddress) 74 | } 75 | 76 | // Only add the config if it is passed in from the user. Otherwise, use the default. 77 | if solrPrometheusExporter.Spec.Config != "" { 78 | solrVolumes = []corev1.Volume{{ 79 | Name: "solr-prometheus-exporter-xml", 80 | VolumeSource: corev1.VolumeSource{ 81 | ConfigMap: &corev1.ConfigMapVolumeSource{ 82 | LocalObjectReference: corev1.LocalObjectReference{ 83 | Name: solrPrometheusExporter.MetricsConfigMapName(), 84 | }, 85 | Items: []corev1.KeyToPath{ 86 | { 87 | Key: "solr-prometheus-exporter.xml", 88 | Path: "solr-prometheus-exporter.xml", 89 | }, 90 | }, 91 | }, 92 | }, 93 | }} 94 | 95 | volumeMounts = []corev1.VolumeMount{{Name: "solr-prometheus-exporter-xml", MountPath: "/opt/solr-exporter", ReadOnly: true}} 96 | 97 | exporterArgs = append(exporterArgs, "-f", "/opt/solr-exporter/solr-prometheus-exporter.xml") 98 | } else { 99 | exporterArgs = append(exporterArgs, "-f", "/opt/solr/contrib/prometheus-exporter/conf/solr-exporter-config.xml") 100 | } 101 | 102 | entrypoint := DefaultPrometheusExporterEntrypoint 103 | if solrPrometheusExporter.Spec.ExporterEntrypoint != "" { 104 | entrypoint = solrPrometheusExporter.Spec.ExporterEntrypoint 105 | } 106 | 107 | deployment := &appsv1.Deployment{ 108 | ObjectMeta: metav1.ObjectMeta{ 109 | Name: solrPrometheusExporter.MetricsDeploymentName(), 110 | Namespace: solrPrometheusExporter.GetNamespace(), 111 | Labels: labels, 112 | }, 113 | Spec: appsv1.DeploymentSpec{ 114 | Selector: &metav1.LabelSelector{ 115 | MatchLabels: selectorLabels, 116 | }, 117 | Replicas: &singleReplica, 118 | Template: corev1.PodTemplateSpec{ 119 | ObjectMeta: metav1.ObjectMeta{ 120 | Labels: labels, 121 | }, 122 | Spec: corev1.PodSpec{ 123 | TerminationGracePeriodSeconds: &gracePeriodTerm, 124 | SecurityContext: &corev1.PodSecurityContext{ 125 | FSGroup: &fsGroup, 126 | }, 127 | Volumes: solrVolumes, 128 | Containers: []corev1.Container{ 129 | { 130 | Name: "solr-prometheus-exporter", 131 | Image: solrPrometheusExporter.Spec.Image.ToImageName(), 132 | ImagePullPolicy: solrPrometheusExporter.Spec.Image.PullPolicy, 133 | Ports: []corev1.ContainerPort{{ContainerPort: SolrMetricsPort, Name: SolrMetricsPortName}}, 134 | VolumeMounts: volumeMounts, 135 | Command: []string{entrypoint}, 136 | Args: exporterArgs, 137 | 138 | LivenessProbe: &corev1.Probe{ 139 | InitialDelaySeconds: 20, 140 | PeriodSeconds: 10, 141 | Handler: corev1.Handler{ 142 | HTTPGet: &corev1.HTTPGetAction{ 143 | Scheme: corev1.URISchemeHTTP, 144 | Path: "/metrics", 145 | Port: intstr.FromInt(SolrMetricsPort), 146 | }, 147 | }, 148 | }, 149 | }, 150 | }, 151 | }, 152 | }, 153 | }, 154 | } 155 | 156 | if solrPrometheusExporter.Spec.Image.ImagePullSecret != "" { 157 | deployment.Spec.Template.Spec.ImagePullSecrets = []corev1.LocalObjectReference{ 158 | {Name: solrPrometheusExporter.Spec.Image.ImagePullSecret}, 159 | } 160 | } 161 | 162 | if solrPrometheusExporter.Spec.PodPolicy.Resources.Limits != nil || solrPrometheusExporter.Spec.PodPolicy.Resources.Requests != nil { 163 | deployment.Spec.Template.Spec.Containers[0].Resources = solrPrometheusExporter.Spec.PodPolicy.Resources 164 | } 165 | 166 | return deployment 167 | } 168 | 169 | // GenerateConfigMap returns a new corev1.ConfigMap pointer generated for the Solr Prometheus Exporter instance solr-prometheus-exporter.xml 170 | // solrPrometheusExporter: SolrPrometheusExporter instance 171 | func GenerateMetricsConfigMap(solrPrometheusExporter *solr.SolrPrometheusExporter) *corev1.ConfigMap { 172 | labels := solrPrometheusExporter.SharedLabelsWith(solrPrometheusExporter.GetLabels()) 173 | 174 | configMap := &corev1.ConfigMap{ 175 | ObjectMeta: metav1.ObjectMeta{ 176 | Name: solrPrometheusExporter.MetricsConfigMapName(), 177 | Namespace: solrPrometheusExporter.GetNamespace(), 178 | Labels: labels, 179 | }, 180 | Data: map[string]string{ 181 | "solr-prometheus-exporter.xml": solrPrometheusExporter.Spec.Config, 182 | }, 183 | } 184 | return configMap 185 | } 186 | 187 | // CopyConfigMapFields copies the owned fields from one ConfigMap to another 188 | func CopyMetricsConfigMapFields(from, to *corev1.ConfigMap) bool { 189 | requireUpdate := false 190 | for k, v := range from.Labels { 191 | if to.Labels[k] != v { 192 | requireUpdate = true 193 | } 194 | to.Labels[k] = v 195 | } 196 | 197 | for k, v := range from.Annotations { 198 | if to.Annotations[k] != v { 199 | requireUpdate = true 200 | } 201 | to.Annotations[k] = v 202 | } 203 | 204 | // Don't copy the entire Spec, because we can't overwrite the clusterIp field 205 | 206 | if !reflect.DeepEqual(to.Data, from.Data) { 207 | requireUpdate = true 208 | } 209 | to.Data = from.Data 210 | 211 | return requireUpdate 212 | } 213 | 214 | // GenerateSolrMetricsService returns a new corev1.Service pointer generated for the SolrCloud Prometheus Exporter deployment 215 | // Metrics will be collected on this service endpoint, as we don't want to double-tick data if multiple exporters are runnning. 216 | // solrPrometheusExporter: solrPrometheusExporter instance 217 | func GenerateSolrMetricsService(solrPrometheusExporter *solr.SolrPrometheusExporter) *corev1.Service { 218 | copyLabels := solrPrometheusExporter.GetLabels() 219 | if copyLabels == nil { 220 | copyLabels = map[string]string{} 221 | } 222 | labels := solrPrometheusExporter.SharedLabelsWith(solrPrometheusExporter.GetLabels()) 223 | labels["service-type"] = "metrics" 224 | 225 | selectorLabels := solrPrometheusExporter.SharedLabels() 226 | selectorLabels["technology"] = solr.SolrPrometheusExporterTechnologyLabel 227 | 228 | service := &corev1.Service{ 229 | ObjectMeta: metav1.ObjectMeta{ 230 | Name: solrPrometheusExporter.MetricsServiceName(), 231 | Namespace: solrPrometheusExporter.GetNamespace(), 232 | Labels: labels, 233 | Annotations: map[string]string{ 234 | "prometheus.io/scrape": "true", 235 | "prometheus.io/scheme": "http", 236 | "prometheus.io/path": "/metrics", 237 | "prometheus.io/port": strconv.Itoa(ExtSolrMetricsPort), 238 | }, 239 | }, 240 | Spec: corev1.ServiceSpec{ 241 | Ports: []corev1.ServicePort{ 242 | {Name: ExtSolrMetricsPortName, Port: ExtSolrMetricsPort, Protocol: corev1.ProtocolTCP, TargetPort: intstr.FromInt(SolrMetricsPort)}, 243 | }, 244 | Selector: selectorLabels, 245 | }, 246 | } 247 | return service 248 | } 249 | 250 | // CreateMetricsIngressRule returns a new Ingress Rule generated for the solr metrics endpoint 251 | // This is not currently used, as an ingress is not created for the metrics endpoint. 252 | 253 | // solrCloud: SolrCloud instance 254 | // nodeName: string Name of the node 255 | // ingressBaseDomain: string base domain for the ingress controller 256 | func CreateMetricsIngressRule(solrPrometheusExporter *solr.SolrPrometheusExporter, ingressBaseDomain string) extv1.IngressRule { 257 | externalAddress := solrPrometheusExporter.MetricsIngressUrl(ingressBaseDomain) 258 | return extv1.IngressRule{ 259 | Host: externalAddress, 260 | IngressRuleValue: extv1.IngressRuleValue{ 261 | HTTP: &extv1.HTTPIngressRuleValue{ 262 | Paths: []extv1.HTTPIngressPath{ 263 | { 264 | Backend: extv1.IngressBackend{ 265 | ServiceName: solrPrometheusExporter.MetricsServiceName(), 266 | ServicePort: intstr.FromInt(ExtSolrMetricsPort), 267 | }, 268 | }, 269 | }, 270 | }, 271 | }, 272 | } 273 | } 274 | -------------------------------------------------------------------------------- /api/v1beta1/solrbackup_types.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Bloomberg Finance LP. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package v1beta1 18 | 19 | import ( 20 | "fmt" 21 | corev1 "k8s.io/api/core/v1" 22 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 23 | "strings" 24 | ) 25 | 26 | // EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! 27 | // NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. 28 | 29 | const ( 30 | DefaultAWSCliImageRepo = "infrastructureascode/aws-cli" 31 | DefaultAWSCliImageVersion = "1.16.204" 32 | DefaultS3Retries = 5 33 | ) 34 | 35 | // SolrBackupSpec defines the desired state of SolrBackup 36 | type SolrBackupSpec struct { 37 | // A reference to the SolrCloud to create a backup for 38 | SolrCloud string `json:"solrCloud"` 39 | 40 | // The list of collections to backup. If empty, all collections in the cloud will be backed up. 41 | // +optional 42 | Collections []string `json:"collections,omitempty"` 43 | 44 | // Persistence is the specification on how to persist the backup data. 45 | Persistence PersistenceSource `json:"persistence"` 46 | } 47 | 48 | func (spec *SolrBackupSpec) withDefaults(backupName string) (changed bool) { 49 | changed = spec.Persistence.withDefaults(backupName) || changed 50 | 51 | return changed 52 | } 53 | 54 | // PersistenceSource defines the location and method of persisting the backup data. 55 | // Exactly one member must be specified. 56 | type PersistenceSource struct { 57 | // Persist to an s3 compatible endpoint 58 | // +optional 59 | S3 *S3PersistenceSource `json:",omitempty"` 60 | 61 | // Persist to a volume 62 | // +optional 63 | Volume *VolumePersistenceSource `json:"volume,omitempty"` 64 | } 65 | 66 | func (spec *PersistenceSource) withDefaults(backupName string) (changed bool) { 67 | if spec.Volume != nil { 68 | changed = spec.Volume.withDefaults(backupName) || changed 69 | } 70 | 71 | if spec.S3 != nil { 72 | changed = spec.S3.withDefaults(backupName) || changed 73 | } 74 | 75 | return changed 76 | } 77 | 78 | // S3PersistenceSource defines the specs for connecting to s3 for persistence 79 | type S3PersistenceSource struct { 80 | // The S3 compatible endpoint URL 81 | // +optional 82 | EndpointUrl string `json:"endpointUrl,omitempty"` 83 | 84 | // The Default region to use with AWS. 85 | // Can also be provided through a configFile in the secrets. 86 | // Overridden by any endpointUrl value provided. 87 | // +optional 88 | Region string `json:"region,omitempty"` 89 | 90 | // The S3 bucket to store/find the backup data 91 | Bucket string `json:"bucket"` 92 | 93 | // The key for the referenced tarred & zipped backup file 94 | // Defaults to the name of the backup/restore + '.tgz' 95 | // +optional 96 | Key string `json:"key"` 97 | 98 | // The number of retries to communicate with S3 99 | // +optional 100 | Retries *int32 `json:"retries,omitempty"` 101 | 102 | // The secrets to use when configuring and authenticating s3 calls 103 | Secrets S3Secrets `json:"secrets"` 104 | 105 | // Image containing the AWS Cli 106 | // +optional 107 | AWSCliImage ContainerImage `json:"AWSCliImage,omitempty"` 108 | } 109 | 110 | func (spec *S3PersistenceSource) withDefaults(backupName string) (changed bool) { 111 | changed = spec.AWSCliImage.withDefaults(DefaultAWSCliImageRepo, DefaultAWSCliImageVersion, DefaultPullPolicy) || changed 112 | 113 | if spec.Key == "" { 114 | spec.Key = backupName + ".tgz" 115 | changed = true 116 | } else if strings.HasPrefix(spec.Key, "/") { 117 | spec.Key = strings.TrimPrefix(spec.Key, "/") 118 | changed = true 119 | } 120 | if spec.Retries == nil { 121 | retries := int32(DefaultS3Retries) 122 | spec.Retries = &retries 123 | changed = true 124 | } 125 | 126 | return changed 127 | } 128 | 129 | // S3Secrets describes the secrets provided for accessing s3. 130 | type S3Secrets struct { 131 | // The name of the secrets object to use 132 | Name string `json:"fromSecret"` 133 | 134 | // The key (within the provided secret) of an AWS Config file to use 135 | // +optional 136 | ConfigFile string `json:"configFile,omitempty"` 137 | 138 | // The key (within the provided secret) of an AWS Credentials file to use 139 | // +optional 140 | CredentialsFile string `json:"credentialsFile,omitempty"` 141 | 142 | // The key (within the provided secret) of the Access Key ID to use 143 | // +optional 144 | AccessKeyId string `json:"accessKeyId,omitempty"` 145 | 146 | // The key (within the provided secret) of the Secret Access Key to use 147 | // +optional 148 | SecretAccessKey string `json:"secretAccessKey,omitempty"` 149 | } 150 | 151 | // UploadSpec defines the location and method of uploading the backup data 152 | type VolumePersistenceSource struct { 153 | // The volume for persistence 154 | VolumeSource corev1.VolumeSource `json:"source"` 155 | 156 | // The location of the persistence directory within the volume 157 | // +optional 158 | Path string `json:"path,omitempty"` 159 | 160 | // The filename of the tarred & zipped backup file 161 | // Defaults to the name of the backup/restore + '.tgz' 162 | // +optional 163 | Filename string `json:"filename"` 164 | 165 | // BusyBox image for manipulating and moving data 166 | // +optional 167 | BusyBoxImage ContainerImage `json:"busyBoxImage,omitempty"` 168 | } 169 | 170 | func (spec *VolumePersistenceSource) withDefaults(backupName string) (changed bool) { 171 | changed = spec.BusyBoxImage.withDefaults(DefaultBusyBoxImageRepo, DefaultBusyBoxImageVersion, DefaultPullPolicy) || changed 172 | 173 | if spec.Path != "" && strings.HasPrefix(spec.Path, "/") { 174 | spec.Path = strings.TrimPrefix(spec.Path, "/") 175 | changed = true 176 | } 177 | 178 | if spec.Filename != "" { 179 | spec.Filename = backupName + ".tgz" 180 | changed = true 181 | } 182 | 183 | return changed 184 | } 185 | 186 | // SolrBackupStatus defines the observed state of SolrBackup 187 | type SolrBackupStatus struct { 188 | // Version of the Solr being backed up 189 | SolrVersion string `json:"solrVersion"` 190 | 191 | // The status of each collection's backup progress 192 | // +optional 193 | CollectionBackupStatuses []CollectionBackupStatus `json:"collectionBackupStatuses,omitempty"` 194 | 195 | // Whether the backups are in progress of being persisted 196 | PersistenceStatus BackupPersistenceStatus `json:"persistenceStatus"` 197 | 198 | // Version of the Solr being backed up 199 | // +optional 200 | FinishTime *metav1.Time `json:"finishTimestamp,omitempty"` 201 | 202 | // Whether the backup was successful 203 | // +optional 204 | Successful *bool `json:"successful,omitempty"` 205 | 206 | // Whether the backup has finished 207 | Finished bool `json:"finished,omitempty"` 208 | } 209 | 210 | // CollectionBackupStatus defines the progress of a Solr Collection's backup 211 | type CollectionBackupStatus struct { 212 | // Solr Collection name 213 | Collection string `json:"collection"` 214 | 215 | // Whether the collection is being backed up 216 | // +optional 217 | InProgress bool `json:"inProgress,omitempty"` 218 | 219 | // Time that the collection backup started at 220 | // +optional 221 | StartTime *metav1.Time `json:"startTimestamp,omitempty"` 222 | 223 | // The status of the asynchronous backup call to solr 224 | // +optional 225 | AsyncBackupStatus string `json:"asyncBackupStatus,omitempty"` 226 | 227 | // Whether the backup has finished 228 | Finished bool `json:"finished,omitempty"` 229 | 230 | // Time that the collection backup finished at 231 | // +optional 232 | FinishTime *metav1.Time `json:"finishTimestamp,omitempty"` 233 | 234 | // Whether the backup was successful 235 | // +optional 236 | Successful *bool `json:"successful,omitempty"` 237 | } 238 | 239 | // BackupPersistenceStatus defines the status of persisting Solr backup data 240 | type BackupPersistenceStatus struct { 241 | // Whether the collection is being backed up 242 | // +optional 243 | InProgress bool `json:"inProgress,omitempty"` 244 | 245 | // Time that the collection backup started at 246 | // +optional 247 | StartTime *metav1.Time `json:"startTimestamp,omitempty"` 248 | 249 | // Whether the persistence has finished 250 | Finished bool `json:"finished,omitempty"` 251 | 252 | // Time that the collection backup finished at 253 | // +optional 254 | FinishTime *metav1.Time `json:"finishTimestamp,omitempty"` 255 | 256 | // Whether the backup was successful 257 | // +optional 258 | Successful *bool `json:"successful,omitempty"` 259 | } 260 | 261 | func (sb *SolrBackup) SharedLabels() map[string]string { 262 | return sb.SharedLabelsWith(map[string]string{}) 263 | } 264 | 265 | func (sb *SolrBackup) SharedLabelsWith(labels map[string]string) map[string]string { 266 | newLabels := map[string]string{} 267 | 268 | if labels != nil { 269 | for k, v := range labels { 270 | newLabels[k] = v 271 | } 272 | } 273 | 274 | newLabels["solr-backup"] = sb.Name 275 | return newLabels 276 | } 277 | 278 | // HeadlessServiceName returns the name of the headless service for the cloud 279 | func (sb *SolrBackup) PersistenceJobName() string { 280 | return fmt.Sprintf("%s-solr-backup-persistence", sb.GetName()) 281 | } 282 | 283 | // +kubebuilder:object:root=true 284 | // SolrBackup is the Schema for the solrbackups API 285 | // +kubebuilder:categories=all 286 | // +kubebuilder:subresource:status 287 | // +kubebuilder:printcolumn:name="Cloud",type="string",JSONPath=".spec.solrCloud",description="Solr Cloud" 288 | // +kubebuilder:printcolumn:name="Finished",type="boolean",JSONPath=".status.finished",description="Whether the backup has finished" 289 | // +kubebuilder:printcolumn:name="Successful",type="boolean",JSONPath=".status.successful",description="Whether the backup was successful" 290 | // +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" 291 | type SolrBackup struct { 292 | metav1.TypeMeta `json:",inline"` 293 | metav1.ObjectMeta `json:"metadata,omitempty"` 294 | 295 | Spec SolrBackupSpec `json:"spec,omitempty"` 296 | Status SolrBackupStatus `json:"status,omitempty"` 297 | } 298 | 299 | // WithDefaults set default values when not defined in the spec. 300 | func (sb *SolrBackup) WithDefaults() bool { 301 | return sb.Spec.withDefaults(sb.Name) 302 | } 303 | 304 | // +kubebuilder:object:root=true 305 | 306 | // SolrBackupList contains a list of SolrBackup 307 | type SolrBackupList struct { 308 | metav1.TypeMeta `json:",inline"` 309 | metav1.ListMeta `json:"metadata,omitempty"` 310 | Items []SolrBackup `json:"items"` 311 | } 312 | 313 | func init() { 314 | SchemeBuilder.Register(&SolrBackup{}, &SolrBackupList{}) 315 | } 316 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2017 Bloomberg Finance L.P. 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. -------------------------------------------------------------------------------- /controllers/util/collection_util.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Bloomberg Finance LP. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package util 18 | 19 | import ( 20 | "net/url" 21 | "strconv" 22 | "strings" 23 | ) 24 | 25 | // CreateCollection to request collection creation on SolrCloud 26 | func CreateCollection(cloud string, collection string, numShards int64, replicationFactor int64, autoAddReplicas bool, routerName string, routerField string, shards string, collectionConfigName string, namespace string) (success bool, err error) { 27 | queryParams := url.Values{} 28 | replicationFactorParameter := strconv.FormatInt(replicationFactor, 10) 29 | numShardsParameter := strconv.FormatInt(numShards, 10) 30 | queryParams.Add("action", "CREATE") 31 | queryParams.Add("name", collection) 32 | queryParams.Add("replicationFactor", replicationFactorParameter) 33 | queryParams.Add("autoAddReplicas", strconv.FormatBool(autoAddReplicas)) 34 | queryParams.Add("collection.configName", collectionConfigName) 35 | queryParams.Add("router.field", routerField) 36 | 37 | if routerName == "implicit" { 38 | queryParams.Add("router.name", routerName) 39 | queryParams.Add("shards", shards) 40 | } else if routerName == "compositeId" { 41 | queryParams.Add("router.name", routerName) 42 | queryParams.Add("numShards", numShardsParameter) 43 | } else { 44 | log.Info("router.name must be either compositeId or implicit. Provided: ", routerName) 45 | } 46 | 47 | resp := &SolrAsyncResponse{} 48 | 49 | log.Info("Calling to create collection", "namespace", namespace, "cloud", cloud, "collection", collection) 50 | err = CallCollectionsApi(cloud, namespace, queryParams, resp) 51 | 52 | if err == nil { 53 | if resp.ResponseHeader.Status == 0 { 54 | success = true 55 | } 56 | } else { 57 | log.Error(err, "Error creating collection", "namespace", namespace, "cloud", cloud, "collection", collection) 58 | } 59 | 60 | return success, err 61 | } 62 | 63 | // CreateCollecttionAlias to request the creation of an alias to one or more collections 64 | func CreateCollecttionAlias(cloud string, alias string, aliasType string, collections []string, namespace string) (err error) { 65 | queryParams := url.Values{} 66 | collectionsArray := strings.Join(collections, ",") 67 | queryParams.Add("action", "CREATEALIAS") 68 | queryParams.Add("name", alias) 69 | queryParams.Add("collections", collectionsArray) 70 | 71 | resp := &SolrAsyncResponse{} 72 | 73 | log.Info("Calling to create alias", "namespace", namespace, "cloud", cloud, "alias", alias, "to collections", collectionsArray) 74 | err = CallCollectionsApi(cloud, namespace, queryParams, resp) 75 | 76 | if err == nil { 77 | if resp.ResponseHeader.Status == 0 { 78 | log.Info("ResponseHeader.Status", "ResponseHeader.Status", resp.ResponseHeader.Status) 79 | } 80 | } else { 81 | log.Error(err, "Error creating alias", "namespace", namespace, "cloud", cloud, "alias", alias, "to collections", collectionsArray) 82 | } 83 | 84 | return err 85 | 86 | } 87 | 88 | // DeleteCollection to request collection deletion on SolrCloud 89 | func DeleteCollection(cloud string, collection string, namespace string) (success bool, err error) { 90 | queryParams := url.Values{} 91 | queryParams.Add("action", "DELETE") 92 | queryParams.Add("name", collection) 93 | 94 | resp := &SolrAsyncResponse{} 95 | 96 | log.Info("Calling to delete collection", "namespace", namespace, "cloud", cloud, "collection", collection) 97 | err = CallCollectionsApi(cloud, namespace, queryParams, resp) 98 | 99 | if err == nil { 100 | if resp.ResponseHeader.Status == 0 { 101 | success = true 102 | } 103 | } else { 104 | log.Error(err, "Error deleting collection", "namespace", namespace, "cloud", cloud, "collection") 105 | } 106 | 107 | return success, err 108 | } 109 | 110 | // DeleteCollectionAlias removes an alias 111 | func DeleteCollectionAlias(cloud string, alias string, namespace string) (success bool, err error) { 112 | queryParams := url.Values{} 113 | queryParams.Add("action", "DELETEALIAS") 114 | queryParams.Add("name", alias) 115 | 116 | resp := &SolrAsyncResponse{} 117 | 118 | log.Info("Calling to delete collection alias", "namespace", namespace, "cloud", cloud, "alias", alias) 119 | err = CallCollectionsApi(cloud, namespace, queryParams, resp) 120 | 121 | if err == nil { 122 | if resp.ResponseHeader.Status == 0 { 123 | success = true 124 | } 125 | } else { 126 | log.Error(err, "Error deleting collection alias", "namespace", namespace, "cloud", cloud, "alias", alias) 127 | } 128 | 129 | return success, err 130 | } 131 | 132 | // ModifyCollection to request collection modification on SolrCloud. 133 | func ModifyCollection(cloud string, collection string, replicationFactor int64, autoAddReplicas bool, maxShardsPerNode int64, collectionConfigName string, namespace string) (success bool, err error) { 134 | queryParams := url.Values{} 135 | replicationFactorParameter := strconv.FormatInt(replicationFactor, 10) 136 | maxShardsPerNodeParameter := strconv.FormatInt(maxShardsPerNode, 10) 137 | queryParams.Add("action", "MODIFYCOLLECTION") 138 | queryParams.Add("collection", collection) 139 | queryParams.Add("replicationFactor", replicationFactorParameter) 140 | queryParams.Add("maxShardsPerNode", maxShardsPerNodeParameter) 141 | queryParams.Add("autoAddReplicas", strconv.FormatBool(autoAddReplicas)) 142 | queryParams.Add("collection.configName", collectionConfigName) 143 | 144 | resp := &SolrAsyncResponse{} 145 | 146 | log.Info("Calling to modify collection", "namespace", namespace, "cloud", cloud, "collection", collection) 147 | err = CallCollectionsApi(cloud, namespace, queryParams, resp) 148 | 149 | if err == nil { 150 | if resp.ResponseHeader.Status == 0 { 151 | success = true 152 | } 153 | } else { 154 | log.Error(err, "Error modifying collection", "namespace", namespace, "cloud", cloud, "collection") 155 | } 156 | 157 | return success, err 158 | } 159 | 160 | // CheckIfCollectionModificationRequired to check if the collection's modifiable parameters have changed in spec and need to be updated 161 | func CheckIfCollectionModificationRequired(cloud string, collection string, replicationFactor int64, autoAddReplicas bool, maxShardsPerNode int64, collectionConfigName string, namespace string) (success bool, err error) { 162 | queryParams := url.Values{} 163 | replicationFactorParameter := strconv.FormatInt(replicationFactor, 10) 164 | maxShardsPerNodeParameter := strconv.FormatInt(maxShardsPerNode, 10) 165 | autoAddReplicasParameter := strconv.FormatBool(autoAddReplicas) 166 | success = false 167 | queryParams.Add("action", "CLUSTERSTATUS") 168 | queryParams.Add("collection", collection) 169 | 170 | resp := &SolrClusterStatusResponse{} 171 | 172 | err = CallCollectionsApi(cloud, namespace, queryParams, &resp) 173 | 174 | if collectionResp, ok := resp.Cluster.Collections[collection].(map[string]interface{}); ok { 175 | // Check modifiable collection parameters 176 | if collectionResp["autoAddReplicas"] != autoAddReplicasParameter { 177 | log.Info("Collection modification required, autoAddReplicas changed", "autoAddReplicas", autoAddReplicasParameter) 178 | success = true 179 | } 180 | 181 | if collectionResp["maxShardsPerNode"] != maxShardsPerNodeParameter { 182 | log.Info("Collection modification required, maxShardsPerNode changed", "maxShardsPerNode", maxShardsPerNodeParameter) 183 | success = true 184 | } 185 | 186 | if collectionResp["replicationFactor"] != replicationFactorParameter { 187 | log.Info("Collection modification required, replicationFactor changed", "replicationFactor", replicationFactorParameter) 188 | success = true 189 | } 190 | if collectionResp["configName"] != collectionConfigName { 191 | log.Info("Collection modification required, configName changed", "configName", collectionConfigName) 192 | success = true 193 | } 194 | } else { 195 | log.Error(err, "Error calling collection API status", "namespace", namespace, "cloud", cloud, "collection", collection) 196 | } 197 | 198 | return success, err 199 | } 200 | 201 | // CheckIfCollectionExists to request if collection exists in list of collection 202 | func CheckIfCollectionExists(cloud string, collection string, namespace string) (success bool) { 203 | queryParams := url.Values{} 204 | queryParams.Add("action", "LIST") 205 | 206 | resp := &SolrCollectionsListResponse{} 207 | 208 | log.Info("Calling to list collections", "namespace", namespace, "cloud", cloud, "collection", collection) 209 | err := CallCollectionsApi(cloud, namespace, queryParams, resp) 210 | 211 | if err == nil { 212 | if containsCollection(resp.Collections, collection) { 213 | success = true 214 | } 215 | } else { 216 | log.Error(err, "Error listing collections", "namespace", namespace, "cloud", cloud, "collection") 217 | } 218 | 219 | return success 220 | } 221 | 222 | // CurrentCollectionAliasDetails will return a success if details found for an alias and comma separated string of associated collections 223 | func CurrentCollectionAliasDetails(cloud string, alias string, namespace string) (success bool, collections string) { 224 | queryParams := url.Values{} 225 | queryParams.Add("action", "LISTALIASES") 226 | 227 | resp := &SolrCollectionAliasDetailsResponse{} 228 | 229 | err := CallCollectionsApi(cloud, namespace, queryParams, resp) 230 | 231 | if err == nil { 232 | success, collections := containsAlias(resp.Aliases, alias) 233 | if success { 234 | return success, collections 235 | } 236 | } 237 | 238 | return success, "" 239 | } 240 | 241 | type SolrCollectionAliasDetailsResponse struct { 242 | SolrResponseHeader SolrCollectionResponseHeader `json:"responseHeader"` 243 | 244 | // +optional 245 | Aliases map[string]string `json:"aliases"` 246 | } 247 | 248 | type SolrCollectionResponseHeader struct { 249 | Status int `json:"status"` 250 | 251 | QTime int `json:"QTime"` 252 | } 253 | 254 | type SolrCollectionAsyncStatus struct { 255 | AsyncState string `json:"state"` 256 | 257 | Message string `json:"msg"` 258 | } 259 | 260 | type SolrCollectionsListResponse struct { 261 | ResponseHeader SolrCollectionResponseHeader `json:"responseHeader"` 262 | 263 | // +optional 264 | RequestId string `json:"requestId"` 265 | 266 | // +optional 267 | Status SolrCollectionAsyncStatus `json:"status"` 268 | 269 | Collections []string `json:"collections"` 270 | } 271 | 272 | type SolrClusterStatusResponse struct { 273 | ResponseHeader SolrCollectionResponseHeader `json:"responseHeader"` 274 | 275 | Cluster SolrClusterStatusCluster `json:"cluster"` 276 | } 277 | 278 | type SolrClusterStatusCluster struct { 279 | Collections map[string]interface{} `json:"collections"` 280 | } 281 | 282 | // ContainsString helper function to test string contains 283 | func ContainsString(slice []string, s string) bool { 284 | for _, item := range slice { 285 | if item == s { 286 | return true 287 | } 288 | } 289 | return false 290 | } 291 | 292 | // RemoveString helper function to remove string 293 | func RemoveString(slice []string, s string) (result []string) { 294 | for _, item := range slice { 295 | if item == s { 296 | continue 297 | } 298 | result = append(result, item) 299 | } 300 | return 301 | } 302 | 303 | // containsCollection helper function to check if collection in list 304 | func containsCollection(collections []string, collection string) bool { 305 | for _, a := range collections { 306 | if a == collection { 307 | return true 308 | } 309 | } 310 | return false 311 | } 312 | 313 | // containsAlias helper function to check if alias defined and return success and associated collections 314 | func containsAlias(aliases map[string]string, alias string) (success bool, collections string) { 315 | for k, v := range aliases { 316 | if k == alias { 317 | return true, v 318 | } 319 | } 320 | return false, "" 321 | } 322 | -------------------------------------------------------------------------------- /controllers/solrbackup_controller.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Bloomberg Finance LP. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package controllers 18 | 19 | import ( 20 | "context" 21 | "github.com/bloomberg/solr-operator/controllers/util" 22 | "github.com/go-logr/logr" 23 | batchv1 "k8s.io/api/batch/v1" 24 | "k8s.io/apimachinery/pkg/api/errors" 25 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 26 | "k8s.io/apimachinery/pkg/runtime" 27 | "k8s.io/apimachinery/pkg/types" 28 | "k8s.io/client-go/rest" 29 | "reflect" 30 | ctrl "sigs.k8s.io/controller-runtime" 31 | "sigs.k8s.io/controller-runtime/pkg/client" 32 | "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" 33 | "sigs.k8s.io/controller-runtime/pkg/reconcile" 34 | "time" 35 | 36 | solrv1beta1 "github.com/bloomberg/solr-operator/api/v1beta1" 37 | ) 38 | 39 | var ( 40 | Config *rest.Config 41 | ) 42 | 43 | // SolrBackupReconciler reconciles a SolrBackup object 44 | type SolrBackupReconciler struct { 45 | client.Client 46 | Log logr.Logger 47 | scheme *runtime.Scheme 48 | } 49 | 50 | // +kubebuilder:rbac:groups="",resources=pods/exec,verbs=create 51 | // +kubebuilder:rbac:groups=batch,resources=jobs,verbs=get;list;watch;create;update;patch;delete 52 | // +kubebuilder:rbac:groups=batch,resources=jobs/status,verbs=get;update;patch 53 | // +kubebuilder:rbac:groups=solr.bloomberg.com,resources=solrclouds,verbs=get;list;watch 54 | // +kubebuilder:rbac:groups=solr.bloomberg.com,resources=solrclouds/status,verbs=get 55 | // +kubebuilder:rbac:groups=solr.bloomberg.com,resources=solrbackups,verbs=get;list;watch;create;update;patch;delete 56 | // +kubebuilder:rbac:groups=solr.bloomberg.com,resources=solrbackups/status,verbs=get;update;patch 57 | 58 | func (r *SolrBackupReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { 59 | _ = context.Background() 60 | _ = r.Log.WithValues("solrbackup", req.NamespacedName) 61 | // Fetch the SolrBackup instance 62 | backup := &solrv1beta1.SolrBackup{} 63 | err := r.Get(context.TODO(), req.NamespacedName, backup) 64 | if err != nil { 65 | if errors.IsNotFound(err) { 66 | // Object not found, return. Created objects are automatically garbage collected. 67 | // For additional cleanup logic use finalizers. 68 | return reconcile.Result{}, nil 69 | } 70 | // Error reading the object - requeue the req. 71 | return reconcile.Result{}, err 72 | } 73 | 74 | oldStatus := backup.Status.DeepCopy() 75 | 76 | changed := backup.WithDefaults() 77 | if changed { 78 | r.Log.Info("Setting default settings for solr-backup", "namespace", backup.Namespace, "name", backup.Name) 79 | if err := r.Update(context.TODO(), backup); err != nil { 80 | return reconcile.Result{}, err 81 | } 82 | return reconcile.Result{Requeue: true}, nil 83 | } 84 | 85 | // When working with the collection backups, auto-requeue after 5 seconds 86 | // to check on the status of the async solr backup calls 87 | requeueOrNot := reconcile.Result{Requeue: true, RequeueAfter: time.Second * 5} 88 | 89 | solrCloud, allCollectionsComplete, collectionActionTaken, err := reconcileSolrCloudBackup(r, backup) 90 | if err != nil { 91 | r.Log.Error(err, "Error while taking SolrCloud backup") 92 | } 93 | if allCollectionsComplete && collectionActionTaken { 94 | // Requeue immediately to start the persisting job 95 | // From here on in the backup lifecycle, requeueing will not happen for the backup. 96 | requeueOrNot = reconcile.Result{Requeue: true} 97 | } else if solrCloud == nil { 98 | requeueOrNot = reconcile.Result{} 99 | } else { 100 | // Only persist if the backup CRD is not finished (something bad happended) 101 | // and the collection backups are all complete (not necessarily successful) 102 | // Do not do this right after the collectionsBackup have been complete, wait till the next cycle 103 | if allCollectionsComplete && !backup.Status.Finished { 104 | // We will count on the Job updates to be notifified 105 | requeueOrNot = reconcile.Result{} 106 | err = persistSolrCloudBackups(r, backup, solrCloud) 107 | } 108 | if err != nil { 109 | r.Log.Error(err, "Error while persisting SolrCloud backup") 110 | } 111 | } 112 | 113 | if backup.Status.Finished && backup.Status.FinishTime == nil { 114 | now := metav1.Now() 115 | backup.Status.FinishTime = &now 116 | backup.Status.Successful = backup.Status.PersistenceStatus.Successful 117 | } 118 | 119 | if !reflect.DeepEqual(oldStatus, backup.Status) { 120 | r.Log.Info("Updating status for solr-backup", "namespace", backup.Namespace, "name", backup.Name) 121 | err = r.Status().Update(context.TODO(), backup) 122 | } 123 | 124 | if backup.Status.Finished { 125 | requeueOrNot = reconcile.Result{} 126 | } 127 | 128 | return requeueOrNot, err 129 | } 130 | 131 | func reconcileSolrCloudBackup(r *SolrBackupReconciler, backup *solrv1beta1.SolrBackup) (solrCloud *solrv1beta1.SolrCloud, collectionBackupsFinished bool, actionTaken bool, err error) { 132 | // Get the solrCloud that this backup is for. 133 | solrCloud = &solrv1beta1.SolrCloud{} 134 | err = r.Get(context.TODO(), types.NamespacedName{Namespace: backup.Namespace, Name: backup.Spec.SolrCloud}, solrCloud) 135 | if err != nil && errors.IsNotFound(err) { 136 | r.Log.Error(err, "Could not find cloud to backup", "namespace", backup.Namespace, "backupName", backup.Name, "solrCloudName", backup.Spec.SolrCloud) 137 | return nil, collectionBackupsFinished, actionTaken, err 138 | } else if err != nil { 139 | return nil, collectionBackupsFinished, actionTaken, err 140 | } 141 | 142 | // First check if the collection backups have been completed 143 | collectionBackupsFinished = util.CheckStatusOfCollectionBackups(backup) 144 | 145 | // If the collectionBackups are complete, then nothing else has to be done here 146 | if collectionBackupsFinished { 147 | return solrCloud, collectionBackupsFinished, actionTaken, nil 148 | } 149 | 150 | actionTaken = true 151 | 152 | // This should only occur before the backup processes have been started 153 | if backup.Status.SolrVersion == "" { 154 | // Prep the backup directory in the persistentVolume 155 | err := util.EnsureDirectoryForBackup(solrCloud, backup.Name, Config) 156 | if err != nil { 157 | return solrCloud, collectionBackupsFinished, actionTaken, err 158 | } 159 | 160 | // Make sure that all solr nodes are active and have the backupRestore shared volume mounted 161 | cloudReady := solrCloud.Status.BackupRestoreReady && (solrCloud.Status.Replicas == solrCloud.Status.ReadyReplicas) 162 | if !cloudReady { 163 | r.Log.Info("Cloud not ready for backup backup", "namespace", backup.Namespace, "cloud", solrCloud.Name, "backup", backup.Name) 164 | return solrCloud, collectionBackupsFinished, actionTaken, errors.NewServiceUnavailable("Cloud is not ready for backups or restores") 165 | } 166 | 167 | // Only set the solr version at the start of the backup. This shouldn't change throughout the backup. 168 | backup.Status.SolrVersion = solrCloud.Status.Version 169 | } 170 | 171 | // Go through each collection specified and reconcile the backup. 172 | for _, collection := range backup.Spec.Collections { 173 | _, err = reconcileSolrCollectionBackup(backup, solrCloud, collection) 174 | } 175 | 176 | // First check if the collection backups have been completed 177 | collectionBackupsFinished = util.CheckStatusOfCollectionBackups(backup) 178 | 179 | return solrCloud, collectionBackupsFinished, actionTaken, err 180 | } 181 | 182 | func reconcileSolrCollectionBackup(backup *solrv1beta1.SolrBackup, solrCloud *solrv1beta1.SolrCloud, collection string) (finished bool, err error) { 183 | now := metav1.Now() 184 | collectionBackupStatus := solrv1beta1.CollectionBackupStatus{} 185 | collectionBackupStatus.Collection = collection 186 | backupIndex := -1 187 | // Get the backup status for this collection, if one exists 188 | for i, status := range backup.Status.CollectionBackupStatuses { 189 | if status.Collection == collection { 190 | collectionBackupStatus = status 191 | backupIndex = i 192 | } 193 | } 194 | 195 | // If the collection backup hasn't started, start it 196 | if !collectionBackupStatus.InProgress && !collectionBackupStatus.Finished { 197 | 198 | // Start the backup by calling solr 199 | started, err := util.StartBackupForCollection(solrCloud.Name, collection, backup.Name, backup.Namespace) 200 | if err != nil { 201 | return true, err 202 | } 203 | collectionBackupStatus.InProgress = started 204 | if started && collectionBackupStatus.StartTime == nil { 205 | collectionBackupStatus.StartTime = &now 206 | } 207 | } else if collectionBackupStatus.InProgress { 208 | // Check the state of the backup, when it is in progress, and update the state accordingly 209 | finished, successful, asyncStatus, error := util.CheckBackupForCollection(solrCloud.Name, collection, backup.Name, backup.Namespace) 210 | if error != nil { 211 | return false, error 212 | } 213 | collectionBackupStatus.Finished = finished 214 | if finished { 215 | collectionBackupStatus.InProgress = false 216 | if collectionBackupStatus.Successful == nil { 217 | collectionBackupStatus.Successful = &successful 218 | } 219 | collectionBackupStatus.AsyncBackupStatus = "" 220 | if collectionBackupStatus.FinishTime == nil { 221 | collectionBackupStatus.FinishTime = &now 222 | } 223 | 224 | err = util.DeleteAsyncInfoForBackup(solrCloud.Name, collection, backup.Name, backup.Namespace) 225 | } else { 226 | collectionBackupStatus.AsyncBackupStatus = asyncStatus 227 | } 228 | } 229 | 230 | if backupIndex < 0 { 231 | backup.Status.CollectionBackupStatuses = append(backup.Status.CollectionBackupStatuses, collectionBackupStatus) 232 | } else { 233 | backup.Status.CollectionBackupStatuses[backupIndex] = collectionBackupStatus 234 | } 235 | 236 | return collectionBackupStatus.Finished, err 237 | } 238 | 239 | func persistSolrCloudBackups(r *SolrBackupReconciler, backup *solrv1beta1.SolrBackup, solrCloud *solrv1beta1.SolrCloud) (err error) { 240 | if backup.Status.PersistenceStatus.Finished { 241 | return nil 242 | } 243 | now := metav1.Now() 244 | 245 | persistenceJob := util.GenerateBackupPersistenceJobForCloud(backup, solrCloud) 246 | if err := controllerutil.SetControllerReference(backup, persistenceJob, r.scheme); err != nil { 247 | return err 248 | } 249 | 250 | foundPersistenceJob := &batchv1.Job{} 251 | err = r.Get(context.TODO(), types.NamespacedName{Name: persistenceJob.Name, Namespace: persistenceJob.Namespace}, foundPersistenceJob) 252 | if err == nil && !backup.Status.PersistenceStatus.InProgress { 253 | 254 | } 255 | if err != nil && errors.IsNotFound(err) { 256 | r.Log.Info("Creating Persistence Job", "namespace", persistenceJob.Namespace, "name", persistenceJob.Name) 257 | err = r.Create(context.TODO(), persistenceJob) 258 | backup.Status.PersistenceStatus.InProgress = true 259 | if backup.Status.PersistenceStatus.StartTime == nil { 260 | backup.Status.PersistenceStatus.StartTime = &now 261 | } 262 | } else if err != nil { 263 | return err 264 | } else { 265 | backup.Status.PersistenceStatus.FinishTime = foundPersistenceJob.Status.CompletionTime 266 | tru := true 267 | fals := false 268 | numFailLimit := int32(0) 269 | if foundPersistenceJob.Spec.BackoffLimit != nil { 270 | numFailLimit = *foundPersistenceJob.Spec.BackoffLimit 271 | } 272 | if foundPersistenceJob.Status.Succeeded > 0 { 273 | backup.Status.PersistenceStatus.Successful = &tru 274 | } else if foundPersistenceJob.Status.Failed > numFailLimit { 275 | backup.Status.PersistenceStatus.Successful = &fals 276 | } 277 | 278 | if backup.Status.PersistenceStatus.Successful != nil { 279 | backup.Status.PersistenceStatus.InProgress = false 280 | backup.Status.PersistenceStatus.Finished = true 281 | backup.Status.PersistenceStatus.FinishTime = &now 282 | backup.Status.Finished = true 283 | backup.Status.Successful = backup.Status.PersistenceStatus.Successful 284 | } 285 | } 286 | return err 287 | } 288 | 289 | func (r *SolrBackupReconciler) SetupWithManager(mgr ctrl.Manager) error { 290 | r.scheme = mgr.GetScheme() 291 | return ctrl.NewControllerManagedBy(mgr). 292 | For(&solrv1beta1.SolrBackup{}). 293 | Owns(&batchv1.Job{}). 294 | Complete(r) 295 | } 296 | -------------------------------------------------------------------------------- /controllers/util/zk_util.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Bloomberg Finance LP. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package util 18 | 19 | import ( 20 | "reflect" 21 | 22 | solr "github.com/bloomberg/solr-operator/api/v1beta1" 23 | etcd "github.com/coreos/etcd-operator/pkg/apis/etcd/v1beta2" 24 | zk "github.com/pravega/zookeeper-operator/pkg/apis/zookeeper/v1beta1" 25 | appsv1 "k8s.io/api/apps/v1" 26 | corev1 "k8s.io/api/core/v1" 27 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 28 | "k8s.io/apimachinery/pkg/util/intstr" 29 | logf "sigs.k8s.io/controller-runtime/pkg/runtime/log" 30 | ) 31 | 32 | var log = logf.Log.WithName("controller") 33 | 34 | // GenerateZookeeperCluster returns a new ZookeeperCluster pointer generated for the SolrCloud instance 35 | // object: SolrCloud instance 36 | // zkSpec: the spec of the ZookeeperCluster to generate 37 | func GenerateZookeeperCluster(solrCloud *solr.SolrCloud, zkSpec solr.ZookeeperSpec) *zk.ZookeeperCluster { 38 | // TODO: Default and Validate these with Webhooks 39 | labels := solrCloud.SharedLabelsWith(solrCloud.GetLabels()) 40 | labels["technology"] = solr.ZookeeperTechnologyLabel 41 | 42 | zkCluster := &zk.ZookeeperCluster{ 43 | ObjectMeta: metav1.ObjectMeta{ 44 | Name: solrCloud.ProvidedZookeeperName(), 45 | Namespace: solrCloud.GetNamespace(), 46 | Labels: labels, 47 | }, 48 | Spec: zk.ZookeeperClusterSpec{ 49 | Image: zk.ContainerImage{ 50 | Repository: zkSpec.Image.Repository, 51 | Tag: zkSpec.Image.Tag, 52 | PullPolicy: zkSpec.Image.PullPolicy, 53 | }, 54 | Labels: labels, 55 | Replicas: *zkSpec.Replicas, 56 | PersistentVolumeClaimSpec: *zkSpec.PersistentVolumeClaimSpec, 57 | }, 58 | } 59 | 60 | // Append Pod Policies if provided by user 61 | if zkSpec.ZookeeperPod.Affinity != nil { 62 | zkCluster.Spec.Pod.Affinity = zkSpec.ZookeeperPod.Affinity 63 | } 64 | 65 | if zkSpec.ZookeeperPod.Resources.Limits != nil || zkSpec.ZookeeperPod.Resources.Requests != nil { 66 | zkCluster.Spec.Pod.Resources = zkSpec.ZookeeperPod.Resources 67 | } 68 | 69 | return zkCluster 70 | } 71 | 72 | // CopyZookeeperClusterFields copies the owned fields from one ZookeeperCluster to another 73 | // Returns true if the fields copied from don't match to. 74 | func CopyZookeeperClusterFields(from, to *zk.ZookeeperCluster) bool { 75 | requireUpdate := false 76 | for k, v := range to.Labels { 77 | if from.Labels[k] != v { 78 | log.Info("Updating Zookeeper label ", k, v) 79 | requireUpdate = true 80 | } 81 | } 82 | to.Labels = from.Labels 83 | 84 | for k, v := range to.Annotations { 85 | if from.Annotations[k] != v { 86 | log.Info("Updating Zk annotation", k, v) 87 | requireUpdate = true 88 | } 89 | } 90 | to.Annotations = from.Annotations 91 | 92 | if !reflect.DeepEqual(to.Spec.Replicas, from.Spec.Replicas) { 93 | log.Info("Updating Zk replicas") 94 | requireUpdate = true 95 | } 96 | to.Spec.Replicas = from.Spec.Replicas 97 | 98 | if !reflect.DeepEqual(to.Spec.Image.Repository, from.Spec.Image.Repository) { 99 | log.Info("Updating Zk image repository") 100 | requireUpdate = true 101 | } 102 | to.Spec.Image.Repository = from.Spec.Image.Repository 103 | 104 | if !reflect.DeepEqual(to.Spec.Image.Tag, from.Spec.Image.Tag) { 105 | log.Info("Updating Zk image tag") 106 | requireUpdate = true 107 | } 108 | to.Spec.Image.Tag = from.Spec.Image.Tag 109 | 110 | if !reflect.DeepEqual(to.Spec.PersistentVolumeClaimSpec.Resources.Requests, from.Spec.PersistentVolumeClaimSpec.Resources.Requests) { 111 | requireUpdate = true 112 | } 113 | to.Spec.PersistentVolumeClaimSpec.Resources.Requests = from.Spec.PersistentVolumeClaimSpec.Resources.Requests 114 | 115 | if !reflect.DeepEqual(to.Spec.Pod.Resources, from.Spec.Pod.Resources) { 116 | log.Info("Updating Zk pod resources") 117 | requireUpdate = true 118 | } 119 | to.Spec.Pod.Resources = from.Spec.Pod.Resources 120 | 121 | if from.Spec.Pod.Affinity != nil { 122 | if !reflect.DeepEqual(to.Spec.Pod.Affinity.NodeAffinity, from.Spec.Pod.Affinity.NodeAffinity) { 123 | log.Info("Updating Zk pod node affinity") 124 | log.Info("Update required because:", "Spec.Pod.Affinity.NodeAffinity changed from", from.Spec.Pod.Affinity.NodeAffinity, "To:", to.Spec.Pod.Affinity.NodeAffinity) 125 | requireUpdate = true 126 | } 127 | 128 | if !reflect.DeepEqual(to.Spec.Pod.Affinity.PodAffinity, from.Spec.Pod.Affinity.PodAffinity) { 129 | log.Info("Updating Zk pod node affinity") 130 | log.Info("Update required because:", "Spec.Pod.Affinity.PodAffinity changed from", from.Spec.Pod.Affinity.PodAffinity, "To:", to.Spec.Pod.Affinity.PodAffinity) 131 | requireUpdate = true 132 | } 133 | to.Spec.Pod.Affinity = from.Spec.Pod.Affinity 134 | } 135 | 136 | return requireUpdate 137 | } 138 | 139 | // GenerateEtcdCluster returns a new EtcdCluster pointer generated for the SolrCloud instance 140 | // object: SolrCloud instance 141 | // etcdSpec: the spec of the EtcdCluster to generate 142 | // busyBoxImage: the image of busyBox to use 143 | func GenerateEtcdCluster(solrCloud *solr.SolrCloud, etcdSpec solr.EtcdSpec, busyBoxImage solr.ContainerImage) *etcd.EtcdCluster { 144 | // TODO: Default and Validate these with Webhooks 145 | labels := solrCloud.SharedLabelsWith(solrCloud.GetLabels()) 146 | labels["technology"] = solr.ZookeeperTechnologyLabel 147 | 148 | etcdCluster := &etcd.EtcdCluster{ 149 | ObjectMeta: metav1.ObjectMeta{ 150 | Name: solrCloud.ProvidedZetcdName(), 151 | Namespace: solrCloud.GetNamespace(), 152 | Labels: labels, 153 | }, 154 | Spec: etcd.ClusterSpec{ 155 | Version: etcdSpec.Image.Tag, 156 | Repository: etcdSpec.Image.Repository, 157 | Size: *etcdSpec.Replicas, 158 | Pod: &etcd.PodPolicy{ 159 | Labels: labels, 160 | BusyboxImage: busyBoxImage.ToImageName(), 161 | }, 162 | }, 163 | } 164 | 165 | // Append Pod Policies if provided by user 166 | if etcdSpec.EtcdPod.Affinity != nil { 167 | etcdCluster.Spec.Pod.Affinity = etcdSpec.EtcdPod.Affinity 168 | } 169 | 170 | if etcdSpec.EtcdPod.Resources.Limits != nil || etcdSpec.EtcdPod.Resources.Requests != nil { 171 | etcdCluster.Spec.Pod.Resources = etcdSpec.EtcdPod.Resources 172 | } 173 | 174 | return etcdCluster 175 | } 176 | 177 | // CopyEtcdClusterFields copies the owned fields from one EtcdCluster to another 178 | // Returns true if the fields copied from don't match to. 179 | func CopyEtcdClusterFields(from, to *etcd.EtcdCluster) bool { 180 | requireUpdate := false 181 | for k, v := range to.Labels { 182 | if from.Labels[k] != v { 183 | requireUpdate = true 184 | } 185 | } 186 | to.Labels = from.Labels 187 | 188 | for k, v := range to.Annotations { 189 | if from.Annotations[k] != v { 190 | requireUpdate = true 191 | } 192 | } 193 | to.Annotations = from.Annotations 194 | 195 | if !reflect.DeepEqual(to.Spec, from.Spec) { 196 | requireUpdate = true 197 | } 198 | to.Spec = from.Spec 199 | 200 | return requireUpdate 201 | } 202 | 203 | // GenerateZetcdDeployment returns a new appsv1.Deployment for Zetcd 204 | // solrCloud: SolrCloud instance 205 | // spec: ZetcdSpec 206 | func GenerateZetcdDeployment(solrCloud *solr.SolrCloud, spec solr.ZetcdSpec) *appsv1.Deployment { 207 | // TODO: Default and Validate these with Webhooks 208 | labels := solrCloud.SharedLabelsWith(solrCloud.GetLabels()) 209 | selectorLabels := solrCloud.SharedLabels() 210 | 211 | labels["technology"] = solr.ZookeeperTechnologyLabel 212 | selectorLabels["technology"] = solr.ZookeeperTechnologyLabel 213 | 214 | labels["app"] = "zetcd" 215 | selectorLabels["app"] = "zetcd" 216 | 217 | deployment := &appsv1.Deployment{ 218 | ObjectMeta: metav1.ObjectMeta{ 219 | Name: solrCloud.ProvidedZetcdName(), 220 | Namespace: solrCloud.GetNamespace(), 221 | Labels: labels, 222 | }, 223 | Spec: appsv1.DeploymentSpec{ 224 | Replicas: spec.Replicas, 225 | Selector: &metav1.LabelSelector{ 226 | MatchLabels: selectorLabels, 227 | }, 228 | Template: corev1.PodTemplateSpec{ 229 | ObjectMeta: metav1.ObjectMeta{Labels: labels}, 230 | Spec: corev1.PodSpec{ 231 | Containers: []corev1.Container{ 232 | { 233 | Name: "zetcd", 234 | Image: spec.Image.ToImageName(), 235 | ImagePullPolicy: spec.Image.PullPolicy, 236 | Ports: []corev1.ContainerPort{{ContainerPort: 2181}}, 237 | }, 238 | }, 239 | }, 240 | }, 241 | }, 242 | } 243 | 244 | if spec.ZetcdPod.Resources.Limits != nil || spec.ZetcdPod.Resources.Requests != nil { 245 | deployment.Spec.Template.Spec.Containers[0].Resources = spec.ZetcdPod.Resources 246 | } 247 | 248 | if spec.ZetcdPod.Affinity != nil { 249 | deployment.Spec.Template.Spec.Affinity = spec.ZetcdPod.Affinity 250 | } 251 | 252 | return deployment 253 | } 254 | 255 | // CopyDeploymentFields copies the owned fields from one Deployment to another 256 | // Returns true if the fields copied from don't match to. 257 | func CopyDeploymentFields(from, to *appsv1.Deployment) bool { 258 | requireUpdate := false 259 | for k, v := range from.Labels { 260 | if to.Labels[k] != v { 261 | requireUpdate = true 262 | } 263 | to.Labels[k] = v 264 | } 265 | 266 | for k, v := range from.Annotations { 267 | if to.Annotations[k] != v { 268 | requireUpdate = true 269 | } 270 | to.Annotations[k] = v 271 | } 272 | 273 | if !reflect.DeepEqual(to.Spec.Replicas, from.Spec.Replicas) { 274 | requireUpdate = true 275 | to.Spec.Replicas = from.Spec.Replicas 276 | } 277 | 278 | if !reflect.DeepEqual(to.Spec.Selector, from.Spec.Selector) { 279 | requireUpdate = true 280 | to.Spec.Selector = from.Spec.Selector 281 | } 282 | 283 | if !reflect.DeepEqual(to.Spec.Template.Labels, from.Spec.Template.Labels) { 284 | requireUpdate = true 285 | to.Spec.Template.Labels = from.Spec.Template.Labels 286 | } 287 | 288 | if !reflect.DeepEqual(to.Spec.Template.Spec.Volumes, from.Spec.Template.Spec.Volumes) { 289 | requireUpdate = true 290 | to.Spec.Template.Spec.Volumes = from.Spec.Template.Spec.Volumes 291 | } 292 | 293 | if !reflect.DeepEqual(to.Spec.Template.Spec.Affinity, from.Spec.Template.Spec.Affinity) { 294 | requireUpdate = true 295 | to.Spec.Template.Spec.Affinity = from.Spec.Template.Spec.Affinity 296 | } 297 | 298 | if len(to.Spec.Template.Spec.Containers) != len(from.Spec.Template.Spec.Containers) { 299 | requireUpdate = true 300 | to.Spec.Template.Spec.Containers = from.Spec.Template.Spec.Containers 301 | } else if !reflect.DeepEqual(to.Spec.Template.Spec.Containers, from.Spec.Template.Spec.Containers) { 302 | for i := 0; i < len(to.Spec.Template.Spec.Containers); i++ { 303 | if !reflect.DeepEqual(to.Spec.Template.Spec.Containers[i].Name, from.Spec.Template.Spec.Containers[i].Name) { 304 | requireUpdate = true 305 | to.Spec.Template.Spec.Containers[i].Name = from.Spec.Template.Spec.Containers[i].Name 306 | } 307 | 308 | if !reflect.DeepEqual(to.Spec.Template.Spec.Containers[i].Image, from.Spec.Template.Spec.Containers[i].Image) { 309 | requireUpdate = true 310 | to.Spec.Template.Spec.Containers[i].Image = from.Spec.Template.Spec.Containers[i].Image 311 | } 312 | 313 | if !reflect.DeepEqual(to.Spec.Template.Spec.Containers[i].ImagePullPolicy, from.Spec.Template.Spec.Containers[i].ImagePullPolicy) { 314 | requireUpdate = true 315 | to.Spec.Template.Spec.Containers[i].ImagePullPolicy = from.Spec.Template.Spec.Containers[i].ImagePullPolicy 316 | } 317 | 318 | if !reflect.DeepEqual(to.Spec.Template.Spec.Containers[i].Command, from.Spec.Template.Spec.Containers[i].Command) { 319 | requireUpdate = true 320 | to.Spec.Template.Spec.Containers[i].Command = from.Spec.Template.Spec.Containers[i].Command 321 | } 322 | 323 | if !reflect.DeepEqual(to.Spec.Template.Spec.Containers[i].Args, from.Spec.Template.Spec.Containers[i].Args) { 324 | requireUpdate = true 325 | to.Spec.Template.Spec.Containers[i].Args = from.Spec.Template.Spec.Containers[i].Args 326 | } 327 | 328 | if !reflect.DeepEqual(to.Spec.Template.Spec.Containers[i].Env, from.Spec.Template.Spec.Containers[i].Env) { 329 | requireUpdate = true 330 | to.Spec.Template.Spec.Containers[i].Env = from.Spec.Template.Spec.Containers[i].Env 331 | } 332 | 333 | if !reflect.DeepEqual(to.Spec.Template.Spec.Containers[i].Resources, from.Spec.Template.Spec.Containers[i].Resources) { 334 | requireUpdate = true 335 | to.Spec.Template.Spec.Containers[i].Resources = from.Spec.Template.Spec.Containers[i].Resources 336 | } 337 | } 338 | } 339 | 340 | return requireUpdate 341 | } 342 | 343 | // GenerateZetcdService returns a new corev1.Service pointer generated for the Zetcd deployment 344 | // solrCloud: SolrCloud instance 345 | // spec: ZetcdSpec 346 | func GenerateZetcdService(solrCloud *solr.SolrCloud, spec solr.ZetcdSpec) *corev1.Service { 347 | // TODO: Default and Validate these with Webhooks 348 | labels := solrCloud.SharedLabelsWith(solrCloud.GetLabels()) 349 | selectorLabels := solrCloud.SharedLabels() 350 | 351 | labels["technology"] = solr.ZookeeperTechnologyLabel 352 | selectorLabels["technology"] = solr.ZookeeperTechnologyLabel 353 | 354 | labels["app"] = "zetcd" 355 | selectorLabels["app"] = "zetcd" 356 | 357 | service := &corev1.Service{ 358 | ObjectMeta: metav1.ObjectMeta{ 359 | Name: solrCloud.ProvidedZetcdName(), 360 | Namespace: solrCloud.GetNamespace(), 361 | Labels: labels, 362 | }, 363 | Spec: corev1.ServiceSpec{ 364 | Ports: []corev1.ServicePort{ 365 | {Port: 2181, TargetPort: intstr.IntOrString{IntVal: 2181, Type: intstr.Int}}, 366 | }, 367 | Selector: selectorLabels, 368 | }, 369 | } 370 | return service 371 | } 372 | -------------------------------------------------------------------------------- /config/crds/solr_v1beta1_solrcloud.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apiextensions.k8s.io/v1beta1 2 | kind: CustomResourceDefinition 3 | metadata: 4 | creationTimestamp: null 5 | labels: 6 | controller-tools.k8s.io: "1.0" 7 | name: solrclouds.solr.bloomberg.com 8 | spec: 9 | additionalPrinterColumns: 10 | - JSONPath: .status.version 11 | description: Solr Version of the cloud 12 | name: Version 13 | type: string 14 | - JSONPath: .status.targetVersion 15 | description: Target Solr Version of the cloud 16 | name: TargetVersion 17 | type: string 18 | - JSONPath: .spec.replicas 19 | description: Number of solr nodes configured to run in the cloud 20 | name: DesiredNodes 21 | type: integer 22 | - JSONPath: .status.replicas 23 | description: Number of solr nodes running 24 | name: Nodes 25 | type: integer 26 | - JSONPath: .status.readyReplicas 27 | description: Number of solr nodes connected to the cloud 28 | name: ReadyNodes 29 | type: integer 30 | - JSONPath: .metadata.creationTimestamp 31 | name: Age 32 | type: date 33 | group: solr.bloomberg.com 34 | names: 35 | categories: 36 | - all 37 | kind: SolrCloud 38 | plural: solrclouds 39 | shortNames: 40 | - solr 41 | scope: Namespaced 42 | subresources: 43 | scale: 44 | specReplicasPath: .spec.replicas 45 | statusReplicasPath: .status.readyReplicas 46 | status: {} 47 | validation: 48 | openAPIV3Schema: 49 | properties: 50 | apiVersion: 51 | description: 'APIVersion defines the versioned schema of this representation 52 | of an object. Servers should convert recognized schemas to the latest 53 | internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#resources' 54 | type: string 55 | kind: 56 | description: 'Kind is a string value representing the REST resource this 57 | object represents. Servers may infer this from the endpoint the client 58 | submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#types-kinds' 59 | type: string 60 | metadata: 61 | type: object 62 | spec: 63 | properties: 64 | backupRestoreVolume: 65 | description: 'Required for backups & restores to be enabled. This is 66 | a volumeSource for a volume that will be mounted to all solrNodes 67 | to store backups and load restores. The data within the volume will 68 | be namespaces for this instance, so feel free to use the same volume 69 | for multiple clouds. Since the volume will be mounted to all solrNodes, 70 | it must be able to be written from multiple pods. If a PVC reference 71 | is given, the PVC must have `accessModes: - ReadWriteMany`. Other 72 | options are to use a NFS volume.' 73 | type: object 74 | busyBoxImage: 75 | properties: 76 | imagePullSecret: 77 | type: string 78 | pullPolicy: 79 | type: string 80 | repository: 81 | type: string 82 | tag: 83 | type: string 84 | type: object 85 | dataPvcSpec: 86 | description: DataPvcSpec is the spec to describe PVC for the solr node 87 | to store its data. This field is optional. If no PVC spec is provided, 88 | each solr node will use emptyDir as the data volume 89 | type: object 90 | pod: 91 | description: Pod defines the policy to create pod for the SolrCloud. 92 | Updating the Pod does not take effect on any existing pods. 93 | properties: 94 | affinity: 95 | description: The scheduling constraints on pods. 96 | type: object 97 | resources: 98 | description: Resources is the resource requirements for the container. 99 | This field cannot be updated once the cluster is created. 100 | type: object 101 | type: object 102 | replicas: 103 | description: The number of solr nodes to run 104 | format: int32 105 | type: integer 106 | solrGCTune: 107 | description: Set GC Tuning configuration through GC_TUNE environment 108 | variable 109 | type: string 110 | solrImage: 111 | properties: 112 | imagePullSecret: 113 | type: string 114 | pullPolicy: 115 | type: string 116 | repository: 117 | type: string 118 | tag: 119 | type: string 120 | type: object 121 | solrJavaMem: 122 | type: string 123 | solrLogLevel: 124 | description: Set the Solr Log level, defaults to INFO 125 | type: string 126 | solrOpts: 127 | description: You can add common system properties to the SOLR_OPTS environment 128 | variable SolrOpts is the string interface for these optional settings 129 | type: string 130 | solrPodPolicy: 131 | description: Pod defines the policy to create pod for the SolrCloud. 132 | Updating the Pod does not take effect on any existing pods. 133 | properties: 134 | affinity: 135 | description: The scheduling constraints on pods. 136 | type: object 137 | resources: 138 | description: Resources is the resource requirements for the container. 139 | This field cannot be updated once the cluster is created. 140 | type: object 141 | type: object 142 | zookeeperRef: 143 | description: The information for the Zookeeper this SolrCloud should 144 | connect to Can be a zookeeper that is running, or one that is created 145 | by the solr operator 146 | properties: 147 | connectionInfo: 148 | description: A zookeeper ensemble that is run independently of the 149 | solr operator If an externalConnectionString is provided, but 150 | no internalConnectionString is, the external will be used as the 151 | internal 152 | properties: 153 | chroot: 154 | description: The ChRoot to connect solr at 155 | type: string 156 | externalConnectionString: 157 | description: The connection string to connect to the ensemble 158 | from outside of the Kubernetes cluster If external and no 159 | internal connection string is provided, the external cnx string 160 | will be used as the internal cnx string 161 | type: string 162 | internalConnectionString: 163 | description: The connection string to connect to the ensemble 164 | from within the Kubernetes cluster 165 | type: string 166 | type: object 167 | provided: 168 | description: 'A zookeeper that is created by the solr operator Note: 169 | This option will not allow the SolrCloud to run across kube-clusters.' 170 | properties: 171 | zookeeper: 172 | description: 'Create a new Zookeeper Ensemble with the following 173 | spec Note: Requires - The zookeeperOperator flag to be provided 174 | to the Solr Operator - A zookeeper operator to be running' 175 | properties: 176 | image: 177 | description: Image of Zookeeper to run 178 | properties: 179 | imagePullSecret: 180 | type: string 181 | pullPolicy: 182 | type: string 183 | repository: 184 | type: string 185 | tag: 186 | type: string 187 | type: object 188 | persistentVolumeClaimSpec: 189 | description: PersistentVolumeClaimSpec is the spec to describe 190 | PVC for the zk container This field is optional. If no 191 | PVC spec, etcd container will use emptyDir as volume 192 | type: object 193 | replicas: 194 | description: Number of members to create up for the ZK ensemble 195 | Defaults to 3 196 | format: int32 197 | type: integer 198 | zookeeperPodPolicy: 199 | description: Pod resources for zookeeper pod 200 | properties: 201 | affinity: 202 | description: The scheduling constraints on pods. 203 | type: object 204 | resources: 205 | description: Resources is the resource requirements 206 | for the container. This field cannot be updated once 207 | the cluster is created. 208 | type: object 209 | type: object 210 | type: object 211 | type: object 212 | type: object 213 | type: object 214 | status: 215 | properties: 216 | backupRestoreReady: 217 | description: BackupRestoreReady announces whether the solrCloud has 218 | the backupRestorePVC mounted to all pods and therefore is ready for 219 | backups and restores. 220 | type: boolean 221 | externalCommonAddress: 222 | description: ExternalCommonAddress is the external common http address 223 | for all solr nodes. Will only be provided when an ingressUrl is provided 224 | for the cloud 225 | type: string 226 | internalCommonAddress: 227 | description: InternalCommonAddress is the internal common http address 228 | for all solr nodes 229 | type: string 230 | readyReplicas: 231 | description: ReadyReplicas is the number of number of ready replicas 232 | in the cluster 233 | format: int32 234 | type: integer 235 | replicas: 236 | description: Replicas is the number of number of desired replicas in 237 | the cluster 238 | format: int32 239 | type: integer 240 | solrNodes: 241 | description: SolrNodes contain the statuses of each solr node running 242 | in this solr cloud. 243 | items: 244 | properties: 245 | externalAddress: 246 | description: An address the node can be connected to from outside 247 | of the Kube cluster Will only be provided when an ingressUrl 248 | is provided for the cloud 249 | type: string 250 | internalAddress: 251 | description: An address the node can be connected to from within 252 | the Kube cluster 253 | type: string 254 | name: 255 | description: The name of the pod running the node 256 | type: string 257 | ready: 258 | description: Is the node up and running 259 | type: boolean 260 | version: 261 | description: The version of solr that the node is running 262 | type: string 263 | required: 264 | - name 265 | - internalAddress 266 | - ready 267 | - version 268 | type: object 269 | type: array 270 | targetVersion: 271 | description: The version of solr that the cloud is meant to be running. 272 | Will only be provided when the cloud is migrating between versions 273 | type: string 274 | version: 275 | description: The version of solr that the cloud is running 276 | type: string 277 | zookeeperConnectionInfo: 278 | description: ZookeeperConnectionInfo is the information on how to connect 279 | to the used Zookeeper 280 | properties: 281 | chroot: 282 | description: The ChRoot to connect solr at 283 | type: string 284 | externalConnectionString: 285 | description: The connection string to connect to the ensemble from 286 | outside of the Kubernetes cluster If external and no internal 287 | connection string is provided, the external cnx string will be 288 | used as the internal cnx string 289 | type: string 290 | internalConnectionString: 291 | description: The connection string to connect to the ensemble from 292 | within the Kubernetes cluster 293 | type: string 294 | type: object 295 | required: 296 | - solrNodes 297 | - replicas 298 | - readyReplicas 299 | - version 300 | - internalCommonAddress 301 | - zookeeperConnectionInfo 302 | - backupRestoreReady 303 | type: object 304 | version: v1beta1 305 | status: 306 | acceptedNames: 307 | kind: "" 308 | plural: "" 309 | conditions: [] 310 | storedVersions: [] 311 | -------------------------------------------------------------------------------- /controllers/util/backup_util.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Bloomberg Finance LP. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package util 18 | 19 | import ( 20 | "bytes" 21 | "fmt" 22 | solr "github.com/bloomberg/solr-operator/api/v1beta1" 23 | batchv1 "k8s.io/api/batch/v1" 24 | corev1 "k8s.io/api/core/v1" 25 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 26 | "k8s.io/apimachinery/pkg/runtime" 27 | "k8s.io/client-go/kubernetes" 28 | "k8s.io/client-go/rest" 29 | "k8s.io/client-go/tools/remotecommand" 30 | "net/url" 31 | ) 32 | 33 | const ( 34 | BaseBackupRestorePath = "/var/solr-backup-restore" 35 | TarredFile = "/var/solr-backup-restore/backup.tgz" 36 | BackupTarCommand = "cd " + BaseBackupRestorePath + " && tar -czf /tmp/backup.tgz * && mv /tmp/backup.tgz " + TarredFile + " && chmod -R a+rwx " + TarredFile + " && cd - && " 37 | CleanupCommand = " && rm -rf " + BaseBackupRestorePath + "/{*,.*}" 38 | 39 | AWSSecretDir = "/var/aws" 40 | 41 | JobTTLSeconds = int32(60) 42 | ) 43 | 44 | func BackupRestoreSubPathForCloud(cloud string) string { 45 | return "cloud/" + cloud 46 | } 47 | 48 | func BackupSubPathForCloud(cloud string, backupName string) string { 49 | return BackupRestoreSubPathForCloud(cloud) + "/backups/" + backupName 50 | } 51 | 52 | func RestoreSubPathForCloud(cloud string, restoreName string) string { 53 | return BackupRestoreSubPathForCloud(cloud) + "/restores/" + restoreName 54 | } 55 | 56 | func BackupPath(backupName string) string { 57 | return BaseBackupRestorePath + "/backups/" + backupName 58 | } 59 | 60 | func RestorePath(backupName string) string { 61 | return BaseBackupRestorePath + "/restores/" + backupName 62 | } 63 | 64 | func AsyncIdForCollectionBackup(collection string, backupName string) string { 65 | return backupName + "-" + collection 66 | } 67 | 68 | func CheckStatusOfCollectionBackups(backup *solr.SolrBackup) (allFinished bool) { 69 | fals := false 70 | 71 | // Check if all collection backups have been completed, this is updated in the loop 72 | allFinished = len(backup.Status.CollectionBackupStatuses) > 0 73 | 74 | // Check if persistence should be skipped if no backup completed successfully 75 | anySuccessful := false 76 | 77 | for _, collectionStatus := range backup.Status.CollectionBackupStatuses { 78 | allFinished = allFinished && collectionStatus.Finished 79 | anySuccessful = anySuccessful || (collectionStatus.Successful != nil && *collectionStatus.Successful) 80 | } 81 | if allFinished && !anySuccessful { 82 | backup.Status.Finished = true 83 | if backup.Status.Successful == nil { 84 | backup.Status.Successful = &fals 85 | } 86 | } 87 | return 88 | } 89 | 90 | func GenerateBackupPersistenceJobForCloud(backup *solr.SolrBackup, solrCloud *solr.SolrCloud) *batchv1.Job { 91 | return GenerateBackupPersistenceJob(backup, *solrCloud.Spec.BackupRestoreVolume, BackupSubPathForCloud(solrCloud.Name, backup.Name)) 92 | } 93 | 94 | // GenerateBackupPersistenceJob creates a Job that will persist backup data and purge the backup from the solrBackupVolume 95 | func GenerateBackupPersistenceJob(solrBackup *solr.SolrBackup, solrBackupVolume corev1.VolumeSource, backupSubPath string) *batchv1.Job { 96 | copyLabels := solrBackup.GetLabels() 97 | if copyLabels == nil { 98 | copyLabels = map[string]string{} 99 | } 100 | labels := solrBackup.SharedLabelsWith(solrBackup.GetLabels()) 101 | 102 | // ttlSeconds := JobTTLSeconds 103 | 104 | image, env, command, volume, volumeMount, numRetries := GeneratePersistenceOptions(solrBackup) 105 | 106 | volumes := []corev1.Volume{ 107 | { 108 | Name: "backup-data", 109 | VolumeSource: solrBackupVolume, 110 | }, 111 | } 112 | volumeMounts := []corev1.VolumeMount{ 113 | { 114 | MountPath: BaseBackupRestorePath, 115 | Name: "backup-data", 116 | SubPath: backupSubPath, 117 | ReadOnly: false, 118 | }, 119 | } 120 | if volume != nil && volumeMount != nil { 121 | volumes = append(volumes, *volume) 122 | volumeMounts = append(volumeMounts, *volumeMount) 123 | } 124 | 125 | parallelismAndCompletions := int32(1) 126 | 127 | job := &batchv1.Job{ 128 | ObjectMeta: metav1.ObjectMeta{ 129 | Name: solrBackup.PersistenceJobName(), 130 | Namespace: solrBackup.GetNamespace(), 131 | Labels: labels, 132 | }, 133 | Spec: batchv1.JobSpec{ 134 | // TTLSecondsAfterFinished: &ttlSeconds, 135 | BackoffLimit: numRetries, 136 | Parallelism: ¶llelismAndCompletions, 137 | Completions: ¶llelismAndCompletions, 138 | Template: corev1.PodTemplateSpec{ 139 | ObjectMeta: metav1.ObjectMeta{ 140 | Labels: labels, 141 | }, 142 | Spec: corev1.PodSpec{ 143 | Volumes: volumes, 144 | Containers: []corev1.Container{ 145 | { 146 | Name: "backup-persistence", 147 | Image: image.ToImageName(), 148 | ImagePullPolicy: image.PullPolicy, 149 | VolumeMounts: volumeMounts, 150 | Env: env, 151 | Command: command, 152 | }, 153 | }, 154 | RestartPolicy: corev1.RestartPolicyNever, 155 | }, 156 | }, 157 | }, 158 | } 159 | return job 160 | } 161 | 162 | // GeneratePersistenceOptions creates options for a Job that will persist backup data 163 | func GeneratePersistenceOptions(solrBackup *solr.SolrBackup) (image solr.ContainerImage, envVars []corev1.EnvVar, command []string, volume *corev1.Volume, volumeMount *corev1.VolumeMount, numRetries *int32) { 164 | persistenceSource := solrBackup.Spec.Persistence 165 | if persistenceSource.Volume != nil { 166 | // Options for persisting to a volume 167 | image = persistenceSource.Volume.BusyBoxImage 168 | envVars = []corev1.EnvVar{ 169 | { 170 | Name: "FILE_NAME", 171 | Value: persistenceSource.Volume.Filename, 172 | }, 173 | } 174 | // Copy the information to the persistent storage, and delete it from the backup-restore volume. 175 | command = []string{"sh", "-c", BackupTarCommand + "cp " + TarredFile + " \"/var/backup-persistence/${FILE_NAME}\"" + CleanupCommand} 176 | 177 | volume = &corev1.Volume{ 178 | Name: "persistence", 179 | VolumeSource: persistenceSource.Volume.VolumeSource, 180 | } 181 | volumeMount = &corev1.VolumeMount{ 182 | Name: "persistence", 183 | SubPath: persistenceSource.Volume.Path, 184 | ReadOnly: false, 185 | MountPath: "/var/backup-persistence", 186 | } 187 | r := int32(1) 188 | numRetries = &r 189 | } else if persistenceSource.S3 != nil { 190 | s3 := persistenceSource.S3 191 | // Options for persisting to S3 192 | image = s3.AWSCliImage 193 | envVars = []corev1.EnvVar{ 194 | { 195 | Name: "BUCKET", 196 | Value: s3.Bucket, 197 | }, 198 | { 199 | Name: "KEY", 200 | Value: s3.Key, 201 | }, 202 | { 203 | Name: "ENDPOINT_URL", 204 | Value: s3.EndpointUrl, 205 | }, 206 | } 207 | // Set up optional Environment variables 208 | if s3.Region != "" { 209 | envVars = append(envVars, corev1.EnvVar{ 210 | Name: "AWS_DEFAULT_REGION", 211 | Value: s3.Region, 212 | }) 213 | } 214 | if s3.Secrets.AccessKeyId != "" { 215 | envVars = append(envVars, corev1.EnvVar{ 216 | Name: "AWS_ACCESS_KEY_ID", 217 | ValueFrom: &corev1.EnvVarSource{ 218 | SecretKeyRef: &corev1.SecretKeySelector{ 219 | LocalObjectReference: corev1.LocalObjectReference{ 220 | Name: s3.Secrets.Name, 221 | }, 222 | Key: s3.Secrets.AccessKeyId, 223 | }, 224 | }, 225 | }) 226 | } 227 | if s3.Secrets.SecretAccessKey != "" { 228 | envVars = append(envVars, corev1.EnvVar{ 229 | Name: "AWS_SECRET_ACCESS_KEY", 230 | ValueFrom: &corev1.EnvVarSource{ 231 | SecretKeyRef: &corev1.SecretKeySelector{ 232 | LocalObjectReference: corev1.LocalObjectReference{ 233 | Name: s3.Secrets.Name, 234 | }, 235 | Key: s3.Secrets.SecretAccessKey, 236 | }, 237 | }, 238 | }) 239 | } 240 | if s3.Secrets.ConfigFile != "" { 241 | envVars = append(envVars, corev1.EnvVar{ 242 | Name: "AWS_CONFIG_FILE", 243 | Value: AWSSecretDir + "/config", 244 | }) 245 | } 246 | if s3.Secrets.CredentialsFile != "" { 247 | envVars = append(envVars, corev1.EnvVar{ 248 | Name: "AWS_SHARED_CREDENTIALS_FILE", 249 | Value: AWSSecretDir + "/credentials", 250 | }) 251 | } 252 | 253 | // If a config or credentials file is provided in the secrets, load them up in a volume 254 | if s3.Secrets.ConfigFile != "" || s3.Secrets.CredentialsFile != "" { 255 | readonly := int32(400) 256 | volume = &corev1.Volume{ 257 | Name: "awsSecrets", 258 | VolumeSource: corev1.VolumeSource{ 259 | Secret: &corev1.SecretVolumeSource{ 260 | SecretName: s3.Secrets.Name, 261 | Items: []corev1.KeyToPath{ 262 | { 263 | Key: s3.Secrets.ConfigFile, 264 | Path: "config", 265 | Mode: &readonly, 266 | }, 267 | { 268 | Key: s3.Secrets.CredentialsFile, 269 | Path: "credentials", 270 | Mode: &readonly, 271 | }, 272 | }, 273 | }, 274 | }, 275 | } 276 | volumeMount = &corev1.VolumeMount{ 277 | Name: "awsSecrets", 278 | ReadOnly: true, 279 | MountPath: AWSSecretDir, 280 | } 281 | } 282 | 283 | // Only include the endpoint URL if it's provided 284 | includeUrl := "" 285 | if s3.EndpointUrl != "" { 286 | includeUrl = "--endpoint-url \"${ENDPOINT_URL}\" " 287 | } 288 | 289 | command = []string{"sh", "-c", BackupTarCommand + "aws s3 cp " + includeUrl + TarredFile + " \"s3://${BUCKET}/${KEY}\"" + CleanupCommand} 290 | numRetries = persistenceSource.S3.Retries 291 | } 292 | 293 | return image, envVars, command, volume, volumeMount, numRetries 294 | } 295 | 296 | func StartBackupForCollection(cloud string, collection string, backupName string, namespace string) (success bool, err error) { 297 | queryParams := url.Values{} 298 | queryParams.Add("action", "BACKUP") 299 | queryParams.Add("collection", collection) 300 | queryParams.Add("name", collection) 301 | queryParams.Add("location", BackupPath(backupName)) 302 | queryParams.Add("async", AsyncIdForCollectionBackup(collection, backupName)) 303 | 304 | resp := &SolrAsyncResponse{} 305 | 306 | log.Info("Calling to start collection backup", "namespace", namespace, "cloud", cloud, "collection", collection, "backup", backupName) 307 | err = CallCollectionsApi(cloud, namespace, queryParams, resp) 308 | 309 | if err == nil { 310 | if resp.ResponseHeader.Status == 0 { 311 | success = true 312 | } 313 | } else { 314 | log.Error(err, "Error starting collection backup", "namespace", namespace, "cloud", cloud, "collection", collection, "backup", backupName) 315 | } 316 | 317 | return success, err 318 | } 319 | 320 | func CheckBackupForCollection(cloud string, collection string, backupName string, namespace string) (finished bool, success bool, asyncStatus string, err error) { 321 | queryParams := url.Values{} 322 | queryParams.Add("action", "REQUESTSTATUS") 323 | queryParams.Add("requestid", AsyncIdForCollectionBackup(collection, backupName)) 324 | 325 | resp := &SolrAsyncResponse{} 326 | 327 | log.Info("Calling to check on collection backup", "namespace", namespace, "cloud", cloud, "collection", collection, "backup", backupName) 328 | err = CallCollectionsApi(cloud, namespace, queryParams, resp) 329 | 330 | if err == nil { 331 | if resp.ResponseHeader.Status == 0 { 332 | asyncStatus = resp.Status.AsyncState 333 | if resp.Status.AsyncState == "completed" { 334 | finished = true 335 | success = true 336 | } 337 | if resp.Status.AsyncState == "failed" { 338 | finished = true 339 | success = false 340 | } 341 | } 342 | } else { 343 | log.Error(err, "Error checking on collection backup", "namespace", namespace, "cloud", cloud, "collection", collection, "backup", backupName) 344 | } 345 | 346 | return finished, success, asyncStatus, err 347 | } 348 | 349 | func DeleteAsyncInfoForBackup(cloud string, collection string, backupName string, namespace string) (err error) { 350 | queryParams := url.Values{} 351 | queryParams.Add("action", "DELETESTATUS") 352 | queryParams.Add("requestid", AsyncIdForCollectionBackup(collection, backupName)) 353 | 354 | resp := &SolrAsyncResponse{} 355 | 356 | log.Info("Calling to delete async info for backup command.", "namespace", namespace, "cloud", cloud, "collection", collection, "backup", backupName) 357 | err = CallCollectionsApi(cloud, namespace, queryParams, resp) 358 | if err != nil { 359 | log.Error(err, "Error deleting async data for collection backup", "namespace", namespace, "cloud", cloud, "collection", collection, "backup", backupName) 360 | } 361 | 362 | return err 363 | } 364 | 365 | type SolrAsyncResponse struct { 366 | ResponseHeader SolrResponseHeader `json:"responseHeader"` 367 | 368 | // +optional 369 | RequestId string `json:"requestId"` 370 | 371 | // +optional 372 | Status SolrAsyncStatus `json:"status"` 373 | } 374 | 375 | type SolrResponseHeader struct { 376 | Status int `json:"status"` 377 | 378 | QTime int `json:"QTime"` 379 | } 380 | 381 | type SolrAsyncStatus struct { 382 | // Possible states can be found here: https://github.com/apache/lucene-solr/blob/1d85cd783863f75cea133fb9c452302214165a4d/solr/solrj/src/java/org/apache/solr/client/solrj/response/RequestStatusState.java 383 | AsyncState string `json:"state"` 384 | 385 | Message string `json:"msg"` 386 | } 387 | 388 | func EnsureDirectoryForBackup(solrCloud *solr.SolrCloud, backup string, config *rest.Config) (err error) { 389 | backupPath := BackupPath(backup) 390 | // Create an empty directory for the backup 391 | return RunExecForPod( 392 | solrCloud.GetAllSolrNodeNames()[0], 393 | solrCloud.Namespace, 394 | []string{"/bin/bash", "-c", "rm -rf " + backupPath + " && mkdir -p " + backupPath}, 395 | *config, 396 | ) 397 | } 398 | 399 | func RunExecForPod(podName string, namespace string, command []string, config rest.Config) (err error) { 400 | client := &kubernetes.Clientset{} 401 | if client, err = kubernetes.NewForConfig(&config); err != nil { 402 | return err 403 | } 404 | req := client.CoreV1().RESTClient().Post(). 405 | Resource("pods"). 406 | Name(podName). 407 | Namespace(namespace). 408 | SubResource("exec") 409 | scheme := runtime.NewScheme() 410 | if err := corev1.AddToScheme(scheme); err != nil { 411 | return fmt.Errorf("error adding to scheme: %v", err) 412 | } 413 | 414 | parameterCodec := runtime.NewParameterCodec(scheme) 415 | req.VersionedParams(&corev1.PodExecOptions{ 416 | Command: command, 417 | Container: "solrcloud-node", 418 | Stdin: false, 419 | Stdout: true, 420 | Stderr: true, 421 | TTY: false, 422 | }, parameterCodec) 423 | 424 | exec, err := remotecommand.NewSPDYExecutor(&config, "POST", req.URL()) 425 | if err != nil { 426 | return fmt.Errorf("error while creating Executor: %v", err) 427 | } 428 | 429 | var stdout, stderr bytes.Buffer 430 | err = exec.Stream(remotecommand.StreamOptions{ 431 | Stdout: &stdout, 432 | Stderr: &stderr, 433 | Tty: false, 434 | }) 435 | if err != nil { 436 | return fmt.Errorf("error in Stream: %v", err) 437 | } 438 | 439 | return nil 440 | } 441 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Solr Operator 2 | [![Build Status](https://travis-ci.com/bloomberg/solr-operator.svg?branch=master)](https://travis-ci.com/bloomberg/solr-operator) [![Go Report Card](https://goreportcard.com/badge/github.com/bloomberg/solr-operator)](https://goreportcard.com/report/github.com/bloomberg/solr-operator) ![Latest Version](https://img.shields.io/github/tag/bloomberg/solr-operator) [![Docker Pulls](https://img.shields.io/docker/pulls/bloomberg/solr-operator)](https://hub.docker.com/r/bloomberg/solr-operator/) 3 | 4 | The __Solr Operator__ manages Apache Solr Clouds within Kubernetes. It is built on top of the [Kube Builder](https://github.com/kubernetes-sigs/kubebuilder) framework. 5 | 6 | The project is currently in beta (`v1beta1`), and while we do not anticipate changing the API in backwards-incompatible ways there is no such guarantee yet. 7 | 8 | ## Menu 9 | 10 | - [Getting Started](#getting-started) 11 | - [Solr Cloud](#running-a-solr-cloud) 12 | - [Solr Collections](#solr-collections) 13 | - [Solr Backups](#solr-backups) 14 | - [Solr Metrics](#solr-prometheus-exporter) 15 | - [Contributions](#contributions) 16 | - [License](#license) 17 | - [Code of Conduct](#code-of-conduct) 18 | - [Security Vulnerability Reporting](#security-vulnerability-reporting) 19 | 20 | # Getting Started 21 | 22 | Install the Zookeeper & Etcd Operators, which this operator depends on by default. 23 | Each is optional, as described in the [Zookeeper](#zookeeper-reference) section. 24 | 25 | ```bash 26 | $ kubectl apply -f example/ext_ops.yaml 27 | ``` 28 | 29 | Install the Solr CRDs & Operator 30 | 31 | ```bash 32 | $ make install deploy 33 | ``` 34 | 35 | ## Running a Solr Cloud 36 | 37 | ### Creating 38 | 39 | Make sure that the solr-operator and a zookeeper-operator are running. 40 | 41 | Create an example Solr cloud, with the following configuration. 42 | 43 | ```bash 44 | $ cat example/test_solrcloud.yaml 45 | 46 | apiVersion: solr.bloomberg.com/v1beta1 47 | kind: SolrCloud 48 | metadata: 49 | name: example 50 | spec: 51 | replicas: 4 52 | solrImage: 53 | tag: 8.1.1 54 | ``` 55 | 56 | Apply it to your Kubernetes cluster. 57 | 58 | ```bash 59 | $ kubectl apply -f example/test_solrcloud.yaml 60 | $ kubectl get solrclouds 61 | 62 | NAME VERSION DESIREDNODES NODES READYNODES AGE 63 | example 8.1.1 4 2 1 2m 64 | 65 | $ kubectl get solrclouds 66 | 67 | NAME VERSION DESIREDNODES NODES READYNODES AGE 68 | example 8.1.1 4 4 4 8m 69 | ``` 70 | 71 | ### Scaling 72 | 73 | Increase the number of Solr nodes in your cluster. 74 | 75 | ```bash 76 | $ kubectl scale --replicas=5 solrcloud/example 77 | ``` 78 | 79 | ### Deleting 80 | 81 | Decrease the number of Solr nodes in your cluster. 82 | 83 | ```bash 84 | $ kubectl delete solrcloud example 85 | ``` 86 | 87 | ### Dependent Kubernetes Resources 88 | 89 | What actually gets created when the Solr Cloud is spun up? 90 | 91 | ```bash 92 | $ kubectl get all 93 | 94 | NAME READY STATUS RESTARTS AGE 95 | pod/example-solrcloud-0 1/1 Running 7 47h 96 | pod/example-solrcloud-1 1/1 Running 6 47h 97 | pod/example-solrcloud-2 1/1 Running 0 47h 98 | pod/example-solrcloud-3 1/1 Running 6 47h 99 | pod/example-solrcloud-zk-0 1/1 Running 0 49d 100 | pod/example-solrcloud-zk-1 1/1 Running 0 49d 101 | pod/example-solrcloud-zk-2 1/1 Running 0 49d 102 | pod/example-solrcloud-zk-3 1/1 Running 0 49d 103 | pod/example-solrcloud-zk-4 1/1 Running 0 49d 104 | pod/solr-operator-8449d4d96f-cmf8p 1/1 Running 0 47h 105 | pod/zk-operator-674676769c-gd4jr 1/1 Running 0 49d 106 | 107 | NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE 108 | service/example-solrcloud-0 ClusterIP ##.###.###.## 80/TCP 47h 109 | service/example-solrcloud-1 ClusterIP ##.###.##.# 80/TCP 47h 110 | service/example-solrcloud-2 ClusterIP ##.###.###.## 80/TCP 47h 111 | service/example-solrcloud-3 ClusterIP ##.###.##.### 80/TCP 47h 112 | service/example-solrcloud-common ClusterIP ##.###.###.### 80/TCP 47h 113 | service/example-solrcloud-headless ClusterIP None 80/TCP 47h 114 | service/example-solrcloud-zk-client ClusterIP ##.###.###.### 21210/TCP 49d 115 | service/example-solrcloud-zk-headless ClusterIP None 22210/TCP,23210/TCP 49d 116 | 117 | NAME READY UP-TO-DATE AVAILABLE AGE 118 | deployment.apps/solr-operator 1/1 1 1 49d 119 | deployment.apps/zk-operator 1/1 1 1 49d 120 | 121 | NAME DESIRED CURRENT READY AGE 122 | replicaset.apps/solr-operator-8449d4d96f 1 1 1 2d1h 123 | replicaset.apps/zk-operator-674676769c 1 1 1 49d 124 | 125 | NAME READY AGE 126 | statefulset.apps/example-solrcloud 4/4 47h 127 | statefulset.apps/example-solrcloud-zk 5/5 49d 128 | 129 | NAME HOSTS PORTS AGE 130 | ingress.extensions/example-solrcloud-common default-example-solrcloud.test.domain,default-example-solrcloud-0.test.domain + 3 more... 80 2d2h 131 | 132 | NAME VERSION DESIREDNODES NODES READYNODES AGE 133 | solrcloud.solr.bloomberg.com/example 8.1.1 4 4 4 47h 134 | ``` 135 | 136 | ### Solr Collections 137 | 138 | Solr-operator can manage the creation, deletion and modification of Solr collections. 139 | 140 | Collection creation requires a Solr Cloud to apply against. Presently, SolrCollection supports both implicit and compositeId router types, with some of the basic configuration options including `autoAddReplicas`. 141 | 142 | Create an example set of collections against on the "example" solr cloud 143 | 144 | ```bash 145 | $ cat example/test_solrcollection.yaml 146 | 147 | apiVersion: solr.bloomberg.com/v1beta1 148 | kind: SolrCollection 149 | metadata: 150 | name: example-collection-1 151 | spec: 152 | solrCloud: example 153 | collection: example-collection 154 | routerName: compositeId 155 | autoAddReplicas: false 156 | numShards: 2 157 | replicationFactor: 1 158 | maxShardsPerNode: 1 159 | collectionConfigName: "_default" 160 | --- 161 | apiVersion: solr.bloomberg.com/v1beta1 162 | kind: SolrCollection 163 | metadata: 164 | name: example-collection-2-compositeid-autoadd 165 | spec: 166 | solrCloud: example 167 | collection: example-collection-2 168 | routerName: compositeId 169 | autoAddReplicas: true 170 | numShards: 2 171 | replicationFactor: 1 172 | maxShardsPerNode: 1 173 | collectionConfigName: "_default" 174 | --- 175 | apiVersion: solr.bloomberg.com/v1beta1 176 | kind: SolrCollection 177 | metadata: 178 | name: example-collection-3-implicit 179 | spec: 180 | solrCloud: example 181 | collection: example-collection-3-implicit 182 | routerName: implicit 183 | autoAddReplicas: true 184 | numShards: 2 185 | replicationFactor: 1 186 | maxShardsPerNode: 1 187 | shards: "fooshard1,fooshard2" 188 | collectionConfigName: "_default" 189 | ``` 190 | 191 | ```bash 192 | $ kubectl apply -f examples/test_solrcollections.yaml 193 | ``` 194 | 195 | ## Zookeeper 196 | ======= 197 | ### Zookeeper Reference 198 | 199 | Solr Clouds require an Apache Zookeeper to connect to. 200 | 201 | The Solr operator gives a few options. 202 | 203 | #### ZK Connection Info 204 | 205 | This is an external/internal connection string as well as an optional chRoot to an already running Zookeeeper ensemble. 206 | If you provide an external connection string, you do not _have_ to provide an internal one as well. 207 | 208 | #### Provided Instance 209 | 210 | If you do not require the Solr cloud to run cross-kube cluster, and do not want to manage your own Zookeeper ensemble, 211 | the solr-operator can manage Zookeeper ensemble(s) for you. 212 | 213 | ##### Zookeeper 214 | 215 | Using the [zookeeper-operator](https://github.com/pravega/zookeeper-operator), a new Zookeeper ensemble can be spun up for 216 | each solrCloud that has this option specified. 217 | 218 | The startup parameter `zookeeper-operator` must be provided on startup of the solr-operator for this parameter to be available. 219 | 220 | ##### Zetcd 221 | 222 | Using [etcd-operator](https://github.com/coreos/etcd-operator), a new Etcd ensemble can be spun up for each solrCloud that has this option specified. 223 | A [Zetcd](https://github.com/etcd-io/zetcd) deployment is also created so that Solr can interact with Etcd as if it were a Zookeeper ensemble. 224 | 225 | The startup parameter `etcd-operator` must be provided on startup of the solr-operator for this parameter to be available. 226 | 227 | ## Solr Backups 228 | 229 | Solr backups require 3 things: 230 | - A solr cloud running in kubernetes to backup 231 | - The list of collections to backup 232 | - A shared volume reference that can be written to from many clouds 233 | - This could be a NFS volume, a persistent volume claim (that has `ReadWriteMany` access), etc. 234 | - The same volume can be used for many solr clouds in the same namespace, as the data stored within the volume is namespaced. 235 | - A way to persist the data. The currently supported persistence methods are: 236 | - A volume reference (this does not have to be `ReadWriteMany`) 237 | - An S3 endpoint. 238 | 239 | Backups will be tarred before they are persisted. 240 | 241 | There is no current way to restore these backups, but that is in the roadmap to implement. 242 | 243 | 244 | ## Solr Prometheus Exporter 245 | 246 | Solr metrics can be collected from solr clouds/standalone solr both residing within the kubernetes cluster and outside. 247 | To use the Prometheus exporter, the easiest thing to do is just provide a reference to a Solr instance. That can be any of the following: 248 | - The name and namespace of the Solr Cloud CRD 249 | - The Zookeeper connection information of the Solr Cloud 250 | - The address of the standalone Solr instance 251 | 252 | You can also provide a custom Prometheus Exporter config, Solr version, and exporter options as described in the 253 | [Solr ref-guide](https://lucene.apache.org/solr/guide/monitoring-solr-with-prometheus-and-grafana.html#command-line-parameters). 254 | 255 | Note that a few of the official Solr docker images do not enable the Prometheus Exporter. 256 | Versions `6.6` - `7.x` and `8.2` - `master` should have the exporter available. 257 | 258 | 259 | ## Solr Images 260 | 261 | ### Official Solr Images 262 | 263 | The solr-operator will work with any of the [official Solr images](https://hub.docker.com/_/solr) currently available. 264 | 265 | ### Build Your Own Private Solr Images 266 | 267 | The solr-operator supports private Docker repo access for Solr images you may want to store in a private Docker repo. It is recommended to source your image from the official Solr images. 268 | 269 | Using a private image requires you have a K8s secret preconfigured with appropreiate access to the image. (type: kubernetes.io/dockerconfigjson) 270 | 271 | ``` 272 | apiVersion: solr.bloomberg.com/v1beta1 273 | kind: SolrCloud 274 | metadata: 275 | name: example-private-repo-solr-image 276 | spec: 277 | replicas: 3 278 | solrImage: 279 | repository: myprivate-repo.jfrog.io/solr 280 | tag: 8.2.0 281 | imagePullSecret: "k8s-docker-registry-secret" 282 | ``` 283 | 284 | ## Solr Operator 285 | 286 | ### Solr Operator Input Args 287 | 288 | * **-zookeeper-operator** Whether or not to use the Zookeeper Operator to create dependent Zookeeepers. 289 | Required to use the `ProvidedZookeeper.Zookeeper` option within the Spec. 290 | If _true_, then a Zookeeper Operator must be running for the cluster. 291 | ( _true_ | _false_ , defaults to _false_) 292 | * **-etcd-operator** Whether or not to use the Etcd Operator to create dependent Zetcd clusters. 293 | Required to use the `ProvidedZookeeper.Zetcd` option within the Spec. 294 | If _true_, then an Etcd Operator must be running for the cluster. 295 | ( _true_ | _false_ , defaults to _false_) 296 | * **-ingress-base-url** If you desire to make solr externally addressable via ingresses, a base ingress domain is required. 297 | Solr Clouds will be created with ingress rules at `*.(ingress-base-url)`. 298 | ( _optional_ , e.g. `ing.base.domain` ) 299 | ## Development 300 | 301 | ### Updating the CRD 302 | 303 | The CRD should be updated anytime you update the API. 304 | 305 | ```bash 306 | $ make manifests 307 | ``` 308 | 309 | ### Docker 310 | 311 | #### Docker Images 312 | 313 | Two Docker images are published to [DockerHub](https://hub.docker.com/r/bloomberg/solr-operator), both based off of the same base image. 314 | 315 | - [Base Image](build/Dockerfile.build.dynamic) - Downloads vendor directories, builds operator executable (This is not published, only used to build the following images) 316 | - [Slim Image](build/Dockerfile.slim) - Contains only the operator executable 317 | - [Vendor Image](build/Dockerfile.slim) - Contains the operator executable as well as all vendored dependencies (at `/solr-operator-vendor-sources`) 318 | 319 | #### Building 320 | 321 | Building and releasing a test operator image with a custom namespace. 322 | 323 | ```bash 324 | $ NAMESPACE=your-namespace make docker-base-build docker-build docker-push 325 | ``` 326 | 327 | ### Docker for Mac Local Development Setup 328 | 329 | #### Install and configure on Docker for Mac 330 | 331 | 1. (Download and install latest stable version)[https://docs.docker.com/docker-for-mac/install/] 332 | 2. Enable K8s under perferences 333 | 3. Ensure you have kubectl installed on your Mac (if using Brew: `brew install kubernetes-cli` 334 | 3. Run through [Getting Started](#getting-started) 335 | 3. Install nginx-ingress configuration 336 | 337 | ``` 338 | kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/master/deploy/static/mandatory.yaml 339 | kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/master/deploy/static/provider/cloud-generic.yaml 340 | ``` 341 | 342 | 4. Ensure your /etc/hosts file is setup to use the ingress for your SolrCloud. Here is what you need if you name your SolrCloud 'example' with 3 replicas 343 | 344 | ``` 345 | 127.0.0.1 localhost default-example-solrcloud.ing.local.domain ing.local.domain default-example-solrcloud-0.ing.local.domain default-example-solrcloud-1.ing.local.domain default-example-solrcloud-2.ing.local.domain dinghy-ping.localhost 346 | ``` 347 | 348 | 5. Navigate to your browser: http://default-example-solrcloud.ing.local.domain/solr/#/ to validate everything is working 349 | 350 | 351 | ## Version Compatability 352 | 353 | ### Backwards Incompatible CRD Changes 354 | 355 | #### v0.1.1 356 | - `SolrCloud.Spec.persistentVolumeClaim` was renamed to `SolrCloud.Spec.dataPvcSpec` 357 | 358 | ### Compatibility with Kubernetes Versions 359 | 360 | #### Fully Compatible - v1.12+ 361 | 362 | #### Feature Gates required for older versions 363 | 364 | - *v1.10* - CustomResourceSubresources 365 | 366 | ## Contributions 367 | 368 | We :heart: contributions. 369 | 370 | Have you had a good experience with ? Why not share some love and contribute code, or just let us know about any issues you had with it? 371 | 372 | We welcome issue reports [here](../../../issues); be sure to choose the proper issue template for your issue, so that we can be sure you're providing the necessary information. 373 | 374 | Before sending a [Pull Request](../../../pulls), please make sure you read our 375 | [Contribution Guidelines](https://github.com/bloomberg/.github/blob/master/CONTRIBUTING.md). 376 | 377 | ## License 378 | 379 | Please read the [LICENSE](../LICENSE) file here. 380 | 381 | ## Code of Conduct 382 | 383 | This project has adopted a [Code of Conduct](https://github.com/bloomberg/.github/blob/master/CODE_OF_CONDUCT.md). 384 | If you have any concerns about the Code, or behavior which you have experienced in the project, please 385 | contact us at opensource@bloomberg.net. 386 | 387 | ## Security Vulnerability Reporting 388 | 389 | If you believe you have identified a security vulnerability in this project, please send email to the project 390 | team at opensource@bloomberg.net, detailing the suspected issue and any methods you've found to reproduce it. 391 | 392 | Please do NOT open an issue in the GitHub repository, as we'd prefer to keep vulnerability reports private until 393 | we've had an opportunity to review and address them. 394 | --------------------------------------------------------------------------------