From 19aa4477f9d3677242a0cefc4a42c49b6b983ba7 Mon Sep 17 00:00:00 2001 From: Ewout Prangsma Date: Fri, 18 May 2018 11:54:53 +0200 Subject: [PATCH 1/6] Adding structure for ArangoDeploymentReplication operator --- Makefile | 6 +- main.go | 51 +-- .../arango-deployment-replication-dev.yaml | 130 ++++++ .../deployment-replication.yaml | 52 +++ .../deployment-replication/rbac.yaml | 75 ++++ pkg/apis/replication/v1alpha/doc.go | 25 ++ pkg/apis/replication/v1alpha/errors.go | 37 ++ pkg/apis/replication/v1alpha/register.go | 59 +++ pkg/apis/replication/v1alpha/replication.go | 63 +++ .../replication/v1alpha/replication_phase.go | 42 ++ .../replication/v1alpha/replication_spec.go | 57 +++ .../replication/v1alpha/replication_state.go | 32 ++ .../v1alpha/zz_generated.deepcopy.go | 122 ++++++ .../clientset/versioned/clientset.go | 26 +- .../versioned/fake/clientset_generated.go | 12 + .../clientset/versioned/fake/register.go | 2 + .../clientset/versioned/scheme/register.go | 2 + .../v1alpha/arangodeploymentreplication.go | 175 ++++++++ .../typed/replication/v1alpha/doc.go | 21 + .../typed/replication/v1alpha/fake/doc.go | 21 + .../fake/fake_arangodeploymentreplication.go | 141 +++++++ .../v1alpha/fake/fake_replication_client.go | 41 ++ .../v1alpha/generated_expansion.go | 22 + .../replication/v1alpha/replication_client.go | 91 +++++ .../informers/externalversions/factory.go | 6 + .../informers/externalversions/generic.go | 5 + .../externalversions/replication/interface.go | 50 +++ .../v1alpha/arangodeploymentreplication.go | 93 +++++ .../replication/v1alpha/interface.go | 49 +++ .../v1alpha/arangodeploymentreplication.go | 98 +++++ .../v1alpha/expansion_generated.go | 31 ++ pkg/operator/crd.go | 10 +- pkg/operator/operator.go | 82 ++-- .../operator_deployment_relication.go | 215 ++++++++++ pkg/replication/deployment_replication.go | 380 ++++++++++++++++++ pkg/replication/errors.go | 29 ++ tools/manifests/manifest_builder.go | 57 ++- 37 files changed, 2337 insertions(+), 73 deletions(-) create mode 100644 manifests/arango-deployment-replication-dev.yaml create mode 100644 manifests/templates/deployment-replication/deployment-replication.yaml create mode 100644 manifests/templates/deployment-replication/rbac.yaml create mode 100644 pkg/apis/replication/v1alpha/doc.go create mode 100644 pkg/apis/replication/v1alpha/errors.go create mode 100644 pkg/apis/replication/v1alpha/register.go create mode 100644 pkg/apis/replication/v1alpha/replication.go create mode 100644 pkg/apis/replication/v1alpha/replication_phase.go create mode 100644 pkg/apis/replication/v1alpha/replication_spec.go create mode 100644 pkg/apis/replication/v1alpha/replication_state.go create mode 100644 pkg/apis/replication/v1alpha/zz_generated.deepcopy.go create mode 100644 pkg/generated/clientset/versioned/typed/replication/v1alpha/arangodeploymentreplication.go create mode 100644 pkg/generated/clientset/versioned/typed/replication/v1alpha/doc.go create mode 100644 pkg/generated/clientset/versioned/typed/replication/v1alpha/fake/doc.go create mode 100644 pkg/generated/clientset/versioned/typed/replication/v1alpha/fake/fake_arangodeploymentreplication.go create mode 100644 pkg/generated/clientset/versioned/typed/replication/v1alpha/fake/fake_replication_client.go create mode 100644 pkg/generated/clientset/versioned/typed/replication/v1alpha/generated_expansion.go create mode 100644 pkg/generated/clientset/versioned/typed/replication/v1alpha/replication_client.go create mode 100644 pkg/generated/informers/externalversions/replication/interface.go create mode 100644 pkg/generated/informers/externalversions/replication/v1alpha/arangodeploymentreplication.go create mode 100644 pkg/generated/informers/externalversions/replication/v1alpha/interface.go create mode 100644 pkg/generated/listers/replication/v1alpha/arangodeploymentreplication.go create mode 100644 pkg/generated/listers/replication/v1alpha/expansion_generated.go create mode 100644 pkg/operator/operator_deployment_relication.go create mode 100644 pkg/replication/deployment_replication.go create mode 100644 pkg/replication/errors.go diff --git a/Makefile b/Makefile index dde514010..4c008e2c2 100644 --- a/Makefile +++ b/Makefile @@ -50,6 +50,7 @@ ifndef MANIFESTSUFFIX endif endif MANIFESTPATHDEPLOYMENT := manifests/arango-deployment$(MANIFESTSUFFIX).yaml +MANIFESTPATHDEPLOYMENTREPLICATION := manifests/arango-deployment-replication$(MANIFESTSUFFIX).yaml MANIFESTPATHSTORAGE := manifests/arango-storage$(MANIFESTSUFFIX).yaml MANIFESTPATHTEST := manifests/arango-test$(MANIFESTSUFFIX).yaml ifndef DEPLOYMENTNAMESPACE @@ -172,7 +173,7 @@ update-generated: $(GOBUILDDIR) "all" \ "github.com/arangodb/kube-arangodb/pkg/generated" \ "github.com/arangodb/kube-arangodb/pkg/apis" \ - "deployment:v1alpha storage:v1alpha" \ + "deployment:v1alpha replication:v1alpha storage:v1alpha" \ --go-header-file "./tools/codegen/boilerplate.go.txt" \ $(VERIFYARGS) @@ -270,6 +271,7 @@ endif kubectl apply -f manifests/crd.yaml kubectl apply -f $(MANIFESTPATHSTORAGE) kubectl apply -f $(MANIFESTPATHDEPLOYMENT) + kubectl apply -f $(MANIFESTPATHDEPLOYMENTREPLICATION) kubectl apply -f $(MANIFESTPATHTEST) $(ROOTDIR)/scripts/kube_create_storage.sh $(DEPLOYMENTNAMESPACE) $(ROOTDIR)/scripts/kube_run_tests.sh $(DEPLOYMENTNAMESPACE) $(TESTIMAGE) "$(ENTERPRISEIMAGE)" $(TESTTIMEOUT) $(TESTLENGTHOPTIONS) @@ -345,6 +347,7 @@ minikube-start: delete-operator: kubectl delete -f $(MANIFESTPATHTEST) --ignore-not-found kubectl delete -f $(MANIFESTPATHDEPLOYMENT) --ignore-not-found + kubectl delete -f $(MANIFESTPATHDEPLOYMENTREPLICATION) --ignore-not-found kubectl delete -f $(MANIFESTPATHSTORAGE) --ignore-not-found .PHONY: redeploy-operator @@ -352,5 +355,6 @@ redeploy-operator: delete-operator manifests kubectl apply -f manifests/crd.yaml kubectl apply -f $(MANIFESTPATHSTORAGE) kubectl apply -f $(MANIFESTPATHDEPLOYMENT) + kubectl apply -f $(MANIFESTPATHDEPLOYMENTREPLICATION) kubectl apply -f $(MANIFESTPATHTEST) kubectl get pods diff --git a/main.go b/main.go index 5f83ac185..56b276fb7 100644 --- a/main.go +++ b/main.go @@ -79,15 +79,17 @@ var ( tlsSecretName string } operatorOptions struct { - enableDeployment bool // Run deployment operator - enableStorage bool // Run deployment operator + enableDeployment bool // Run deployment operator + enableDeploymentReplication bool // Run deployment-replication operator + enableStorage bool // Run local-storage operator } chaosOptions struct { allowed bool } - livenessProbe probe.LivenessProbe - deploymentProbe probe.ReadyProbe - storageProbe probe.ReadyProbe + livenessProbe probe.LivenessProbe + deploymentProbe probe.ReadyProbe + deploymentReplicationProbe probe.ReadyProbe + storageProbe probe.ReadyProbe ) func init() { @@ -97,6 +99,7 @@ func init() { f.StringVar(&serverOptions.tlsSecretName, "server.tls-secret-name", "", "Name of secret containing tls.crt & tls.key for HTTPS server (if empty, self-signed certificate is used)") f.StringVar(&logLevel, "log.level", defaultLogLevel, "Set initial log level") f.BoolVar(&operatorOptions.enableDeployment, "operator.deployment", false, "Enable to run the ArangoDeployment operator") + f.BoolVar(&operatorOptions.enableDeploymentReplication, "operator.deployment-replication", false, "Enable to run the ArangoDeploymentReplication operator") f.BoolVar(&operatorOptions.enableStorage, "operator.storage", false, "Enable to run the ArangoLocalStorage operator") f.BoolVar(&chaosOptions.allowed, "chaos.allowed", false, "Set to allow chaos in deployments. Only activated when allowed and enabled in deployment") } @@ -121,8 +124,8 @@ func cmdMainRun(cmd *cobra.Command, args []string) { } // Check operating mode - if !operatorOptions.enableDeployment && !operatorOptions.enableStorage { - cliLog.Fatal().Err(err).Msg("Turn on --operator.deployment or --operator.storage or both") + if !operatorOptions.enableDeployment && !operatorOptions.enableDeploymentReplication && !operatorOptions.enableStorage { + cliLog.Fatal().Err(err).Msg("Turn on --operator.deployment, --operator.deployment-replication, --operator.storage or any combination of these") } // Log version @@ -209,24 +212,26 @@ func newOperatorConfigAndDeps(id, namespace, name string) (operator.Config, oper eventRecorder := createRecorder(cliLog, kubecli, name, namespace) cfg := operator.Config{ - ID: id, - Namespace: namespace, - PodName: name, - ServiceAccount: serviceAccount, - LifecycleImage: image, - EnableDeployment: operatorOptions.enableDeployment, - EnableStorage: operatorOptions.enableStorage, - AllowChaos: chaosOptions.allowed, + ID: id, + Namespace: namespace, + PodName: name, + ServiceAccount: serviceAccount, + LifecycleImage: image, + EnableDeployment: operatorOptions.enableDeployment, + EnableDeploymentReplication: operatorOptions.enableDeploymentReplication, + EnableStorage: operatorOptions.enableStorage, + AllowChaos: chaosOptions.allowed, } deps := operator.Dependencies{ - LogService: logService, - KubeCli: kubecli, - KubeExtCli: kubeExtCli, - CRCli: crCli, - EventRecorder: eventRecorder, - LivenessProbe: &livenessProbe, - DeploymentProbe: &deploymentProbe, - StorageProbe: &storageProbe, + LogService: logService, + KubeCli: kubecli, + KubeExtCli: kubeExtCli, + CRCli: crCli, + EventRecorder: eventRecorder, + LivenessProbe: &livenessProbe, + DeploymentProbe: &deploymentProbe, + DeploymentReplicationProbe: &deploymentReplicationProbe, + StorageProbe: &storageProbe, } return cfg, deps, nil diff --git a/manifests/arango-deployment-replication-dev.yaml b/manifests/arango-deployment-replication-dev.yaml new file mode 100644 index 000000000..a95878fb8 --- /dev/null +++ b/manifests/arango-deployment-replication-dev.yaml @@ -0,0 +1,130 @@ +## deployment-replication/rbac.yaml +## Cluster role granting access to ArangoDeploymentReplication resources. +apiVersion: rbac.authorization.k8s.io/v1beta1 +kind: ClusterRole +metadata: + name: arango-deployment-replications +rules: +- apiGroups: ["replication.database.arangodb.com"] + resources: ["arangodeploymentreplications"] + verbs: ["*"] + +--- + +## Cluster role granting access to all resources needed by the ArangoDeploymentReplication operator. +apiVersion: rbac.authorization.k8s.io/v1beta1 +kind: ClusterRole +metadata: + name: arango-deployment-replication-operator +rules: +- apiGroups: ["replication.database.arangodb.com"] + resources: ["arangodeploymentreplications"] + verbs: ["*"] +- apiGroups: ["database.arangodb.com"] + resources: ["arangodeployments"] + verbs: ["get"] +- apiGroups: ["apiextensions.k8s.io"] + resources: ["customresourcedefinitions"] + verbs: ["get"] +- apiGroups: [""] + resources: ["pods", "services", "endpoints", "persistentvolumeclaims", "events", "secrets"] + verbs: ["*"] +- apiGroups: [""] + resources: ["nodes"] + verbs: ["get"] +- apiGroups: ["apps"] + resources: ["deployments"] + verbs: ["*"] + +--- + +## Bind the cluster role granting access to ArangoDeploymentReplication resources +## to the default service account of the configured namespace. +apiVersion: rbac.authorization.k8s.io/v1beta1 +kind: RoleBinding +metadata: + name: arango-deployment-replications + namespace: default +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: arango-deployment-replications +subjects: +- kind: ServiceAccount + name: default + namespace: default + +--- + +## Bind the cluster role granting access to all resources needed by +## the ArangoDeploymentReplication operator to the default service account +## the is being used to run the operator deployment. +apiVersion: rbac.authorization.k8s.io/v1beta1 +kind: ClusterRoleBinding +metadata: + name: arango-deployment-replication-operator-default +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: arango-deployment-replication-operator +subjects: +- kind: ServiceAccount + name: default + namespace: default + +--- + +## deployment-replication/deployment-replication.yaml + +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + name: arango-deployment-replication-operator + namespace: default +spec: + replicas: 1 + strategy: + type: Recreate + template: + metadata: + labels: + name: arango-deployment-replication-operator + app: arango-deployment-replication-operator + spec: + containers: + - name: operator + imagePullPolicy: IfNotPresent + image: ewoutp/kube-arangodb@sha256:ee3c19a789b7ac9e4e606617da643a7a54a2dc6d398db7a573f2120d77a45258 + args: + - --operator.deployment-replication + env: + - name: MY_POD_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: MY_POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: MY_POD_IP + valueFrom: + fieldRef: + fieldPath: status.podIP + ports: + - name: metrics + containerPort: 8528 + livenessProbe: + httpGet: + path: /health + port: 8528 + scheme: HTTPS + initialDelaySeconds: 5 + periodSeconds: 10 + readinessProbe: + httpGet: + path: /ready/deploymentReplication + port: 8528 + scheme: HTTPS + initialDelaySeconds: 5 + periodSeconds: 10 + diff --git a/manifests/templates/deployment-replication/deployment-replication.yaml b/manifests/templates/deployment-replication/deployment-replication.yaml new file mode 100644 index 000000000..debb91d3d --- /dev/null +++ b/manifests/templates/deployment-replication/deployment-replication.yaml @@ -0,0 +1,52 @@ + +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + name: {{ .DeploymentReplication.OperatorDeploymentName }} + namespace: {{ .DeploymentReplication.Operator.Namespace }} +spec: + replicas: 1 + strategy: + type: Recreate + template: + metadata: + labels: + name: {{ .DeploymentReplication.OperatorDeploymentName }} + app: arango-deployment-replication-operator + spec: + containers: + - name: operator + imagePullPolicy: {{ .ImagePullPolicy }} + image: {{ .Image }} + args: + - --operator.deployment-replication + env: + - name: MY_POD_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: MY_POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: MY_POD_IP + valueFrom: + fieldRef: + fieldPath: status.podIP + ports: + - name: metrics + containerPort: 8528 + livenessProbe: + httpGet: + path: /health + port: 8528 + scheme: HTTPS + initialDelaySeconds: 5 + periodSeconds: 10 + readinessProbe: + httpGet: + path: /ready/deploymentReplication + port: 8528 + scheme: HTTPS + initialDelaySeconds: 5 + periodSeconds: 10 diff --git a/manifests/templates/deployment-replication/rbac.yaml b/manifests/templates/deployment-replication/rbac.yaml new file mode 100644 index 000000000..90df2baf4 --- /dev/null +++ b/manifests/templates/deployment-replication/rbac.yaml @@ -0,0 +1,75 @@ +{{- if .RBAC -}} +## Cluster role granting access to ArangoDeploymentReplication resources. +apiVersion: rbac.authorization.k8s.io/v1beta1 +kind: ClusterRole +metadata: + name: {{ .DeploymentReplication.User.RoleName }} +rules: +- apiGroups: ["replication.database.arangodb.com"] + resources: ["arangodeploymentreplications"] + verbs: ["*"] + +--- + +## Cluster role granting access to all resources needed by the ArangoDeploymentReplication operator. +apiVersion: rbac.authorization.k8s.io/v1beta1 +kind: ClusterRole +metadata: + name: {{ .DeploymentReplication.Operator.RoleName }} +rules: +- apiGroups: ["replication.database.arangodb.com"] + resources: ["arangodeploymentreplications"] + verbs: ["*"] +- apiGroups: ["database.arangodb.com"] + resources: ["arangodeployments"] + verbs: ["get"] +- apiGroups: ["apiextensions.k8s.io"] + resources: ["customresourcedefinitions"] + verbs: ["get"] +- apiGroups: [""] + resources: ["pods", "services", "endpoints", "persistentvolumeclaims", "events", "secrets"] + verbs: ["*"] +- apiGroups: [""] + resources: ["nodes"] + verbs: ["get"] +- apiGroups: ["apps"] + resources: ["deployments"] + verbs: ["*"] + +--- + +## Bind the cluster role granting access to ArangoDeploymentReplication resources +## to the default service account of the configured namespace. +apiVersion: rbac.authorization.k8s.io/v1beta1 +kind: RoleBinding +metadata: + name: {{ .DeploymentReplication.User.RoleBindingName }} + namespace: {{ .DeploymentReplication.User.Namespace }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: {{ .DeploymentReplication.User.RoleName }} +subjects: +- kind: ServiceAccount + name: {{ .DeploymentReplication.User.ServiceAccountName }} + namespace: {{ .DeploymentReplication.User.Namespace }} + +--- + +## Bind the cluster role granting access to all resources needed by +## the ArangoDeploymentReplication operator to the default service account +## the is being used to run the operator deployment. +apiVersion: rbac.authorization.k8s.io/v1beta1 +kind: ClusterRoleBinding +metadata: + name: {{ .DeploymentReplication.Operator.RoleBindingName }}-{{ .DeploymentReplication.Operator.Namespace }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: {{ .DeploymentReplication.Operator.RoleName }} +subjects: +- kind: ServiceAccount + name: {{ .DeploymentReplication.Operator.ServiceAccountName }} + namespace: {{ .DeploymentReplication.Operator.Namespace }} + +{{- end -}} diff --git a/pkg/apis/replication/v1alpha/doc.go b/pkg/apis/replication/v1alpha/doc.go new file mode 100644 index 000000000..a33182847 --- /dev/null +++ b/pkg/apis/replication/v1alpha/doc.go @@ -0,0 +1,25 @@ +// +// DISCLAIMER +// +// Copyright 2018 ArangoDB GmbH, Cologne, Germany +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// +// Author Ewout Prangsma +// + +// +k8s:deepcopy-gen=package +// +groupName=replication.database.arangodb.com +package v1alpha diff --git a/pkg/apis/replication/v1alpha/errors.go b/pkg/apis/replication/v1alpha/errors.go new file mode 100644 index 000000000..64f72e81c --- /dev/null +++ b/pkg/apis/replication/v1alpha/errors.go @@ -0,0 +1,37 @@ +// +// DISCLAIMER +// +// Copyright 2018 ArangoDB GmbH, Cologne, Germany +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// +// Author Ewout Prangsma +// + +package v1alpha + +import "github.com/pkg/errors" + +var ( + // ValidationError indicates a validation failure + ValidationError = errors.New("validation failed") + + maskAny = errors.WithStack +) + +// IsValidation return true when the given error is or is caused by a ValidationError. +func IsValidation(err error) bool { + return errors.Cause(err) == ValidationError +} diff --git a/pkg/apis/replication/v1alpha/register.go b/pkg/apis/replication/v1alpha/register.go new file mode 100644 index 000000000..ccab2f54d --- /dev/null +++ b/pkg/apis/replication/v1alpha/register.go @@ -0,0 +1,59 @@ +// +// DISCLAIMER +// +// Copyright 2018 ArangoDB GmbH, Cologne, Germany +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// +// Author Ewout Prangsma +// + +package v1alpha + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +const ( + ArangoDeploymentReplicationResourceKind = "ArangoDeploymentReplication" + ArangoDeploymentReplicationResourcePlural = "arangodeploymentreplications" + groupName = "replication.database.arangodb.com" +) + +var ( + SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes) + AddToScheme = SchemeBuilder.AddToScheme + + SchemeGroupVersion = schema.GroupVersion{Group: groupName, Version: "v1alpha"} + ArangoDeploymentReplicationCRDName = ArangoDeploymentReplicationResourcePlural + "." + groupName + ArangoDeploymentReplicationShortNames = []string{"arangorepl"} +) + +// Resource gets an ArangoCluster GroupResource for a specified resource +func Resource(resource string) schema.GroupResource { + return SchemeGroupVersion.WithResource(resource).GroupResource() +} + +// addKnownTypes adds the set of types defined in this package to the supplied scheme. +func addKnownTypes(s *runtime.Scheme) error { + s.AddKnownTypes(SchemeGroupVersion, + &ArangoDeploymentReplication{}, + &ArangoDeploymentReplicationList{}, + ) + metav1.AddToGroupVersion(s, SchemeGroupVersion) + return nil +} diff --git a/pkg/apis/replication/v1alpha/replication.go b/pkg/apis/replication/v1alpha/replication.go new file mode 100644 index 000000000..4bb3be55f --- /dev/null +++ b/pkg/apis/replication/v1alpha/replication.go @@ -0,0 +1,63 @@ +// +// DISCLAIMER +// +// Copyright 2018 ArangoDB GmbH, Cologne, Germany +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// +// Author Ewout Prangsma +// + +package v1alpha + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// ArangoDeploymentReplicationList is a list of ArangoDB deployment replications. +type ArangoDeploymentReplicationList struct { + metav1.TypeMeta `json:",inline"` + // Standard list metadata + // More info: http://releases.k8s.io/HEAD/docs/devel/api-conventions.md#metadata + metav1.ListMeta `json:"metadata,omitempty"` + Items []ArangoDeploymentReplication `json:"items"` +} + +// +genclient +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// ArangoDeploymentReplication contains the entire Kubernetes info for an ArangoDB +// local storage provider. +type ArangoDeploymentReplication struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + Spec DeploymentReplicationSpec `json:"spec"` + Status DeploymentReplicationStatus `json:"status"` +} + +// AsOwner creates an OwnerReference for the given replication +func (d *ArangoDeploymentReplication) AsOwner() metav1.OwnerReference { + trueVar := true + return metav1.OwnerReference{ + APIVersion: SchemeGroupVersion.String(), + Kind: ArangoDeploymentReplicationResourceKind, + Name: d.Name, + UID: d.UID, + Controller: &trueVar, + BlockOwnerDeletion: &trueVar, + } +} diff --git a/pkg/apis/replication/v1alpha/replication_phase.go b/pkg/apis/replication/v1alpha/replication_phase.go new file mode 100644 index 000000000..670224948 --- /dev/null +++ b/pkg/apis/replication/v1alpha/replication_phase.go @@ -0,0 +1,42 @@ +// +// DISCLAIMER +// +// Copyright 2018 ArangoDB GmbH, Cologne, Germany +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// +// Author Ewout Prangsma +// + +package v1alpha + +// DeploymentReplicationPhase is a strongly typed lifetime phase of a deployment replication +type DeploymentReplicationPhase string + +const ( + // DeploymentReplicationPhaseNone indicates that the phase is not set yet + DeploymentReplicationPhaseNone DeploymentReplicationPhase = "" + // DeploymentReplicationPhaseRunning indicates that the deployment replication is under control of the + // ArangoDeploymentReplication operator. + DeploymentReplicationPhaseRunning DeploymentReplicationPhase = "Running" + // DeploymentReplicationPhaseFailed indicates that a deployment replication is in a failed state + // from which automatic recovery is impossible. Inspect `Reason` for more info. + DeploymentReplicationPhaseFailed DeploymentReplicationPhase = "Failed" +) + +// IsFailed returns true if given state is DeploymentStateFailed +func (cs DeploymentReplicationPhase) IsFailed() bool { + return cs == DeploymentReplicationPhaseFailed +} diff --git a/pkg/apis/replication/v1alpha/replication_spec.go b/pkg/apis/replication/v1alpha/replication_spec.go new file mode 100644 index 000000000..54251e22b --- /dev/null +++ b/pkg/apis/replication/v1alpha/replication_spec.go @@ -0,0 +1,57 @@ +// +// DISCLAIMER +// +// Copyright 2018 ArangoDB GmbH, Cologne, Germany +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// +// Author Ewout Prangsma +// + +package v1alpha + +// DeploymentReplicationSpec contains the specification part of +// an ArangoDeploymentReplication. +type DeploymentReplicationSpec struct { +} + +// Validate the given spec, returning an error on validation +// problems or nil if all ok. +func (s DeploymentReplicationSpec) Validate() error { + /* if err := s.StorageClass.Validate(); err != nil { + return maskAny(err) + } + if len(s.LocalPath) == 0 { + return maskAny(errors.Wrapf(ValidationError, "localPath cannot be empty")) + } + for _, p := range s.LocalPath { + if len(p) == 0 { + return maskAny(errors.Wrapf(ValidationError, "localPath cannot contain empty strings")) + } + }*/ + return nil +} + +// SetDefaults fills empty field with default values. +func (s *DeploymentReplicationSpec) SetDefaults() { +} + +// ResetImmutableFields replaces all immutable fields in the given target with values from the source spec. +// It returns a list of fields that have been reset. +// Field names are relative to `spec.`. +func (s DeploymentReplicationSpec) ResetImmutableFields(target *DeploymentReplicationSpec) []string { + var result []string + return result +} diff --git a/pkg/apis/replication/v1alpha/replication_state.go b/pkg/apis/replication/v1alpha/replication_state.go new file mode 100644 index 000000000..c34b113ba --- /dev/null +++ b/pkg/apis/replication/v1alpha/replication_state.go @@ -0,0 +1,32 @@ +// +// DISCLAIMER +// +// Copyright 2018 ArangoDB GmbH, Cologne, Germany +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// +// Author Ewout Prangsma +// + +package v1alpha + +// DeploymentReplicationStatus contains the status part of +// an ArangoDeploymentReplication. +type DeploymentReplicationStatus struct { + // Phase holds the current lifetime phase of the deployment replication + Phase DeploymentReplicationPhase `json:"phase"` + // Reason contains a human readable reason for reaching the current phase (can be empty) + Reason string `json:"reason,omitempty"` // Reason for current phase +} diff --git a/pkg/apis/replication/v1alpha/zz_generated.deepcopy.go b/pkg/apis/replication/v1alpha/zz_generated.deepcopy.go new file mode 100644 index 000000000..8919cdbe7 --- /dev/null +++ b/pkg/apis/replication/v1alpha/zz_generated.deepcopy.go @@ -0,0 +1,122 @@ +// +build !ignore_autogenerated + +// +// DISCLAIMER +// +// Copyright 2018 ArangoDB GmbH, Cologne, Germany +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// + +// This file was autogenerated by deepcopy-gen. Do not edit it manually! + +package v1alpha + +import ( + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ArangoDeploymentReplication) DeepCopyInto(out *ArangoDeploymentReplication) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + out.Status = in.Status + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ArangoDeploymentReplication. +func (in *ArangoDeploymentReplication) DeepCopy() *ArangoDeploymentReplication { + if in == nil { + return nil + } + out := new(ArangoDeploymentReplication) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ArangoDeploymentReplication) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ArangoDeploymentReplicationList) DeepCopyInto(out *ArangoDeploymentReplicationList) { + *out = *in + out.TypeMeta = in.TypeMeta + out.ListMeta = in.ListMeta + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]ArangoDeploymentReplication, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ArangoDeploymentReplicationList. +func (in *ArangoDeploymentReplicationList) DeepCopy() *ArangoDeploymentReplicationList { + if in == nil { + return nil + } + out := new(ArangoDeploymentReplicationList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ArangoDeploymentReplicationList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DeploymentReplicationSpec) DeepCopyInto(out *DeploymentReplicationSpec) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeploymentReplicationSpec. +func (in *DeploymentReplicationSpec) DeepCopy() *DeploymentReplicationSpec { + if in == nil { + return nil + } + out := new(DeploymentReplicationSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DeploymentReplicationStatus) DeepCopyInto(out *DeploymentReplicationStatus) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeploymentReplicationStatus. +func (in *DeploymentReplicationStatus) DeepCopy() *DeploymentReplicationStatus { + if in == nil { + return nil + } + out := new(DeploymentReplicationStatus) + in.DeepCopyInto(out) + return out +} diff --git a/pkg/generated/clientset/versioned/clientset.go b/pkg/generated/clientset/versioned/clientset.go index 6d76c9bde..44936e362 100644 --- a/pkg/generated/clientset/versioned/clientset.go +++ b/pkg/generated/clientset/versioned/clientset.go @@ -21,6 +21,7 @@ package versioned import ( databasev1alpha "github.com/arangodb/kube-arangodb/pkg/generated/clientset/versioned/typed/deployment/v1alpha" + replicationv1alpha "github.com/arangodb/kube-arangodb/pkg/generated/clientset/versioned/typed/replication/v1alpha" storagev1alpha "github.com/arangodb/kube-arangodb/pkg/generated/clientset/versioned/typed/storage/v1alpha" glog "github.com/golang/glog" discovery "k8s.io/client-go/discovery" @@ -33,6 +34,9 @@ type Interface interface { DatabaseV1alpha() databasev1alpha.DatabaseV1alphaInterface // Deprecated: please explicitly pick a version if possible. Database() databasev1alpha.DatabaseV1alphaInterface + ReplicationV1alpha() replicationv1alpha.ReplicationV1alphaInterface + // Deprecated: please explicitly pick a version if possible. + Replication() replicationv1alpha.ReplicationV1alphaInterface StorageV1alpha() storagev1alpha.StorageV1alphaInterface // Deprecated: please explicitly pick a version if possible. Storage() storagev1alpha.StorageV1alphaInterface @@ -42,8 +46,9 @@ type Interface interface { // version included in a Clientset. type Clientset struct { *discovery.DiscoveryClient - databaseV1alpha *databasev1alpha.DatabaseV1alphaClient - storageV1alpha *storagev1alpha.StorageV1alphaClient + databaseV1alpha *databasev1alpha.DatabaseV1alphaClient + replicationV1alpha *replicationv1alpha.ReplicationV1alphaClient + storageV1alpha *storagev1alpha.StorageV1alphaClient } // DatabaseV1alpha retrieves the DatabaseV1alphaClient @@ -57,6 +62,17 @@ func (c *Clientset) Database() databasev1alpha.DatabaseV1alphaInterface { return c.databaseV1alpha } +// ReplicationV1alpha retrieves the ReplicationV1alphaClient +func (c *Clientset) ReplicationV1alpha() replicationv1alpha.ReplicationV1alphaInterface { + return c.replicationV1alpha +} + +// Deprecated: Replication retrieves the default version of ReplicationClient. +// Please explicitly pick a version. +func (c *Clientset) Replication() replicationv1alpha.ReplicationV1alphaInterface { + return c.replicationV1alpha +} + // StorageV1alpha retrieves the StorageV1alphaClient func (c *Clientset) StorageV1alpha() storagev1alpha.StorageV1alphaInterface { return c.storageV1alpha @@ -88,6 +104,10 @@ func NewForConfig(c *rest.Config) (*Clientset, error) { if err != nil { return nil, err } + cs.replicationV1alpha, err = replicationv1alpha.NewForConfig(&configShallowCopy) + if err != nil { + return nil, err + } cs.storageV1alpha, err = storagev1alpha.NewForConfig(&configShallowCopy) if err != nil { return nil, err @@ -106,6 +126,7 @@ func NewForConfig(c *rest.Config) (*Clientset, error) { func NewForConfigOrDie(c *rest.Config) *Clientset { var cs Clientset cs.databaseV1alpha = databasev1alpha.NewForConfigOrDie(c) + cs.replicationV1alpha = replicationv1alpha.NewForConfigOrDie(c) cs.storageV1alpha = storagev1alpha.NewForConfigOrDie(c) cs.DiscoveryClient = discovery.NewDiscoveryClientForConfigOrDie(c) @@ -116,6 +137,7 @@ func NewForConfigOrDie(c *rest.Config) *Clientset { func New(c rest.Interface) *Clientset { var cs Clientset cs.databaseV1alpha = databasev1alpha.New(c) + cs.replicationV1alpha = replicationv1alpha.New(c) cs.storageV1alpha = storagev1alpha.New(c) cs.DiscoveryClient = discovery.NewDiscoveryClient(c) diff --git a/pkg/generated/clientset/versioned/fake/clientset_generated.go b/pkg/generated/clientset/versioned/fake/clientset_generated.go index 1ca192f22..1e3cefd46 100644 --- a/pkg/generated/clientset/versioned/fake/clientset_generated.go +++ b/pkg/generated/clientset/versioned/fake/clientset_generated.go @@ -23,6 +23,8 @@ import ( clientset "github.com/arangodb/kube-arangodb/pkg/generated/clientset/versioned" databasev1alpha "github.com/arangodb/kube-arangodb/pkg/generated/clientset/versioned/typed/deployment/v1alpha" fakedatabasev1alpha "github.com/arangodb/kube-arangodb/pkg/generated/clientset/versioned/typed/deployment/v1alpha/fake" + replicationv1alpha "github.com/arangodb/kube-arangodb/pkg/generated/clientset/versioned/typed/replication/v1alpha" + fakereplicationv1alpha "github.com/arangodb/kube-arangodb/pkg/generated/clientset/versioned/typed/replication/v1alpha/fake" storagev1alpha "github.com/arangodb/kube-arangodb/pkg/generated/clientset/versioned/typed/storage/v1alpha" fakestoragev1alpha "github.com/arangodb/kube-arangodb/pkg/generated/clientset/versioned/typed/storage/v1alpha/fake" "k8s.io/apimachinery/pkg/runtime" @@ -83,6 +85,16 @@ func (c *Clientset) Database() databasev1alpha.DatabaseV1alphaInterface { return &fakedatabasev1alpha.FakeDatabaseV1alpha{Fake: &c.Fake} } +// ReplicationV1alpha retrieves the ReplicationV1alphaClient +func (c *Clientset) ReplicationV1alpha() replicationv1alpha.ReplicationV1alphaInterface { + return &fakereplicationv1alpha.FakeReplicationV1alpha{Fake: &c.Fake} +} + +// Replication retrieves the ReplicationV1alphaClient +func (c *Clientset) Replication() replicationv1alpha.ReplicationV1alphaInterface { + return &fakereplicationv1alpha.FakeReplicationV1alpha{Fake: &c.Fake} +} + // StorageV1alpha retrieves the StorageV1alphaClient func (c *Clientset) StorageV1alpha() storagev1alpha.StorageV1alphaInterface { return &fakestoragev1alpha.FakeStorageV1alpha{Fake: &c.Fake} diff --git a/pkg/generated/clientset/versioned/fake/register.go b/pkg/generated/clientset/versioned/fake/register.go index 33cdee33e..70c37b32b 100644 --- a/pkg/generated/clientset/versioned/fake/register.go +++ b/pkg/generated/clientset/versioned/fake/register.go @@ -21,6 +21,7 @@ package fake import ( databasev1alpha "github.com/arangodb/kube-arangodb/pkg/apis/deployment/v1alpha" + replicationv1alpha "github.com/arangodb/kube-arangodb/pkg/apis/replication/v1alpha" storagev1alpha "github.com/arangodb/kube-arangodb/pkg/apis/storage/v1alpha" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" @@ -53,5 +54,6 @@ func init() { // correctly. func AddToScheme(scheme *runtime.Scheme) { databasev1alpha.AddToScheme(scheme) + replicationv1alpha.AddToScheme(scheme) storagev1alpha.AddToScheme(scheme) } diff --git a/pkg/generated/clientset/versioned/scheme/register.go b/pkg/generated/clientset/versioned/scheme/register.go index 63ca2d1cd..de3b9f360 100644 --- a/pkg/generated/clientset/versioned/scheme/register.go +++ b/pkg/generated/clientset/versioned/scheme/register.go @@ -21,6 +21,7 @@ package scheme import ( databasev1alpha "github.com/arangodb/kube-arangodb/pkg/apis/deployment/v1alpha" + replicationv1alpha "github.com/arangodb/kube-arangodb/pkg/apis/replication/v1alpha" storagev1alpha "github.com/arangodb/kube-arangodb/pkg/apis/storage/v1alpha" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" @@ -53,5 +54,6 @@ func init() { // correctly. func AddToScheme(scheme *runtime.Scheme) { databasev1alpha.AddToScheme(scheme) + replicationv1alpha.AddToScheme(scheme) storagev1alpha.AddToScheme(scheme) } diff --git a/pkg/generated/clientset/versioned/typed/replication/v1alpha/arangodeploymentreplication.go b/pkg/generated/clientset/versioned/typed/replication/v1alpha/arangodeploymentreplication.go new file mode 100644 index 000000000..b548a7665 --- /dev/null +++ b/pkg/generated/clientset/versioned/typed/replication/v1alpha/arangodeploymentreplication.go @@ -0,0 +1,175 @@ +// +// DISCLAIMER +// +// Copyright 2018 ArangoDB GmbH, Cologne, Germany +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// +package v1alpha + +import ( + v1alpha "github.com/arangodb/kube-arangodb/pkg/apis/replication/v1alpha" + scheme "github.com/arangodb/kube-arangodb/pkg/generated/clientset/versioned/scheme" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + rest "k8s.io/client-go/rest" +) + +// ArangoDeploymentReplicationsGetter has a method to return a ArangoDeploymentReplicationInterface. +// A group's client should implement this interface. +type ArangoDeploymentReplicationsGetter interface { + ArangoDeploymentReplications(namespace string) ArangoDeploymentReplicationInterface +} + +// ArangoDeploymentReplicationInterface has methods to work with ArangoDeploymentReplication resources. +type ArangoDeploymentReplicationInterface interface { + Create(*v1alpha.ArangoDeploymentReplication) (*v1alpha.ArangoDeploymentReplication, error) + Update(*v1alpha.ArangoDeploymentReplication) (*v1alpha.ArangoDeploymentReplication, error) + UpdateStatus(*v1alpha.ArangoDeploymentReplication) (*v1alpha.ArangoDeploymentReplication, error) + Delete(name string, options *v1.DeleteOptions) error + DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error + Get(name string, options v1.GetOptions) (*v1alpha.ArangoDeploymentReplication, error) + List(opts v1.ListOptions) (*v1alpha.ArangoDeploymentReplicationList, error) + Watch(opts v1.ListOptions) (watch.Interface, error) + Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1alpha.ArangoDeploymentReplication, err error) + ArangoDeploymentReplicationExpansion +} + +// arangoDeploymentReplications implements ArangoDeploymentReplicationInterface +type arangoDeploymentReplications struct { + client rest.Interface + ns string +} + +// newArangoDeploymentReplications returns a ArangoDeploymentReplications +func newArangoDeploymentReplications(c *ReplicationV1alphaClient, namespace string) *arangoDeploymentReplications { + return &arangoDeploymentReplications{ + client: c.RESTClient(), + ns: namespace, + } +} + +// Get takes name of the arangoDeploymentReplication, and returns the corresponding arangoDeploymentReplication object, and an error if there is any. +func (c *arangoDeploymentReplications) Get(name string, options v1.GetOptions) (result *v1alpha.ArangoDeploymentReplication, err error) { + result = &v1alpha.ArangoDeploymentReplication{} + err = c.client.Get(). + Namespace(c.ns). + Resource("arangodeploymentreplications"). + Name(name). + VersionedParams(&options, scheme.ParameterCodec). + Do(). + Into(result) + return +} + +// List takes label and field selectors, and returns the list of ArangoDeploymentReplications that match those selectors. +func (c *arangoDeploymentReplications) List(opts v1.ListOptions) (result *v1alpha.ArangoDeploymentReplicationList, err error) { + result = &v1alpha.ArangoDeploymentReplicationList{} + err = c.client.Get(). + Namespace(c.ns). + Resource("arangodeploymentreplications"). + VersionedParams(&opts, scheme.ParameterCodec). + Do(). + Into(result) + return +} + +// Watch returns a watch.Interface that watches the requested arangoDeploymentReplications. +func (c *arangoDeploymentReplications) Watch(opts v1.ListOptions) (watch.Interface, error) { + opts.Watch = true + return c.client.Get(). + Namespace(c.ns). + Resource("arangodeploymentreplications"). + VersionedParams(&opts, scheme.ParameterCodec). + Watch() +} + +// Create takes the representation of a arangoDeploymentReplication and creates it. Returns the server's representation of the arangoDeploymentReplication, and an error, if there is any. +func (c *arangoDeploymentReplications) Create(arangoDeploymentReplication *v1alpha.ArangoDeploymentReplication) (result *v1alpha.ArangoDeploymentReplication, err error) { + result = &v1alpha.ArangoDeploymentReplication{} + err = c.client.Post(). + Namespace(c.ns). + Resource("arangodeploymentreplications"). + Body(arangoDeploymentReplication). + Do(). + Into(result) + return +} + +// Update takes the representation of a arangoDeploymentReplication and updates it. Returns the server's representation of the arangoDeploymentReplication, and an error, if there is any. +func (c *arangoDeploymentReplications) Update(arangoDeploymentReplication *v1alpha.ArangoDeploymentReplication) (result *v1alpha.ArangoDeploymentReplication, err error) { + result = &v1alpha.ArangoDeploymentReplication{} + err = c.client.Put(). + Namespace(c.ns). + Resource("arangodeploymentreplications"). + Name(arangoDeploymentReplication.Name). + Body(arangoDeploymentReplication). + Do(). + Into(result) + return +} + +// UpdateStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). + +func (c *arangoDeploymentReplications) UpdateStatus(arangoDeploymentReplication *v1alpha.ArangoDeploymentReplication) (result *v1alpha.ArangoDeploymentReplication, err error) { + result = &v1alpha.ArangoDeploymentReplication{} + err = c.client.Put(). + Namespace(c.ns). + Resource("arangodeploymentreplications"). + Name(arangoDeploymentReplication.Name). + SubResource("status"). + Body(arangoDeploymentReplication). + Do(). + Into(result) + return +} + +// Delete takes name of the arangoDeploymentReplication and deletes it. Returns an error if one occurs. +func (c *arangoDeploymentReplications) Delete(name string, options *v1.DeleteOptions) error { + return c.client.Delete(). + Namespace(c.ns). + Resource("arangodeploymentreplications"). + Name(name). + Body(options). + Do(). + Error() +} + +// DeleteCollection deletes a collection of objects. +func (c *arangoDeploymentReplications) DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error { + return c.client.Delete(). + Namespace(c.ns). + Resource("arangodeploymentreplications"). + VersionedParams(&listOptions, scheme.ParameterCodec). + Body(options). + Do(). + Error() +} + +// Patch applies the patch and returns the patched arangoDeploymentReplication. +func (c *arangoDeploymentReplications) Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1alpha.ArangoDeploymentReplication, err error) { + result = &v1alpha.ArangoDeploymentReplication{} + err = c.client.Patch(pt). + Namespace(c.ns). + Resource("arangodeploymentreplications"). + SubResource(subresources...). + Name(name). + Body(data). + Do(). + Into(result) + return +} diff --git a/pkg/generated/clientset/versioned/typed/replication/v1alpha/doc.go b/pkg/generated/clientset/versioned/typed/replication/v1alpha/doc.go new file mode 100644 index 000000000..f48feba5c --- /dev/null +++ b/pkg/generated/clientset/versioned/typed/replication/v1alpha/doc.go @@ -0,0 +1,21 @@ +// +// DISCLAIMER +// +// Copyright 2018 ArangoDB GmbH, Cologne, Germany +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// +// This package has the automatically generated typed clients. +package v1alpha diff --git a/pkg/generated/clientset/versioned/typed/replication/v1alpha/fake/doc.go b/pkg/generated/clientset/versioned/typed/replication/v1alpha/fake/doc.go new file mode 100644 index 000000000..6055f5176 --- /dev/null +++ b/pkg/generated/clientset/versioned/typed/replication/v1alpha/fake/doc.go @@ -0,0 +1,21 @@ +// +// DISCLAIMER +// +// Copyright 2018 ArangoDB GmbH, Cologne, Germany +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// +// Package fake has the automatically generated clients. +package fake diff --git a/pkg/generated/clientset/versioned/typed/replication/v1alpha/fake/fake_arangodeploymentreplication.go b/pkg/generated/clientset/versioned/typed/replication/v1alpha/fake/fake_arangodeploymentreplication.go new file mode 100644 index 000000000..96c937aed --- /dev/null +++ b/pkg/generated/clientset/versioned/typed/replication/v1alpha/fake/fake_arangodeploymentreplication.go @@ -0,0 +1,141 @@ +// +// DISCLAIMER +// +// Copyright 2018 ArangoDB GmbH, Cologne, Germany +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// +package fake + +import ( + v1alpha "github.com/arangodb/kube-arangodb/pkg/apis/replication/v1alpha" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + labels "k8s.io/apimachinery/pkg/labels" + schema "k8s.io/apimachinery/pkg/runtime/schema" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + testing "k8s.io/client-go/testing" +) + +// FakeArangoDeploymentReplications implements ArangoDeploymentReplicationInterface +type FakeArangoDeploymentReplications struct { + Fake *FakeReplicationV1alpha + ns string +} + +var arangodeploymentreplicationsResource = schema.GroupVersionResource{Group: "replication.database.arangodb.com", Version: "v1alpha", Resource: "arangodeploymentreplications"} + +var arangodeploymentreplicationsKind = schema.GroupVersionKind{Group: "replication.database.arangodb.com", Version: "v1alpha", Kind: "ArangoDeploymentReplication"} + +// Get takes name of the arangoDeploymentReplication, and returns the corresponding arangoDeploymentReplication object, and an error if there is any. +func (c *FakeArangoDeploymentReplications) Get(name string, options v1.GetOptions) (result *v1alpha.ArangoDeploymentReplication, err error) { + obj, err := c.Fake. + Invokes(testing.NewGetAction(arangodeploymentreplicationsResource, c.ns, name), &v1alpha.ArangoDeploymentReplication{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha.ArangoDeploymentReplication), err +} + +// List takes label and field selectors, and returns the list of ArangoDeploymentReplications that match those selectors. +func (c *FakeArangoDeploymentReplications) List(opts v1.ListOptions) (result *v1alpha.ArangoDeploymentReplicationList, err error) { + obj, err := c.Fake. + Invokes(testing.NewListAction(arangodeploymentreplicationsResource, arangodeploymentreplicationsKind, c.ns, opts), &v1alpha.ArangoDeploymentReplicationList{}) + + if obj == nil { + return nil, err + } + + label, _, _ := testing.ExtractFromListOptions(opts) + if label == nil { + label = labels.Everything() + } + list := &v1alpha.ArangoDeploymentReplicationList{} + for _, item := range obj.(*v1alpha.ArangoDeploymentReplicationList).Items { + if label.Matches(labels.Set(item.Labels)) { + list.Items = append(list.Items, item) + } + } + return list, err +} + +// Watch returns a watch.Interface that watches the requested arangoDeploymentReplications. +func (c *FakeArangoDeploymentReplications) Watch(opts v1.ListOptions) (watch.Interface, error) { + return c.Fake. + InvokesWatch(testing.NewWatchAction(arangodeploymentreplicationsResource, c.ns, opts)) + +} + +// Create takes the representation of a arangoDeploymentReplication and creates it. Returns the server's representation of the arangoDeploymentReplication, and an error, if there is any. +func (c *FakeArangoDeploymentReplications) Create(arangoDeploymentReplication *v1alpha.ArangoDeploymentReplication) (result *v1alpha.ArangoDeploymentReplication, err error) { + obj, err := c.Fake. + Invokes(testing.NewCreateAction(arangodeploymentreplicationsResource, c.ns, arangoDeploymentReplication), &v1alpha.ArangoDeploymentReplication{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha.ArangoDeploymentReplication), err +} + +// Update takes the representation of a arangoDeploymentReplication and updates it. Returns the server's representation of the arangoDeploymentReplication, and an error, if there is any. +func (c *FakeArangoDeploymentReplications) Update(arangoDeploymentReplication *v1alpha.ArangoDeploymentReplication) (result *v1alpha.ArangoDeploymentReplication, err error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateAction(arangodeploymentreplicationsResource, c.ns, arangoDeploymentReplication), &v1alpha.ArangoDeploymentReplication{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha.ArangoDeploymentReplication), err +} + +// UpdateStatus was generated because the type contains a Status member. +// Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). +func (c *FakeArangoDeploymentReplications) UpdateStatus(arangoDeploymentReplication *v1alpha.ArangoDeploymentReplication) (*v1alpha.ArangoDeploymentReplication, error) { + obj, err := c.Fake. + Invokes(testing.NewUpdateSubresourceAction(arangodeploymentreplicationsResource, "status", c.ns, arangoDeploymentReplication), &v1alpha.ArangoDeploymentReplication{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha.ArangoDeploymentReplication), err +} + +// Delete takes name of the arangoDeploymentReplication and deletes it. Returns an error if one occurs. +func (c *FakeArangoDeploymentReplications) Delete(name string, options *v1.DeleteOptions) error { + _, err := c.Fake. + Invokes(testing.NewDeleteAction(arangodeploymentreplicationsResource, c.ns, name), &v1alpha.ArangoDeploymentReplication{}) + + return err +} + +// DeleteCollection deletes a collection of objects. +func (c *FakeArangoDeploymentReplications) DeleteCollection(options *v1.DeleteOptions, listOptions v1.ListOptions) error { + action := testing.NewDeleteCollectionAction(arangodeploymentreplicationsResource, c.ns, listOptions) + + _, err := c.Fake.Invokes(action, &v1alpha.ArangoDeploymentReplicationList{}) + return err +} + +// Patch applies the patch and returns the patched arangoDeploymentReplication. +func (c *FakeArangoDeploymentReplications) Patch(name string, pt types.PatchType, data []byte, subresources ...string) (result *v1alpha.ArangoDeploymentReplication, err error) { + obj, err := c.Fake. + Invokes(testing.NewPatchSubresourceAction(arangodeploymentreplicationsResource, c.ns, name, data, subresources...), &v1alpha.ArangoDeploymentReplication{}) + + if obj == nil { + return nil, err + } + return obj.(*v1alpha.ArangoDeploymentReplication), err +} diff --git a/pkg/generated/clientset/versioned/typed/replication/v1alpha/fake/fake_replication_client.go b/pkg/generated/clientset/versioned/typed/replication/v1alpha/fake/fake_replication_client.go new file mode 100644 index 000000000..bea672c63 --- /dev/null +++ b/pkg/generated/clientset/versioned/typed/replication/v1alpha/fake/fake_replication_client.go @@ -0,0 +1,41 @@ +// +// DISCLAIMER +// +// Copyright 2018 ArangoDB GmbH, Cologne, Germany +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// +package fake + +import ( + v1alpha "github.com/arangodb/kube-arangodb/pkg/generated/clientset/versioned/typed/replication/v1alpha" + rest "k8s.io/client-go/rest" + testing "k8s.io/client-go/testing" +) + +type FakeReplicationV1alpha struct { + *testing.Fake +} + +func (c *FakeReplicationV1alpha) ArangoDeploymentReplications(namespace string) v1alpha.ArangoDeploymentReplicationInterface { + return &FakeArangoDeploymentReplications{c, namespace} +} + +// RESTClient returns a RESTClient that is used to communicate +// with API server by this client implementation. +func (c *FakeReplicationV1alpha) RESTClient() rest.Interface { + var ret *rest.RESTClient + return ret +} diff --git a/pkg/generated/clientset/versioned/typed/replication/v1alpha/generated_expansion.go b/pkg/generated/clientset/versioned/typed/replication/v1alpha/generated_expansion.go new file mode 100644 index 000000000..c102dfab5 --- /dev/null +++ b/pkg/generated/clientset/versioned/typed/replication/v1alpha/generated_expansion.go @@ -0,0 +1,22 @@ +// +// DISCLAIMER +// +// Copyright 2018 ArangoDB GmbH, Cologne, Germany +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// +package v1alpha + +type ArangoDeploymentReplicationExpansion interface{} diff --git a/pkg/generated/clientset/versioned/typed/replication/v1alpha/replication_client.go b/pkg/generated/clientset/versioned/typed/replication/v1alpha/replication_client.go new file mode 100644 index 000000000..a2ae5c1c2 --- /dev/null +++ b/pkg/generated/clientset/versioned/typed/replication/v1alpha/replication_client.go @@ -0,0 +1,91 @@ +// +// DISCLAIMER +// +// Copyright 2018 ArangoDB GmbH, Cologne, Germany +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// +package v1alpha + +import ( + v1alpha "github.com/arangodb/kube-arangodb/pkg/apis/replication/v1alpha" + "github.com/arangodb/kube-arangodb/pkg/generated/clientset/versioned/scheme" + serializer "k8s.io/apimachinery/pkg/runtime/serializer" + rest "k8s.io/client-go/rest" +) + +type ReplicationV1alphaInterface interface { + RESTClient() rest.Interface + ArangoDeploymentReplicationsGetter +} + +// ReplicationV1alphaClient is used to interact with features provided by the replication.database.arangodb.com group. +type ReplicationV1alphaClient struct { + restClient rest.Interface +} + +func (c *ReplicationV1alphaClient) ArangoDeploymentReplications(namespace string) ArangoDeploymentReplicationInterface { + return newArangoDeploymentReplications(c, namespace) +} + +// NewForConfig creates a new ReplicationV1alphaClient for the given config. +func NewForConfig(c *rest.Config) (*ReplicationV1alphaClient, error) { + config := *c + if err := setConfigDefaults(&config); err != nil { + return nil, err + } + client, err := rest.RESTClientFor(&config) + if err != nil { + return nil, err + } + return &ReplicationV1alphaClient{client}, nil +} + +// NewForConfigOrDie creates a new ReplicationV1alphaClient for the given config and +// panics if there is an error in the config. +func NewForConfigOrDie(c *rest.Config) *ReplicationV1alphaClient { + client, err := NewForConfig(c) + if err != nil { + panic(err) + } + return client +} + +// New creates a new ReplicationV1alphaClient for the given RESTClient. +func New(c rest.Interface) *ReplicationV1alphaClient { + return &ReplicationV1alphaClient{c} +} + +func setConfigDefaults(config *rest.Config) error { + gv := v1alpha.SchemeGroupVersion + config.GroupVersion = &gv + config.APIPath = "/apis" + config.NegotiatedSerializer = serializer.DirectCodecFactory{CodecFactory: scheme.Codecs} + + if config.UserAgent == "" { + config.UserAgent = rest.DefaultKubernetesUserAgent() + } + + return nil +} + +// RESTClient returns a RESTClient that is used to communicate +// with API server by this client implementation. +func (c *ReplicationV1alphaClient) RESTClient() rest.Interface { + if c == nil { + return nil + } + return c.restClient +} diff --git a/pkg/generated/informers/externalversions/factory.go b/pkg/generated/informers/externalversions/factory.go index bb541551e..c88fc2a3f 100644 --- a/pkg/generated/informers/externalversions/factory.go +++ b/pkg/generated/informers/externalversions/factory.go @@ -30,6 +30,7 @@ import ( versioned "github.com/arangodb/kube-arangodb/pkg/generated/clientset/versioned" deployment "github.com/arangodb/kube-arangodb/pkg/generated/informers/externalversions/deployment" internalinterfaces "github.com/arangodb/kube-arangodb/pkg/generated/informers/externalversions/internalinterfaces" + replication "github.com/arangodb/kube-arangodb/pkg/generated/informers/externalversions/replication" storage "github.com/arangodb/kube-arangodb/pkg/generated/informers/externalversions/storage" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" @@ -129,6 +130,7 @@ type SharedInformerFactory interface { WaitForCacheSync(stopCh <-chan struct{}) map[reflect.Type]bool Database() deployment.Interface + Replication() replication.Interface Storage() storage.Interface } @@ -136,6 +138,10 @@ func (f *sharedInformerFactory) Database() deployment.Interface { return deployment.New(f, f.namespace, f.tweakListOptions) } +func (f *sharedInformerFactory) Replication() replication.Interface { + return replication.New(f, f.namespace, f.tweakListOptions) +} + func (f *sharedInformerFactory) Storage() storage.Interface { return storage.New(f, f.namespace, f.tweakListOptions) } diff --git a/pkg/generated/informers/externalversions/generic.go b/pkg/generated/informers/externalversions/generic.go index e076d52f0..b169aa0ef 100644 --- a/pkg/generated/informers/externalversions/generic.go +++ b/pkg/generated/informers/externalversions/generic.go @@ -26,6 +26,7 @@ import ( "fmt" v1alpha "github.com/arangodb/kube-arangodb/pkg/apis/deployment/v1alpha" + replication_v1alpha "github.com/arangodb/kube-arangodb/pkg/apis/replication/v1alpha" storage_v1alpha "github.com/arangodb/kube-arangodb/pkg/apis/storage/v1alpha" schema "k8s.io/apimachinery/pkg/runtime/schema" cache "k8s.io/client-go/tools/cache" @@ -61,6 +62,10 @@ func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource case v1alpha.SchemeGroupVersion.WithResource("arangodeployments"): return &genericInformer{resource: resource.GroupResource(), informer: f.Database().V1alpha().ArangoDeployments().Informer()}, nil + // Group=replication.database.arangodb.com, Version=v1alpha + case replication_v1alpha.SchemeGroupVersion.WithResource("arangodeploymentreplications"): + return &genericInformer{resource: resource.GroupResource(), informer: f.Replication().V1alpha().ArangoDeploymentReplications().Informer()}, nil + // Group=storage.arangodb.com, Version=v1alpha case storage_v1alpha.SchemeGroupVersion.WithResource("arangolocalstorages"): return &genericInformer{resource: resource.GroupResource(), informer: f.Storage().V1alpha().ArangoLocalStorages().Informer()}, nil diff --git a/pkg/generated/informers/externalversions/replication/interface.go b/pkg/generated/informers/externalversions/replication/interface.go new file mode 100644 index 000000000..6c7a3bcfb --- /dev/null +++ b/pkg/generated/informers/externalversions/replication/interface.go @@ -0,0 +1,50 @@ +// +// DISCLAIMER +// +// Copyright 2018 ArangoDB GmbH, Cologne, Germany +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// + +// This file was automatically generated by informer-gen + +package replication + +import ( + internalinterfaces "github.com/arangodb/kube-arangodb/pkg/generated/informers/externalversions/internalinterfaces" + v1alpha "github.com/arangodb/kube-arangodb/pkg/generated/informers/externalversions/replication/v1alpha" +) + +// Interface provides access to each of this group's versions. +type Interface interface { + // V1alpha provides access to shared informers for resources in V1alpha. + V1alpha() v1alpha.Interface +} + +type group struct { + factory internalinterfaces.SharedInformerFactory + namespace string + tweakListOptions internalinterfaces.TweakListOptionsFunc +} + +// New returns a new Interface. +func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface { + return &group{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} +} + +// V1alpha returns a new v1alpha.Interface. +func (g *group) V1alpha() v1alpha.Interface { + return v1alpha.New(g.factory, g.namespace, g.tweakListOptions) +} diff --git a/pkg/generated/informers/externalversions/replication/v1alpha/arangodeploymentreplication.go b/pkg/generated/informers/externalversions/replication/v1alpha/arangodeploymentreplication.go new file mode 100644 index 000000000..2c45a1651 --- /dev/null +++ b/pkg/generated/informers/externalversions/replication/v1alpha/arangodeploymentreplication.go @@ -0,0 +1,93 @@ +// +// DISCLAIMER +// +// Copyright 2018 ArangoDB GmbH, Cologne, Germany +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// + +// This file was automatically generated by informer-gen + +package v1alpha + +import ( + time "time" + + replication_v1alpha "github.com/arangodb/kube-arangodb/pkg/apis/replication/v1alpha" + versioned "github.com/arangodb/kube-arangodb/pkg/generated/clientset/versioned" + internalinterfaces "github.com/arangodb/kube-arangodb/pkg/generated/informers/externalversions/internalinterfaces" + v1alpha "github.com/arangodb/kube-arangodb/pkg/generated/listers/replication/v1alpha" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + watch "k8s.io/apimachinery/pkg/watch" + cache "k8s.io/client-go/tools/cache" +) + +// ArangoDeploymentReplicationInformer provides access to a shared informer and lister for +// ArangoDeploymentReplications. +type ArangoDeploymentReplicationInformer interface { + Informer() cache.SharedIndexInformer + Lister() v1alpha.ArangoDeploymentReplicationLister +} + +type arangoDeploymentReplicationInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc + namespace string +} + +// NewArangoDeploymentReplicationInformer constructs a new informer for ArangoDeploymentReplication type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewArangoDeploymentReplicationInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredArangoDeploymentReplicationInformer(client, namespace, resyncPeriod, indexers, nil) +} + +// NewFilteredArangoDeploymentReplicationInformer constructs a new informer for ArangoDeploymentReplication type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewFilteredArangoDeploymentReplicationInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { + return cache.NewSharedIndexInformer( + &cache.ListWatch{ + ListFunc: func(options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.ReplicationV1alpha().ArangoDeploymentReplications(namespace).List(options) + }, + WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.ReplicationV1alpha().ArangoDeploymentReplications(namespace).Watch(options) + }, + }, + &replication_v1alpha.ArangoDeploymentReplication{}, + resyncPeriod, + indexers, + ) +} + +func (f *arangoDeploymentReplicationInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredArangoDeploymentReplicationInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +} + +func (f *arangoDeploymentReplicationInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&replication_v1alpha.ArangoDeploymentReplication{}, f.defaultInformer) +} + +func (f *arangoDeploymentReplicationInformer) Lister() v1alpha.ArangoDeploymentReplicationLister { + return v1alpha.NewArangoDeploymentReplicationLister(f.Informer().GetIndexer()) +} diff --git a/pkg/generated/informers/externalversions/replication/v1alpha/interface.go b/pkg/generated/informers/externalversions/replication/v1alpha/interface.go new file mode 100644 index 000000000..c5f378749 --- /dev/null +++ b/pkg/generated/informers/externalversions/replication/v1alpha/interface.go @@ -0,0 +1,49 @@ +// +// DISCLAIMER +// +// Copyright 2018 ArangoDB GmbH, Cologne, Germany +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// + +// This file was automatically generated by informer-gen + +package v1alpha + +import ( + internalinterfaces "github.com/arangodb/kube-arangodb/pkg/generated/informers/externalversions/internalinterfaces" +) + +// Interface provides access to all the informers in this group version. +type Interface interface { + // ArangoDeploymentReplications returns a ArangoDeploymentReplicationInformer. + ArangoDeploymentReplications() ArangoDeploymentReplicationInformer +} + +type version struct { + factory internalinterfaces.SharedInformerFactory + namespace string + tweakListOptions internalinterfaces.TweakListOptionsFunc +} + +// New returns a new Interface. +func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface { + return &version{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} +} + +// ArangoDeploymentReplications returns a ArangoDeploymentReplicationInformer. +func (v *version) ArangoDeploymentReplications() ArangoDeploymentReplicationInformer { + return &arangoDeploymentReplicationInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} +} diff --git a/pkg/generated/listers/replication/v1alpha/arangodeploymentreplication.go b/pkg/generated/listers/replication/v1alpha/arangodeploymentreplication.go new file mode 100644 index 000000000..09bdea528 --- /dev/null +++ b/pkg/generated/listers/replication/v1alpha/arangodeploymentreplication.go @@ -0,0 +1,98 @@ +// +// DISCLAIMER +// +// Copyright 2018 ArangoDB GmbH, Cologne, Germany +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// + +// This file was automatically generated by lister-gen + +package v1alpha + +import ( + v1alpha "github.com/arangodb/kube-arangodb/pkg/apis/replication/v1alpha" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/client-go/tools/cache" +) + +// ArangoDeploymentReplicationLister helps list ArangoDeploymentReplications. +type ArangoDeploymentReplicationLister interface { + // List lists all ArangoDeploymentReplications in the indexer. + List(selector labels.Selector) (ret []*v1alpha.ArangoDeploymentReplication, err error) + // ArangoDeploymentReplications returns an object that can list and get ArangoDeploymentReplications. + ArangoDeploymentReplications(namespace string) ArangoDeploymentReplicationNamespaceLister + ArangoDeploymentReplicationListerExpansion +} + +// arangoDeploymentReplicationLister implements the ArangoDeploymentReplicationLister interface. +type arangoDeploymentReplicationLister struct { + indexer cache.Indexer +} + +// NewArangoDeploymentReplicationLister returns a new ArangoDeploymentReplicationLister. +func NewArangoDeploymentReplicationLister(indexer cache.Indexer) ArangoDeploymentReplicationLister { + return &arangoDeploymentReplicationLister{indexer: indexer} +} + +// List lists all ArangoDeploymentReplications in the indexer. +func (s *arangoDeploymentReplicationLister) List(selector labels.Selector) (ret []*v1alpha.ArangoDeploymentReplication, err error) { + err = cache.ListAll(s.indexer, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha.ArangoDeploymentReplication)) + }) + return ret, err +} + +// ArangoDeploymentReplications returns an object that can list and get ArangoDeploymentReplications. +func (s *arangoDeploymentReplicationLister) ArangoDeploymentReplications(namespace string) ArangoDeploymentReplicationNamespaceLister { + return arangoDeploymentReplicationNamespaceLister{indexer: s.indexer, namespace: namespace} +} + +// ArangoDeploymentReplicationNamespaceLister helps list and get ArangoDeploymentReplications. +type ArangoDeploymentReplicationNamespaceLister interface { + // List lists all ArangoDeploymentReplications in the indexer for a given namespace. + List(selector labels.Selector) (ret []*v1alpha.ArangoDeploymentReplication, err error) + // Get retrieves the ArangoDeploymentReplication from the indexer for a given namespace and name. + Get(name string) (*v1alpha.ArangoDeploymentReplication, error) + ArangoDeploymentReplicationNamespaceListerExpansion +} + +// arangoDeploymentReplicationNamespaceLister implements the ArangoDeploymentReplicationNamespaceLister +// interface. +type arangoDeploymentReplicationNamespaceLister struct { + indexer cache.Indexer + namespace string +} + +// List lists all ArangoDeploymentReplications in the indexer for a given namespace. +func (s arangoDeploymentReplicationNamespaceLister) List(selector labels.Selector) (ret []*v1alpha.ArangoDeploymentReplication, err error) { + err = cache.ListAllByNamespace(s.indexer, s.namespace, selector, func(m interface{}) { + ret = append(ret, m.(*v1alpha.ArangoDeploymentReplication)) + }) + return ret, err +} + +// Get retrieves the ArangoDeploymentReplication from the indexer for a given namespace and name. +func (s arangoDeploymentReplicationNamespaceLister) Get(name string) (*v1alpha.ArangoDeploymentReplication, error) { + obj, exists, err := s.indexer.GetByKey(s.namespace + "/" + name) + if err != nil { + return nil, err + } + if !exists { + return nil, errors.NewNotFound(v1alpha.Resource("arangodeploymentreplication"), name) + } + return obj.(*v1alpha.ArangoDeploymentReplication), nil +} diff --git a/pkg/generated/listers/replication/v1alpha/expansion_generated.go b/pkg/generated/listers/replication/v1alpha/expansion_generated.go new file mode 100644 index 000000000..51d8b72b8 --- /dev/null +++ b/pkg/generated/listers/replication/v1alpha/expansion_generated.go @@ -0,0 +1,31 @@ +// +// DISCLAIMER +// +// Copyright 2018 ArangoDB GmbH, Cologne, Germany +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// + +// This file was automatically generated by lister-gen + +package v1alpha + +// ArangoDeploymentReplicationListerExpansion allows custom methods to be added to +// ArangoDeploymentReplicationLister. +type ArangoDeploymentReplicationListerExpansion interface{} + +// ArangoDeploymentReplicationNamespaceListerExpansion allows custom methods to be added to +// ArangoDeploymentReplicationNamespaceLister. +type ArangoDeploymentReplicationNamespaceListerExpansion interface{} diff --git a/pkg/operator/crd.go b/pkg/operator/crd.go index 0b20f3cac..826a02405 100644 --- a/pkg/operator/crd.go +++ b/pkg/operator/crd.go @@ -24,13 +24,14 @@ package operator import ( deplapi "github.com/arangodb/kube-arangodb/pkg/apis/deployment/v1alpha" + replapi "github.com/arangodb/kube-arangodb/pkg/apis/replication/v1alpha" lsapi "github.com/arangodb/kube-arangodb/pkg/apis/storage/v1alpha" "github.com/arangodb/kube-arangodb/pkg/util/crd" ) // waitForCRD waits for the CustomResourceDefinition (created externally) // to be ready. -func (o *Operator) waitForCRD(enableDeployment, enableStorage bool) error { +func (o *Operator) waitForCRD(enableDeployment, enableDeploymentReplication, enableStorage bool) error { log := o.log if enableDeployment { @@ -40,6 +41,13 @@ func (o *Operator) waitForCRD(enableDeployment, enableStorage bool) error { } } + if enableDeploymentReplication { + log.Debug().Msg("Waiting for ArangoDeploymentReplication CRD to be ready") + if err := crd.WaitCRDReady(o.KubeExtCli, replapi.ArangoDeploymentReplicationCRDName); err != nil { + return maskAny(err) + } + } + if enableStorage { log.Debug().Msg("Waiting for ArangoLocalStorage CRD to be ready") if err := crd.WaitCRDReady(o.KubeExtCli, lsapi.ArangoLocalStorageCRDName); err != nil { diff --git a/pkg/operator/operator.go b/pkg/operator/operator.go index fa0d75bd4..ba43136b4 100644 --- a/pkg/operator/operator.go +++ b/pkg/operator/operator.go @@ -34,10 +34,12 @@ import ( "k8s.io/client-go/tools/record" deplapi "github.com/arangodb/kube-arangodb/pkg/apis/deployment/v1alpha" + replapi "github.com/arangodb/kube-arangodb/pkg/apis/replication/v1alpha" lsapi "github.com/arangodb/kube-arangodb/pkg/apis/storage/v1alpha" "github.com/arangodb/kube-arangodb/pkg/deployment" "github.com/arangodb/kube-arangodb/pkg/generated/clientset/versioned" "github.com/arangodb/kube-arangodb/pkg/logging" + "github.com/arangodb/kube-arangodb/pkg/replication" "github.com/arangodb/kube-arangodb/pkg/storage" "github.com/arangodb/kube-arangodb/pkg/util/probe" ) @@ -47,50 +49,55 @@ const ( ) type Event struct { - Type kwatch.EventType - Deployment *deplapi.ArangoDeployment - LocalStorage *lsapi.ArangoLocalStorage + Type kwatch.EventType + Deployment *deplapi.ArangoDeployment + DeploymentReplication *replapi.ArangoDeploymentReplication + LocalStorage *lsapi.ArangoLocalStorage } type Operator struct { Config Dependencies - log zerolog.Logger - deployments map[string]*deployment.Deployment - localStorages map[string]*storage.LocalStorage + log zerolog.Logger + deployments map[string]*deployment.Deployment + deploymentReplications map[string]*replication.DeploymentReplication + localStorages map[string]*storage.LocalStorage } type Config struct { - ID string - Namespace string - PodName string - ServiceAccount string - LifecycleImage string - EnableDeployment bool - EnableStorage bool - AllowChaos bool + ID string + Namespace string + PodName string + ServiceAccount string + LifecycleImage string + EnableDeployment bool + EnableDeploymentReplication bool + EnableStorage bool + AllowChaos bool } type Dependencies struct { - LogService logging.Service - KubeCli kubernetes.Interface - KubeExtCli apiextensionsclient.Interface - CRCli versioned.Interface - EventRecorder record.EventRecorder - LivenessProbe *probe.LivenessProbe - DeploymentProbe *probe.ReadyProbe - StorageProbe *probe.ReadyProbe + LogService logging.Service + KubeCli kubernetes.Interface + KubeExtCli apiextensionsclient.Interface + CRCli versioned.Interface + EventRecorder record.EventRecorder + LivenessProbe *probe.LivenessProbe + DeploymentProbe *probe.ReadyProbe + DeploymentReplicationProbe *probe.ReadyProbe + StorageProbe *probe.ReadyProbe } // NewOperator instantiates a new operator from given config & dependencies. func NewOperator(config Config, deps Dependencies) (*Operator, error) { o := &Operator{ - Config: config, - Dependencies: deps, - log: deps.LogService.MustGetLogger("operator"), - deployments: make(map[string]*deployment.Deployment), - localStorages: make(map[string]*storage.LocalStorage), + Config: config, + Dependencies: deps, + log: deps.LogService.MustGetLogger("operator"), + deployments: make(map[string]*deployment.Deployment), + deploymentReplications: make(map[string]*replication.DeploymentReplication), + localStorages: make(map[string]*storage.LocalStorage), } return o, nil } @@ -100,6 +107,9 @@ func (o *Operator) Run() { if o.Config.EnableDeployment { go o.runLeaderElection("arango-deployment-operator", o.onStartDeployment) } + if o.Config.EnableDeploymentReplication { + go o.runLeaderElection("arango-deployment-replication-operator", o.onStartDeploymentReplication) + } if o.Config.EnableStorage { go o.runLeaderElection("arango-storage-operator", o.onStartStorage) } @@ -110,7 +120,7 @@ func (o *Operator) Run() { // onStartDeployment starts the deployment operator and run till given channel is closed. func (o *Operator) onStartDeployment(stop <-chan struct{}) { for { - if err := o.waitForCRD(true, false); err == nil { + if err := o.waitForCRD(true, false, false); err == nil { break } else { log.Error().Err(err).Msg("Resource initialization failed") @@ -121,10 +131,24 @@ func (o *Operator) onStartDeployment(stop <-chan struct{}) { o.runDeployments(stop) } +// onStartDeploymentReplication starts the deployment replication operator and run till given channel is closed. +func (o *Operator) onStartDeploymentReplication(stop <-chan struct{}) { + for { + if err := o.waitForCRD(false, true, false); err == nil { + break + } else { + log.Error().Err(err).Msg("Resource initialization failed") + log.Info().Msgf("Retrying in %s...", initRetryWaitTime) + time.Sleep(initRetryWaitTime) + } + } + o.runDeploymentReplications(stop) +} + // onStartStorage starts the storage operator and run till given channel is closed. func (o *Operator) onStartStorage(stop <-chan struct{}) { for { - if err := o.waitForCRD(false, true); err == nil { + if err := o.waitForCRD(false, false, true); err == nil { break } else { log.Error().Err(err).Msg("Resource initialization failed") diff --git a/pkg/operator/operator_deployment_relication.go b/pkg/operator/operator_deployment_relication.go new file mode 100644 index 000000000..fef7bfc3e --- /dev/null +++ b/pkg/operator/operator_deployment_relication.go @@ -0,0 +1,215 @@ +// +// DISCLAIMER +// +// Copyright 2018 ArangoDB GmbH, Cologne, Germany +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// +// Author Ewout Prangsma +// + +package operator + +import ( + "fmt" + + "github.com/pkg/errors" + kwatch "k8s.io/apimachinery/pkg/watch" + "k8s.io/client-go/tools/cache" + + api "github.com/arangodb/kube-arangodb/pkg/apis/replication/v1alpha" + "github.com/arangodb/kube-arangodb/pkg/metrics" + "github.com/arangodb/kube-arangodb/pkg/replication" + "github.com/arangodb/kube-arangodb/pkg/util/k8sutil" +) + +var ( + deploymentReplicationsCreated = metrics.MustRegisterCounter("controller", "deployment_replications_created", "Number of deployment replications that have been created") + deploymentReplicationsDeleted = metrics.MustRegisterCounter("controller", "deployment_replications_deleted", "Number of deployment replications that have been deleted") + deploymentReplicationsFailed = metrics.MustRegisterCounter("controller", "deployment_replications_failed", "Number of deployment replications that have failed") + deploymentReplicationsModified = metrics.MustRegisterCounter("controller", "deployment_replications_modified", "Number of deployment replication modifications") + deploymentReplicationsCurrent = metrics.MustRegisterGauge("controller", "deployment_replications", "Number of deployment replications currently being managed") +) + +// run the deployment replications part of the operator. +// This registers a listener and waits until the process stops. +func (o *Operator) runDeploymentReplications(stop <-chan struct{}) { + rw := k8sutil.NewResourceWatcher( + o.log, + o.Dependencies.CRCli.ReplicationV1alpha().RESTClient(), + api.ArangoDeploymentReplicationResourcePlural, + o.Config.Namespace, + &api.ArangoDeploymentReplication{}, + cache.ResourceEventHandlerFuncs{ + AddFunc: o.onAddArangoDeploymentReplication, + UpdateFunc: o.onUpdateArangoDeploymentReplication, + DeleteFunc: o.onDeleteArangoDeploymentReplication, + }) + + o.Dependencies.DeploymentReplicationProbe.SetReady() + rw.Run(stop) +} + +// onAddArangoDeploymentReplication deployment replication addition callback +func (o *Operator) onAddArangoDeploymentReplication(obj interface{}) { + o.Dependencies.LivenessProbe.Lock() + defer o.Dependencies.LivenessProbe.Unlock() + + apiObject := obj.(*api.ArangoDeploymentReplication) + o.log.Debug(). + Str("name", apiObject.GetObjectMeta().GetName()). + Msg("ArangoDeploymentReplication added") + o.syncArangoDeploymentReplication(apiObject) +} + +// onUpdateArangoDeploymentReplication deployment replication update callback +func (o *Operator) onUpdateArangoDeploymentReplication(oldObj, newObj interface{}) { + o.Dependencies.LivenessProbe.Lock() + defer o.Dependencies.LivenessProbe.Unlock() + + apiObject := newObj.(*api.ArangoDeploymentReplication) + o.log.Debug(). + Str("name", apiObject.GetObjectMeta().GetName()). + Msg("ArangoDeploymentReplication updated") + o.syncArangoDeploymentReplication(apiObject) +} + +// onDeleteArangoDeploymentReplication deployment replication delete callback +func (o *Operator) onDeleteArangoDeploymentReplication(obj interface{}) { + o.Dependencies.LivenessProbe.Lock() + defer o.Dependencies.LivenessProbe.Unlock() + + log := o.log + apiObject, ok := obj.(*api.ArangoDeploymentReplication) + if !ok { + tombstone, ok := obj.(cache.DeletedFinalStateUnknown) + if !ok { + log.Error().Interface("event-object", obj).Msg("unknown object from ArangoDeploymentReplication delete event") + return + } + apiObject, ok = tombstone.Obj.(*api.ArangoDeploymentReplication) + if !ok { + log.Error().Interface("event-object", obj).Msg("Tombstone contained object that is not an ArangoDeploymentReplication") + return + } + } + log.Debug(). + Str("name", apiObject.GetObjectMeta().GetName()). + Msg("ArangoDeploymentReplication deleted") + ev := &Event{ + Type: kwatch.Deleted, + DeploymentReplication: apiObject, + } + + // pt.start() + err := o.handleDeploymentReplicationEvent(ev) + if err != nil { + log.Warn().Err(err).Msg("Failed to handle event") + } + //pt.stop() +} + +// syncArangoDeploymentReplication synchronized the given deployment replication. +func (o *Operator) syncArangoDeploymentReplication(apiObject *api.ArangoDeploymentReplication) { + ev := &Event{ + Type: kwatch.Added, + DeploymentReplication: apiObject, + } + // re-watch or restart could give ADD event. + // If for an ADD event the cluster spec is invalid then it is not added to the local cache + // so modifying that deployment will result in another ADD event + if _, ok := o.deployments[apiObject.Name]; ok { + ev.Type = kwatch.Modified + } + + //pt.start() + err := o.handleDeploymentEvent(ev) + if err != nil { + o.log.Warn().Err(err).Msg("Failed to handle event") + } + //pt.stop() +} + +// handleDeploymentReplicationEvent processed the given event. +func (o *Operator) handleDeploymentReplicationEvent(event *Event) error { + apiObject := event.DeploymentReplication + + if apiObject.Status.Phase.IsFailed() { + deploymentReplicationsFailed.Inc() + if event.Type == kwatch.Deleted { + delete(o.deploymentReplications, apiObject.Name) + return nil + } + return maskAny(fmt.Errorf("ignore failed deployment replication (%s). Please delete its CR", apiObject.Name)) + } + + switch event.Type { + case kwatch.Added: + if _, ok := o.deploymentReplications[apiObject.Name]; ok { + return maskAny(fmt.Errorf("unsafe state. deployment replication (%s) was created before but we received event (%s)", apiObject.Name, event.Type)) + } + + // Fill in defaults + apiObject.Spec.SetDefaults() + // Validate deployment spec + if err := apiObject.Spec.Validate(); err != nil { + return maskAny(errors.Wrapf(err, "invalid deployment replication spec. please fix the following problem with the deployment replication spec: %v", err)) + } + + cfg, deps := o.makeDeploymentReplicationConfigAndDeps(apiObject) + nc, err := replication.New(cfg, deps, apiObject) + if err != nil { + return maskAny(fmt.Errorf("failed to create deployment: %s", err)) + } + o.deploymentReplications[apiObject.Name] = nc + + deploymentReplicationsCreated.Inc() + deploymentReplicationsCurrent.Set(float64(len(o.deploymentReplications))) + + case kwatch.Modified: + repl, ok := o.deploymentReplications[apiObject.Name] + if !ok { + return maskAny(fmt.Errorf("unsafe state. deployment replication (%s) was never created but we received event (%s)", apiObject.Name, event.Type)) + } + repl.Update(apiObject) + deploymentReplicationsModified.Inc() + + case kwatch.Deleted: + repl, ok := o.deploymentReplications[apiObject.Name] + if !ok { + return maskAny(fmt.Errorf("unsafe state. deployment replication (%s) was never created but we received event (%s)", apiObject.Name, event.Type)) + } + repl.Delete() + delete(o.deploymentReplications, apiObject.Name) + deploymentReplicationsDeleted.Inc() + deploymentReplicationsCurrent.Set(float64(len(o.deploymentReplications))) + } + return nil +} + +// makeDeploymentReplicationConfigAndDeps creates a Config & Dependencies object for a new DeploymentReplication. +func (o *Operator) makeDeploymentReplicationConfigAndDeps(apiObject *api.ArangoDeploymentReplication) (replication.Config, replication.Dependencies) { + cfg := replication.Config{ + Namespace: o.Config.Namespace, + } + deps := replication.Dependencies{ + Log: o.Dependencies.LogService.MustGetLogger("deployment-replication").With(). + Str("deployment-replication", apiObject.GetName()). + Logger(), + KubeCli: o.Dependencies.KubeCli, + CRCli: o.Dependencies.CRCli, + } + return cfg, deps +} diff --git a/pkg/replication/deployment_replication.go b/pkg/replication/deployment_replication.go new file mode 100644 index 000000000..f2697c83e --- /dev/null +++ b/pkg/replication/deployment_replication.go @@ -0,0 +1,380 @@ +// +// DISCLAIMER +// +// Copyright 2018 ArangoDB GmbH, Cologne, Germany +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// +// Author Ewout Prangsma +// + +package replication + +import ( + "fmt" + "reflect" + "sync/atomic" + "time" + + "github.com/rs/zerolog" + "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + corev1 "k8s.io/client-go/kubernetes/typed/core/v1" + + api "github.com/arangodb/kube-arangodb/pkg/apis/replication/v1alpha" + "github.com/arangodb/kube-arangodb/pkg/generated/clientset/versioned" + "github.com/arangodb/kube-arangodb/pkg/util/k8sutil" + "github.com/arangodb/kube-arangodb/pkg/util/retry" + "github.com/arangodb/kube-arangodb/pkg/util/trigger" +) + +// Config holds configuration settings for a DeploymentReplication +type Config struct { + Namespace string + // PodName string + // ServiceAccount string +} + +// Dependencies holds dependent services for a DeploymentReplication +type Dependencies struct { + Log zerolog.Logger + KubeCli kubernetes.Interface + CRCli versioned.Interface +} + +// deploymentReplicationEvent strongly typed type of event +type deploymentReplicationEventType string + +const ( + eventArangoDeploymentReplicationUpdated deploymentReplicationEventType = "DeploymentReplicationUpdated" +) + +// seploymentReplicationEvent holds an event passed from the controller to the deployment replication. +type deploymentReplicationEvent struct { + Type deploymentReplicationEventType + DeploymentReplication *api.ArangoDeploymentReplication +} + +const ( + deploymentReplicationEventQueueSize = 100 + minInspectionInterval = time.Second // Ensure we inspect the generated resources no less than with this interval + maxInspectionInterval = time.Minute // Ensure we inspect the generated resources no less than with this interval +) + +// DeploymentReplication is the in process state of an ArangoDeploymentReplication. +type DeploymentReplication struct { + apiObject *api.ArangoDeploymentReplication // API object + status api.DeploymentReplicationStatus // Internal status of the CR + config Config + deps Dependencies + + eventCh chan *deploymentReplicationEvent + stopCh chan struct{} + stopped int32 + + eventsCli corev1.EventInterface + + inspectTrigger trigger.Trigger +} + +// New creates a new DeploymentReplication from the given API object. +func New(config Config, deps Dependencies, apiObject *api.ArangoDeploymentReplication) (*DeploymentReplication, error) { + if err := apiObject.Spec.Validate(); err != nil { + return nil, maskAny(err) + } + dr := &DeploymentReplication{ + apiObject: apiObject, + status: *(apiObject.Status.DeepCopy()), + config: config, + deps: deps, + eventCh: make(chan *deploymentReplicationEvent, deploymentReplicationEventQueueSize), + stopCh: make(chan struct{}), + eventsCli: deps.KubeCli.Core().Events(apiObject.GetNamespace()), + } + + go dr.run() + + return dr, nil +} + +// Update the deployment replication. +// This sends an update event in the event queue. +func (dr *DeploymentReplication) Update(apiObject *api.ArangoDeploymentReplication) { + dr.send(&deploymentReplicationEvent{ + Type: eventArangoDeploymentReplicationUpdated, + DeploymentReplication: apiObject, + }) +} + +// Delete the deployment replication. +// Called when the local storage was deleted by the user. +func (dr *DeploymentReplication) Delete() { + dr.deps.Log.Info().Msg("deployment replication is deleted by user") + if atomic.CompareAndSwapInt32(&dr.stopped, 0, 1) { + close(dr.stopCh) + } +} + +// send given event into the deployment replication event queue. +func (dr *DeploymentReplication) send(ev *deploymentReplicationEvent) { + select { + case dr.eventCh <- ev: + l, ecap := len(dr.eventCh), cap(dr.eventCh) + if l > int(float64(ecap)*0.8) { + dr.deps.Log.Warn(). + Int("used", l). + Int("capacity", ecap). + Msg("event queue buffer is almost full") + } + case <-dr.stopCh: + } +} + +// run is the core the core worker. +// It processes the event queue and polls the state of generated +// resource on a regular basis. +func (dr *DeploymentReplication) run() { + //log := dr.deps.Log + + inspectionInterval := maxInspectionInterval + recentInspectionErrors := 0 + for { + select { + case <-dr.stopCh: + // We're being stopped. + return + + case event := <-dr.eventCh: + // Got event from event queue + switch event.Type { + case eventArangoDeploymentReplicationUpdated: + if err := dr.handleArangoDeploymentReplicationUpdatedEvent(event); err != nil { + dr.failOnError(err, "Failed to handle deployment replication update") + return + } + default: + panic("unknown event type" + event.Type) + } + + case <-dr.inspectTrigger.Done(): + hasError := false + if hasError { + if recentInspectionErrors == 0 { + inspectionInterval = minInspectionInterval + recentInspectionErrors++ + } + } else { + recentInspectionErrors = 0 + } + + case <-time.After(inspectionInterval): + // Trigger inspection + dr.inspectTrigger.Trigger() + // Backoff with next interval + inspectionInterval = time.Duration(float64(inspectionInterval) * 1.5) + if inspectionInterval > maxInspectionInterval { + inspectionInterval = maxInspectionInterval + } + } + } +} + +// handleArangoDeploymentReplicationUpdatedEvent is called when the deployment replication is updated by the user. +func (dr *DeploymentReplication) handleArangoDeploymentReplicationUpdatedEvent(event *deploymentReplicationEvent) error { + log := dr.deps.Log.With().Str("deployoment-replication", event.DeploymentReplication.GetName()).Logger() + repls := dr.deps.CRCli.ReplicationV1alpha().ArangoDeploymentReplications(dr.apiObject.GetNamespace()) + + // Get the most recent version of the deployment replication from the API server + current, err := repls.Get(dr.apiObject.GetName(), metav1.GetOptions{}) + if err != nil { + log.Debug().Err(err).Msg("Failed to get current version of deployment replication from API server") + if k8sutil.IsNotFound(err) { + return nil + } + return maskAny(err) + } + + newAPIObject := current.DeepCopy() + newAPIObject.Spec.SetDefaults() + newAPIObject.Status = dr.status + resetFields := dr.apiObject.Spec.ResetImmutableFields(&newAPIObject.Spec) + if len(resetFields) > 0 { + log.Debug().Strs("fields", resetFields).Msg("Found modified immutable fields") + } + if err := newAPIObject.Spec.Validate(); err != nil { + dr.createEvent(k8sutil.NewErrorEvent("Validation failed", err, dr.apiObject)) + // Try to reset object + if err := dr.updateCRSpec(dr.apiObject.Spec); err != nil { + log.Error().Err(err).Msg("Restore original spec failed") + dr.createEvent(k8sutil.NewErrorEvent("Restore original failed", err, dr.apiObject)) + } + return nil + } + if len(resetFields) > 0 { + for _, fieldName := range resetFields { + log.Debug().Str("field", fieldName).Msg("Reset immutable field") + dr.createEvent(k8sutil.NewImmutableFieldEvent(fieldName, dr.apiObject)) + } + } + + // Save updated spec + if err := dr.updateCRSpec(newAPIObject.Spec); err != nil { + return maskAny(fmt.Errorf("failed to update ArangoDeploymentReplication spec: %v", err)) + } + + // Trigger inspect + dr.inspectTrigger.Trigger() + + return nil +} + +// createEvent creates a given event. +// On error, the error is logged. +func (dr *DeploymentReplication) createEvent(evt *v1.Event) { + if _, err := dr.eventsCli.Create(evt); err != nil { + dr.deps.Log.Error().Err(err).Interface("event", *evt).Msg("Failed to record event") + } +} + +// Update the status of the API object from the internal status +func (dr *DeploymentReplication) updateCRStatus() error { + if reflect.DeepEqual(dr.apiObject.Status, dr.status) { + // Nothing has changed + return nil + } + + // Send update to API server + log := dr.deps.Log + repls := dr.deps.CRCli.ReplicationV1alpha().ArangoDeploymentReplications(dr.apiObject.GetNamespace()) + update := dr.apiObject.DeepCopy() + attempt := 0 + for { + attempt++ + update.Status = dr.status + newAPIObject, err := repls.Update(update) + if err == nil { + // Update internal object + dr.apiObject = newAPIObject + return nil + } + if attempt < 10 && k8sutil.IsConflict(err) { + // API object may have been changed already, + // Reload api object and try again + var current *api.ArangoDeploymentReplication + current, err = repls.Get(update.GetName(), metav1.GetOptions{}) + if err == nil { + update = current.DeepCopy() + continue + } + } + if err != nil { + log.Debug().Err(err).Msg("failed to patch ArangoDeploymentReplication status") + return maskAny(fmt.Errorf("failed to patch ArangoDeploymentReplication status: %v", err)) + } + } +} + +// Update the spec part of the API object (d.apiObject) +// to the given object, while preserving the status. +// On success, d.apiObject is updated. +func (dr *DeploymentReplication) updateCRSpec(newSpec api.DeploymentReplicationSpec) error { + log := dr.deps.Log + repls := dr.deps.CRCli.ReplicationV1alpha().ArangoDeploymentReplications(dr.apiObject.GetNamespace()) + + // Send update to API server + update := dr.apiObject.DeepCopy() + attempt := 0 + for { + attempt++ + update.Spec = newSpec + update.Status = dr.status + newAPIObject, err := repls.Update(update) + if err == nil { + // Update internal object + dr.apiObject = newAPIObject + return nil + } + if attempt < 10 && k8sutil.IsConflict(err) { + // API object may have been changed already, + // Reload api object and try again + var current *api.ArangoDeploymentReplication + current, err = repls.Get(update.GetName(), metav1.GetOptions{}) + if err == nil { + update = current.DeepCopy() + continue + } + } + if err != nil { + log.Debug().Err(err).Msg("failed to patch ArangoDeploymentReplication spec") + return maskAny(fmt.Errorf("failed to patch ArangoDeploymentReplication spec: %v", err)) + } + } +} + +// failOnError reports the given error and sets the deployment replication status to failed. +func (dr *DeploymentReplication) failOnError(err error, msg string) { + log := dr.deps.Log + log.Error().Err(err).Msg(msg) + dr.status.Reason = err.Error() + dr.reportFailedStatus() +} + +// reportFailedStatus sets the status of the deployment replication to Failed and keeps trying to forward +// that to the API server. +func (dr *DeploymentReplication) reportFailedStatus() { + log := dr.deps.Log + log.Info().Msg("local storage failed. Reporting failed reason...") + repls := dr.deps.CRCli.ReplicationV1alpha().ArangoDeploymentReplications(dr.apiObject.GetNamespace()) + + op := func() error { + dr.status.Phase = api.DeploymentReplicationPhaseFailed + err := dr.updateCRStatus() + if err == nil || k8sutil.IsNotFound(err) { + // Status has been updated + return nil + } + + if !k8sutil.IsConflict(err) { + log.Warn().Err(err).Msg("retry report status: fail to update") + return maskAny(err) + } + + depl, err := repls.Get(dr.apiObject.Name, metav1.GetOptions{}) + if err != nil { + // Update (PUT) will return conflict even if object is deleted since we have UID set in object. + // Because it will check UID first and return something like: + // "Precondition failed: UID in precondition: 0xc42712c0f0, UID in object meta: ". + if k8sutil.IsNotFound(err) { + return nil + } + log.Warn().Err(err).Msg("retry report status: fail to get latest version") + return maskAny(err) + } + dr.apiObject = depl + return maskAny(fmt.Errorf("retry needed")) + } + + retry.Retry(op, time.Hour*24*365) +} + +// isOwnerOf returns true if the given object belong to this local storage. +func (dr *DeploymentReplication) isOwnerOf(obj metav1.Object) bool { + ownerRefs := obj.GetOwnerReferences() + if len(ownerRefs) < 1 { + return false + } + return ownerRefs[0].UID == dr.apiObject.UID +} diff --git a/pkg/replication/errors.go b/pkg/replication/errors.go new file mode 100644 index 000000000..e48fb52d3 --- /dev/null +++ b/pkg/replication/errors.go @@ -0,0 +1,29 @@ +// +// DISCLAIMER +// +// Copyright 2018 ArangoDB GmbH, Cologne, Germany +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// +// Author Ewout Prangsma +// + +package replication + +import "github.com/pkg/errors" + +var ( + maskAny = errors.WithStack +) diff --git a/tools/manifests/manifest_builder.go b/tools/manifests/manifest_builder.go index 06c40f010..1a1c5467c 100644 --- a/tools/manifests/manifest_builder.go +++ b/tools/manifests/manifest_builder.go @@ -41,19 +41,24 @@ var ( OutputSuffix string TemplatesDir string - Namespace string - Image string - ImagePullPolicy string - ImageSHA256 bool - DeploymentOperatorName string - StorageOperatorName string - RBAC bool - AllowChaos bool + Namespace string + Image string + ImagePullPolicy string + ImageSHA256 bool + DeploymentOperatorName string + DeploymentReplicationOperatorName string + StorageOperatorName string + RBAC bool + AllowChaos bool } deploymentTemplateNames = []string{ "rbac.yaml", "deployment.yaml", } + deploymentReplicationTemplateNames = []string{ + "rbac.yaml", + "deployment-replication.yaml", + } storageTemplateNames = []string{ "rbac.yaml", "deployment.yaml", @@ -71,6 +76,7 @@ func init() { pflag.StringVar(&options.ImagePullPolicy, "image-pull-policy", "IfNotPresent", "Pull policy of the ArangoDB operator image") pflag.BoolVar(&options.ImageSHA256, "image-sha256", true, "Use SHA256 syntax for image") pflag.StringVar(&options.DeploymentOperatorName, "deployment-operator-name", "arango-deployment-operator", "Name of the ArangoDeployment operator deployment") + pflag.StringVar(&options.DeploymentReplicationOperatorName, "deployment-replication-operator-name", "arango-deployment-replication-operator", "Name of the ArangoDeploymentReplication operator deployment") pflag.StringVar(&options.StorageOperatorName, "storage-operator-name", "arango-storage-operator", "Name of the ArangoLocalStorage operator deployment") pflag.BoolVar(&options.RBAC, "rbac", true, "Use role based access control") pflag.BoolVar(&options.AllowChaos, "allow-chaos", false, "If set, allows chaos in deployments") @@ -79,12 +85,13 @@ func init() { } type TemplateOptions struct { - Image string - ImagePullPolicy string - RBAC bool - Deployment ResourceOptions - Storage ResourceOptions - Test CommonOptions + Image string + ImagePullPolicy string + RBAC bool + Deployment ResourceOptions + DeploymentReplication ResourceOptions + Storage ResourceOptions + Test CommonOptions } type CommonOptions struct { @@ -128,9 +135,10 @@ func main() { // Prepare templates to include templateNameSet := map[string][]string{ - "deployment": deploymentTemplateNames, - "storage": storageTemplateNames, - "test": testTemplateNames, + "deployment": deploymentTemplateNames, + "deployment-replication": deploymentReplicationTemplateNames, + "storage": storageTemplateNames, + "test": testTemplateNames, } // Process templates @@ -154,6 +162,21 @@ func main() { OperatorDeploymentName: "arango-deployment-operator", AllowChaos: options.AllowChaos, }, + DeploymentReplication: ResourceOptions{ + User: CommonOptions{ + Namespace: options.Namespace, + RoleName: "arango-deployment-replications", + RoleBindingName: "arango-deployment-replications", + ServiceAccountName: "default", + }, + Operator: CommonOptions{ + Namespace: options.Namespace, + RoleName: "arango-deployment-replication-operator", + RoleBindingName: "arango-deployment-replication-operator", + ServiceAccountName: "default", + }, + OperatorDeploymentName: "arango-deployment-replication-operator", + }, Storage: ResourceOptions{ User: CommonOptions{ Namespace: options.Namespace, From 018d75333cc6665f260caaaa8e6f90408a42b8f5 Mon Sep 17 00:00:00 2001 From: Ewout Prangsma Date: Fri, 18 May 2018 16:31:31 +0200 Subject: [PATCH 2/6] Building ArangoDeploymentReplication operator --- Makefile | 1 + .../go-certificates/keyfile.go | 77 ++- .../arangodb/arangosync/client/api.go | 333 ++++++++++ .../arangosync/client/api_internal.go | 448 ++++++++++++++ .../arangodb/arangosync/client/client.go | 389 ++++++++++++ .../arangosync/client/client_cache.go | 130 ++++ .../arangosync/client/client_master.go | 582 ++++++++++++++++++ .../arangosync/client/client_worker.go | 119 ++++ .../arangodb/arangosync/client/endpoint.go | 148 +++++ .../arangosync/client/endpoint_test.go | 213 +++++++ .../arangodb/arangosync/client/error.go | 190 ++++++ .../arangodb/arangosync/client/http.go | 64 ++ .../arangosync/pkg/errors/aggregate_error.go | 63 ++ .../arangodb/arangosync/pkg/errors/errors.go | 207 +++++++ .../arangodb/arangosync/pkg/jwt/error.go | 33 + .../arangodb/arangosync/pkg/jwt/jwt.go | 137 +++++ .../arangodb/arangosync/pkg/retry/retry.go | 152 +++++ .../arangodb/arangosync/tasks/task.go | 153 +++++ main.go | 1 + .../arango-deployment-replication-dev.yaml | 4 +- manifests/crd.yaml | 18 + .../deployment-replication.yaml | 2 +- .../v1alpha/authentication_spec.go | 68 ++ pkg/apis/replication/v1alpha/conditions.go | 131 ++++ .../replication/v1alpha/conditions_test.go | 95 +++ .../v1alpha/endpoint_authentication_spec.go | 68 ++ pkg/apis/replication/v1alpha/endpoint_spec.go | 80 +++ .../replication/v1alpha/endpoint_tls_spec.go | 68 ++ .../replication/v1alpha/replication_spec.go | 42 +- ...ication_state.go => replication_status.go} | 3 + .../v1alpha/zz_generated.deepcopy.go | 130 +++- .../operator_deployment_relication.go | 4 +- pkg/replication/deployment_replication.go | 18 +- pkg/replication/sync_client.go | 95 +++ pkg/replication/sync_inspector.go | 190 ++++++ pkg/util/k8sutil/secrets.go | 18 + 36 files changed, 4430 insertions(+), 44 deletions(-) create mode 100644 deps/github.com/arangodb/arangosync/client/api.go create mode 100644 deps/github.com/arangodb/arangosync/client/api_internal.go create mode 100644 deps/github.com/arangodb/arangosync/client/client.go create mode 100644 deps/github.com/arangodb/arangosync/client/client_cache.go create mode 100644 deps/github.com/arangodb/arangosync/client/client_master.go create mode 100644 deps/github.com/arangodb/arangosync/client/client_worker.go create mode 100644 deps/github.com/arangodb/arangosync/client/endpoint.go create mode 100644 deps/github.com/arangodb/arangosync/client/endpoint_test.go create mode 100644 deps/github.com/arangodb/arangosync/client/error.go create mode 100644 deps/github.com/arangodb/arangosync/client/http.go create mode 100644 deps/github.com/arangodb/arangosync/pkg/errors/aggregate_error.go create mode 100644 deps/github.com/arangodb/arangosync/pkg/errors/errors.go create mode 100644 deps/github.com/arangodb/arangosync/pkg/jwt/error.go create mode 100644 deps/github.com/arangodb/arangosync/pkg/jwt/jwt.go create mode 100644 deps/github.com/arangodb/arangosync/pkg/retry/retry.go create mode 100644 deps/github.com/arangodb/arangosync/tasks/task.go create mode 100644 pkg/apis/replication/v1alpha/authentication_spec.go create mode 100644 pkg/apis/replication/v1alpha/conditions.go create mode 100644 pkg/apis/replication/v1alpha/conditions_test.go create mode 100644 pkg/apis/replication/v1alpha/endpoint_authentication_spec.go create mode 100644 pkg/apis/replication/v1alpha/endpoint_spec.go create mode 100644 pkg/apis/replication/v1alpha/endpoint_tls_spec.go rename pkg/apis/replication/v1alpha/{replication_state.go => replication_status.go} (90%) create mode 100644 pkg/replication/sync_client.go create mode 100644 pkg/replication/sync_inspector.go diff --git a/Makefile b/Makefile index 4c008e2c2..dfd8ea227 100644 --- a/Makefile +++ b/Makefile @@ -231,6 +231,7 @@ run-unit-tests: $(GOBUILDDIR) $(SOURCES) golang:$(GOVERSION) \ go test $(TESTVERBOSEOPTIONS) \ $(REPOPATH)/pkg/apis/deployment/v1alpha \ + $(REPOPATH)/pkg/apis/replication/v1alpha \ $(REPOPATH)/pkg/apis/storage/v1alpha \ $(REPOPATH)/pkg/deployment/reconcile \ $(REPOPATH)/pkg/deployment/resources \ diff --git a/deps/github.com/arangodb-helper/go-certificates/keyfile.go b/deps/github.com/arangodb-helper/go-certificates/keyfile.go index 1f57e2c91..376d080db 100644 --- a/deps/github.com/arangodb-helper/go-certificates/keyfile.go +++ b/deps/github.com/arangodb-helper/go-certificates/keyfile.go @@ -38,14 +38,13 @@ import ( "strings" ) -// LoadKeyFile loads a SSL keyfile formatted for the arangod server. -func LoadKeyFile(keyFile string) (tls.Certificate, error) { - raw, err := ioutil.ReadFile(keyFile) - if err != nil { - return tls.Certificate{}, maskAny(err) - } +// Keyfile contains 1 or more certificates and a private key. +type Keyfile tls.Certificate - result := tls.Certificate{} +// NewKeyfile creates a keyfile from given content. +func NewKeyfile(content string) (Keyfile, error) { + raw := []byte(content) + result := Keyfile{} for { var derBlock *pem.Block derBlock, raw = pem.Decode(raw) @@ -56,22 +55,74 @@ func LoadKeyFile(keyFile string) (tls.Certificate, error) { result.Certificate = append(result.Certificate, derBlock.Bytes) } else if derBlock.Type == "PRIVATE KEY" || strings.HasSuffix(derBlock.Type, " PRIVATE KEY") { if result.PrivateKey == nil { + var err error result.PrivateKey, err = parsePrivateKey(derBlock.Bytes) if err != nil { - return tls.Certificate{}, maskAny(err) + return Keyfile{}, maskAny(err) } } } } + return result, nil +} - if len(result.Certificate) == 0 { - return tls.Certificate{}, maskAny(fmt.Errorf("No certificates found in %s", keyFile)) +// Validate the contents of the keyfile +func (kf Keyfile) Validate() error { + if len(kf.Certificate) == 0 { + return maskAny(fmt.Errorf("No certificates found in keyfile")) } - if result.PrivateKey == nil { - return tls.Certificate{}, maskAny(fmt.Errorf("No private key found in %s", keyFile)) + if kf.PrivateKey == nil { + return maskAny(fmt.Errorf("No private key found in keyfile")) } - return result, nil + return nil +} + +// EncodeCACertificates extracts the CA certificate(s) from the given keyfile (if any). +func (kf Keyfile) EncodeCACertificates() (string, error) { + buf := &bytes.Buffer{} + for _, derBytes := range kf.Certificate { + c, err := x509.ParseCertificate(derBytes) + if err != nil { + return "", maskAny(err) + } + if c.IsCA { + pem.Encode(buf, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}) + } + } + + return buf.String(), nil +} + +// EncodeCertificates extracts all certificates from the given keyfile and encodes them as PEM blocks. +func (kf Keyfile) EncodeCertificates() string { + buf := &bytes.Buffer{} + for _, derBytes := range kf.Certificate { + pem.Encode(buf, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}) + } + + return buf.String() +} + +// EncodePrivateKey extract the private key from the given keyfile and encodes is as PEM block. +func (kf Keyfile) EncodePrivateKey() string { + buf := &bytes.Buffer{} + pem.Encode(buf, pemBlockForKey(kf.PrivateKey)) + return buf.String() +} + +// LoadKeyFile loads a SSL keyfile formatted for the arangod server. +func LoadKeyFile(keyFile string) (tls.Certificate, error) { + raw, err := ioutil.ReadFile(keyFile) + if err != nil { + return tls.Certificate{}, maskAny(err) + } + + kf, err := NewKeyfile(string(raw)) + if err != nil { + return tls.Certificate{}, maskAny(err) + } + return tls.Certificate(kf), nil } // ExtractCACertificateFromKeyFile loads a SSL keyfile formatted for the arangod server and diff --git a/deps/github.com/arangodb/arangosync/client/api.go b/deps/github.com/arangodb/arangosync/client/api.go new file mode 100644 index 000000000..a00a17ab0 --- /dev/null +++ b/deps/github.com/arangodb/arangosync/client/api.go @@ -0,0 +1,333 @@ +// +// Copyright 2017 ArangoDB GmbH, Cologne, Germany +// +// The Programs (which include both the software and documentation) contain +// proprietary information of ArangoDB GmbH; they are provided under a license +// agreement containing restrictions on use and disclosure and are also +// protected by copyright, patent and other intellectual and industrial +// property laws. Reverse engineering, disassembly or decompilation of the +// Programs, except to the extent required to obtain interoperability with +// other independently created software or as specified by law, is prohibited. +// +// It shall be the licensee's responsibility to take all appropriate fail-safe, +// backup, redundancy, and other measures to ensure the safe use of +// applications if the Programs are used for purposes such as nuclear, +// aviation, mass transit, medical, or other inherently dangerous applications, +// and ArangoDB GmbH disclaims liability for any damages caused by such use of +// the Programs. +// +// This software is the confidential and proprietary information of ArangoDB +// GmbH. You shall not disclose such confidential and proprietary information +// and shall use it only in accordance with the terms of the license agreement +// you entered into with ArangoDB GmbH. +// +// Author Ewout Prangsma +// + +package client + +import ( + "context" + "time" + + "github.com/arangodb/arangosync/tasks" + "github.com/pkg/errors" +) + +// API of a sync master/worker +type API interface { + // Close this client + Close() error + // Get the version of the sync master/worker + Version(ctx context.Context) (VersionInfo, error) + // Get the role of the sync master/worker + Role(ctx context.Context) (Role, error) + // Health performs a quick health check. + // Returns an error when anything is wrong. If so, check Status. + Health(ctx context.Context) error + // Returns the master API (only valid when Role returns master) + Master() MasterAPI + // Returns the worker API (only valid when Role returns worker) + Worker() WorkerAPI + + // Set the ID of the client that is making requests. + SetClientID(id string) + // SetShared marks the client as shared. + // Closing a shared client will not close all idle connections. + SetShared() + // SynchronizeMasterEndpoints ensures that the client is using all known master + // endpoints. + // Do not use for connections to workers. + // Returns true when endpoints have changed. + SynchronizeMasterEndpoints(ctx context.Context) (bool, error) + // Endpoint returns the currently used endpoint for this client. + Endpoint() Endpoint +} + +const ( + // ClientIDHeaderKey is the name of a request header containing the ID that is + // making the request. + ClientIDHeaderKey = "X-ArangoSync-Client-ID" +) + +// MasterAPI contains API of sync master +type MasterAPI interface { + // Gets the current status of synchronization towards the local cluster. + Status(ctx context.Context) (SyncInfo, error) + // Configure the master to synchronize the local cluster from a given remote cluster. + Synchronize(ctx context.Context, input SynchronizationRequest) error + // Configure the master to stop & completely cancel the current synchronization of the + // local cluster from a remote cluster. + // Errors: + // - RequestTimeoutError when input.WaitTimeout is non-zero and the inactive stage is not reached in time. + CancelSynchronization(ctx context.Context, input CancelSynchronizationRequest) (CancelSynchronizationResponse, error) + // Reset a failed shard synchronization. + ResetShardSynchronization(ctx context.Context, dbName, colName string, shardIndex int) error + // Update the maximum allowed time between messages in a task channel. + SetMessageTimeout(ctx context.Context, timeout time.Duration) error + // Return a list of all known master endpoints of this datacenter. + // The resulting endpoints are usable from inside and outside the datacenter. + GetEndpoints(ctx context.Context) (Endpoint, error) + // Return a list of master endpoints of the leader (syncmaster) of this datacenter. + // Length of returned list will be 1 or the call will fail because no master is available. + // In the very rare occasion that the leadership is changing during this call, a list + // of length 0 can be returned. + // The resulting endpoint is usable only within the same datacenter. + GetLeaderEndpoint(ctx context.Context) (Endpoint, error) + // Return a list of known masters in this datacenter. + Masters(ctx context.Context) ([]MasterInfo, error) + + InternalMasterAPI +} + +// WorkerAPI contains API of sync worker +type WorkerAPI interface { + InternalWorkerAPI +} + +type VersionInfo struct { + Version string `json:"version"` + Build string `json:"build"` +} + +// MasterInfo contains information about a single master. +type MasterInfo struct { + // Unique identifier of the master + ID string `json:"id"` + // Internal endpoint of the master + Endpoint string `json:"endpoint"` + // Is this master the current leader + Leader bool `json:"leader"` +} + +type RoleInfo struct { + Role Role `json:"role"` +} + +type Role string + +const ( + RoleMaster Role = "master" + RoleWorker Role = "worker" +) + +func (r Role) IsMaster() bool { return r == RoleMaster } +func (r Role) IsWorker() bool { return r == RoleWorker } + +type ChannelPrefixInfo struct { + Prefix string `json:"prefix"` +} + +// SyncInfo holds the JSON info returned from `GET /_api/sync` +type SyncInfo struct { + Source Endpoint `json:"source"` // Endpoint of sync master on remote cluster + Status SyncStatus `json:"status"` // Overall status of (incoming) synchronization + Shards []ShardSyncInfo `json:"shards,omitempty"` // Status of incoming synchronization per shard + Outgoing []OutgoingSyncInfo `json:"outgoing,omitempty"` // Status of outgoing synchronization + MessageTimeout time.Duration `json:"messageTimeout,omitempty"` // Maximum time between messages in a task channel +} + +// OutgoingSyncInfo holds JSON info returned as part of `GET /_api/sync` +// regarding a specific target for outgoing synchronization data. +type OutgoingSyncInfo struct { + ID string `json:"id"` // ID of sync master to which data is being send + Endpoint Endpoint `json:"endpoint"` // Endpoint of sync masters to which data is being send + Status SyncStatus `json:"status"` // Overall status for this outgoing target + Shards []ShardSyncInfo `json:"shards,omitempty"` // Status of outgoing synchronization per shard for this target +} + +// ShardSyncInfo holds JSON info returned as part of `GET /_api/sync` +// regarding a specific shard. +type ShardSyncInfo struct { + Database string `json:"database"` // Database containing the collection - shard + Collection string `json:"collection"` // Collection containing the shard + ShardIndex int `json:"shardIndex"` // Index of the shard (0..) + Status SyncStatus `json:"status"` // Status of this shard + StatusMessage string `json:"status_message,omitempty"` // Human readable message about the status of this shard + Delay time.Duration `json:"delay,omitempty"` // Delay between other datacenter and us. + LastMessage time.Time `json:"last_message"` // Time of last message received by the task handling this shard + LastDataChange time.Time `json:"last_data_change"` // Time of last message that resulted in a data change, received by the task handling this shard + LastShardMasterChange time.Time `json:"last_shard_master_change"` // Time of when we last had a change in the status of the shard master + ShardMasterKnown bool `json:"shard_master_known"` // Is the shard master known? +} + +type SyncStatus string + +const ( + // SyncStatusInactive indicates that no synchronization is taking place + SyncStatusInactive SyncStatus = "inactive" + // SyncStatusInitializing indicates that synchronization tasks are being setup + SyncStatusInitializing SyncStatus = "initializing" + // SyncStatusInitialSync indicates that initial synchronization of collections is ongoing + SyncStatusInitialSync SyncStatus = "initial-sync" + // SyncStatusRunning indicates that all collections have been initially synchronized + // and normal transaction synchronization is active. + SyncStatusRunning SyncStatus = "running" + // SyncStatusCancelling indicates that the synchronization process is being cancelled. + SyncStatusCancelling SyncStatus = "cancelling" + // SyncStatusFailed indicates that the synchronization process has encountered an unrecoverable failure + SyncStatusFailed SyncStatus = "failed" +) + +var ( + // ValidSyncStatusValues is a list of all possible sync status values. + ValidSyncStatusValues = []SyncStatus{ + SyncStatusInactive, + SyncStatusInitializing, + SyncStatusInitialSync, + SyncStatusRunning, + SyncStatusCancelling, + SyncStatusFailed, + } +) + +// Normalize converts an empty status to inactive. +func (s SyncStatus) Normalize() SyncStatus { + if s == "" { + return SyncStatusInactive + } + return s +} + +// Equals returns true when the other status is equal to the given +// status (both normalized). +func (s SyncStatus) Equals(other SyncStatus) bool { + return s.Normalize() == other.Normalize() +} + +// IsInactiveOrEmpty returns true if the given status equals inactive or is empty. +func (s SyncStatus) IsInactiveOrEmpty() bool { + return s == SyncStatusInactive || s == "" +} + +// IsInitialSyncOrRunning returns true if the given status equals initial-sync or running. +func (s SyncStatus) IsInitialSyncOrRunning() bool { + return s == SyncStatusInitialSync || s == SyncStatusRunning +} + +// IsActive returns true if the given status indicates an active state. +// The is: initializing, initial-sync or running +func (s SyncStatus) IsActive() bool { + return s == SyncStatusInitializing || s == SyncStatusInitialSync || s == SyncStatusRunning +} + +// +// TLSAuthentication contains configuration for using client certificates +// and TLS verification of the server. +type TLSAuthentication = tasks.TLSAuthentication + +type SynchronizationRequest struct { + // Endpoint of sync master of the source cluster + Source Endpoint `json:"source"` + // Authentication of the master + Authentication TLSAuthentication `json:"authentication"` +} + +// Clone returns a deep copy of the given request. +func (r SynchronizationRequest) Clone() SynchronizationRequest { + c := r + c.Source = r.Source.Clone() + return c +} + +// IsSame returns true if both requests contain the same values. +// The source is considered the same is the intersection of existing & given source is not empty. +// We consider an intersection because: +// - Servers can be down, resulting in a temporary missing endpoint +// - Customer can specify only 1 of all servers +func (r SynchronizationRequest) IsSame(other SynchronizationRequest) bool { + if r.Source.Intersection(other.Source).IsEmpty() { + return false + } + if r.Authentication.ClientCertificate != other.Authentication.ClientCertificate { + return false + } + if r.Authentication.ClientKey != other.Authentication.ClientKey { + return false + } + if r.Authentication.CACertificate != other.Authentication.CACertificate { + return false + } + return true +} + +// Validate checks the values of the given request and returns an error +// in case of improper values. +// Returns nil on success. +func (r SynchronizationRequest) Validate() error { + if len(r.Source) == 0 { + return errors.Wrap(BadRequestError, "source missing") + } + if err := r.Source.Validate(); err != nil { + return errors.Wrapf(BadRequestError, "Invalid source: %s", err.Error()) + } + if r.Authentication.ClientCertificate == "" { + return errors.Wrap(BadRequestError, "clientCertificate missing") + } + if r.Authentication.ClientKey == "" { + return errors.Wrap(BadRequestError, "clientKey missing") + } + if r.Authentication.CACertificate == "" { + return errors.Wrap(BadRequestError, "caCertificate missing") + } + return nil +} + +type CancelSynchronizationRequest struct { + // WaitTimeout is the amount of time the cancel function will wait + // until the synchronization has reached an `inactive` state. + // If this value is zero, the cancel function will only switch to the canceling state + // but not wait until the `inactive` state is reached. + WaitTimeout time.Duration `json:"wait_timeout,omitempty"` + // Force is set if you want to end the synchronization even if the source + // master cannot be reached. + Force bool `json:"force,omitempty"` + // ForceTimeout is the amount of time the syncmaster tries to contact + // the source master to notify it about cancelling the synchronization. + // This fields is only used when Force is true. + ForceTimeout time.Duration `json:"force_timeout,omitempty"` +} + +type CancelSynchronizationResponse struct { + // Aborted is set when synchronization has cancelled (state is now inactive) + // but the source sync master was not notified. + // This is only possible when the Force flags is set on the request. + Aborted bool `json:"aborted,omitempty"` + // Source is the endpoint of sync master on remote cluster that we used + // to be synchronizing from. + Source Endpoint `json:"source,omitempty"` + // ClusterID is the ID of the local synchronization cluster. + ClusterID string `json:"cluster_id,omitempty"` +} + +type SetMessageTimeoutRequest struct { + MessageTimeout time.Duration `json:"messageTimeout"` +} + +type EndpointsResponse struct { + Endpoints Endpoint `json:"endpoints"` +} + +type MastersResponse struct { + Masters []MasterInfo `json:"masters"` +} diff --git a/deps/github.com/arangodb/arangosync/client/api_internal.go b/deps/github.com/arangodb/arangosync/client/api_internal.go new file mode 100644 index 000000000..daa650911 --- /dev/null +++ b/deps/github.com/arangodb/arangosync/client/api_internal.go @@ -0,0 +1,448 @@ +// +// Copyright 2017 ArangoDB GmbH, Cologne, Germany +// +// The Programs (which include both the software and documentation) contain +// proprietary information of ArangoDB GmbH; they are provided under a license +// agreement containing restrictions on use and disclosure and are also +// protected by copyright, patent and other intellectual and industrial +// property laws. Reverse engineering, disassembly or decompilation of the +// Programs, except to the extent required to obtain interoperability with +// other independently created software or as specified by law, is prohibited. +// +// It shall be the licensee's responsibility to take all appropriate fail-safe, +// backup, redundancy, and other measures to ensure the safe use of +// applications if the Programs are used for purposes such as nuclear, +// aviation, mass transit, medical, or other inherently dangerous applications, +// and ArangoDB GmbH disclaims liability for any damages caused by such use of +// the Programs. +// +// This software is the confidential and proprietary information of ArangoDB +// GmbH. You shall not disclose such confidential and proprietary information +// and shall use it only in accordance with the terms of the license agreement +// you entered into with ArangoDB GmbH. +// +// Author Ewout Prangsma +// + +package client + +import ( + "context" + "encoding/json" + "fmt" + "math" + "time" + + "github.com/arangodb/arangosync/tasks" +) + +// InternalMasterAPI contains the internal API of the sync master. +type InternalMasterAPI interface { + // Worker -> Master + + // Load configuration data from the master + ConfigureWorker(ctx context.Context, endpoint string) (WorkerConfiguration, error) + // Return all registered workers + RegisteredWorkers(ctx context.Context) ([]WorkerRegistration, error) + // Return info about a specific worker + RegisteredWorker(ctx context.Context, id string) (WorkerRegistration, error) + // Register (or update registration of) a worker + RegisterWorker(ctx context.Context, endpoint, token, hostID string) (WorkerRegistrationResponse, error) + // Remove the registration of a worker + UnregisterWorker(ctx context.Context, id string) error + // Get info about a specific task + Task(ctx context.Context, id string) (TaskInfo, error) + // Get all known tasks + Tasks(ctx context.Context) ([]TaskInfo, error) + // Get all known tasks for a given channel + TasksByChannel(ctx context.Context, channelName string) ([]TaskInfo, error) + // Notify the master that a task with given ID has completed. + TaskCompleted(ctx context.Context, taskID string, info TaskCompletedRequest) error + // Create tasks to start synchronization of a shard in the given db+col. + SynchronizeShard(ctx context.Context, dbName, colName string, shardIndex int) error + // Stop tasks to synchronize a shard in the given db+col. + CancelSynchronizeShard(ctx context.Context, dbName, colName string, shardIndex int) error + // Report status of the synchronization of a shard back to the master. + SynchronizeShardStatus(ctx context.Context, entries []SynchronizationShardStatusRequestEntry) error + // IsChannelRelevant checks if a MQ channel is still relevant + IsChannelRelevant(ctx context.Context, channelName string) (bool, error) + + // Worker & Master -> Master + // GetDirectMQTopicEndpoint returns an endpoint that the caller can use to fetch direct MQ messages + // from. + // This method requires a directMQ token or client cert for authentication. + GetDirectMQTopicEndpoint(ctx context.Context, channelName string) (DirectMQTopicEndpoint, error) + // RenewDirectMQToken renews a given direct MQ token. + // This method requires a directMQ token for authentication. + RenewDirectMQToken(ctx context.Context, token string) (DirectMQToken, error) + // CloneDirectMQToken creates a clone of a given direct MQ token. + // When the given token is revoked, the newly cloned token is also revoked. + // This method requires a directMQ token for authentication. + CloneDirectMQToken(ctx context.Context, token string) (DirectMQToken, error) + // Add entire direct MQ API + InternalDirectMQAPI + + // Master -> Master + + // Start a task that sends inventory data to a receiving remote cluster. + OutgoingSynchronization(ctx context.Context, input OutgoingSynchronizationRequest) (OutgoingSynchronizationResponse, error) + // Cancel sending synchronization data to the remote cluster with given ID. + CancelOutgoingSynchronization(ctx context.Context, remoteID string) error + // Create tasks to send synchronization data of a shard in the given db+col to a remote cluster. + OutgoingSynchronizeShard(ctx context.Context, remoteID, dbName, colName string, shardIndex int, input OutgoingSynchronizeShardRequest) error + // Stop tasks to send synchronization data of a shard in the given db+col to a remote cluster. + CancelOutgoingSynchronizeShard(ctx context.Context, remoteID, dbName, colName string, shardIndex int) error + // Report status of the synchronization of a shard back to the master. + OutgoingSynchronizeShardStatus(ctx context.Context, entries []SynchronizationShardStatusRequestEntry) error + // Reset a failed shard synchronization. + OutgoingResetShardSynchronization(ctx context.Context, remoteID, dbName, colName string, shardIndex int, newControlChannel, newDataChannel string) error + + // Get a prefix for names of channels that contain message + // going to this master. + ChannelPrefix(ctx context.Context) (string, error) + // Get the local message queue configuration. + GetMessageQueueConfig(ctx context.Context) (MessageQueueConfig, error) +} + +// InternalWorkerAPI contains the internal API of the sync worker. +type InternalWorkerAPI interface { + // StartTask is called by the master to instruct the worker + // to run a task with given instructions. + StartTask(ctx context.Context, data StartTaskRequest) error + // StopTask is called by the master to instruct the worker + // to stop all work on the given task. + StopTask(ctx context.Context, taskID string) error + // SetDirectMQTopicToken configures the token used to access messages of a given channel. + SetDirectMQTopicToken(ctx context.Context, channelName, token string, tokenTTL time.Duration) error + // Add entire direct MQ API + InternalDirectMQAPI +} + +// InternalDirectMQAPI contains the internal API of the sync master/worker wrt direct MQ messages. +type InternalDirectMQAPI interface { + // GetDirectMQMessages return messages for a given MQ channel. + GetDirectMQMessages(ctx context.Context, channelName string) ([]DirectMQMessage, error) + // CommitDirectMQMessage removes all messages from the given channel up to an including the given offset. + CommitDirectMQMessage(ctx context.Context, channelName string, offset int64) error +} + +// MessageQueueConfig contains all deployment configuration info for the local MQ. +type MessageQueueConfig struct { + Type string `json:"type"` + Endpoints []string `json:"endpoints"` + Authentication TLSAuthentication `json:"authentication"` +} + +// Clone returns a deep copy of the given config +func (c MessageQueueConfig) Clone() MessageQueueConfig { + result := c + result.Endpoints = append([]string{}, c.Endpoints...) + return result +} + +// ConfigureWorkerRequest is the JSON body for the ConfigureWorker request. +type ConfigureWorkerRequest struct { + Endpoint string `json:"endpoint"` // Endpoint of the worker +} + +// WorkerConfiguration contains configuration data passed from +// the master to the worker. +type WorkerConfiguration struct { + Cluster struct { + Endpoints []string `json:"endpoints"` + JWTSecret string `json:"jwtSecret,omitempty"` + MaxDocumentSize int `json:"maxDocumentSize,omitempty"` + // Minimum replication factor of new/modified collections + MinReplicationFactor int `json:"min-replication-factor,omitempty"` + // Maximum replication factor of new/modified collections + MaxReplicationFactor int `json:"max-replication-factor,omitempty"` + } `json:"cluster"` + HTTPServer struct { + Certificate string `json:"certificate"` + Key string `json:"key"` + } `json:"httpServer"` + MessageQueue struct { + MessageQueueConfig // MQ configuration of local MQ + } `json:"mq"` +} + +// SetDefaults fills empty values with defaults +func (c *WorkerConfiguration) SetDefaults() { + if c.Cluster.MinReplicationFactor <= 0 { + c.Cluster.MinReplicationFactor = 1 + } + if c.Cluster.MaxReplicationFactor <= 0 { + c.Cluster.MaxReplicationFactor = math.MaxInt32 + } +} + +// Validate the given configuration. +// Return an error on validation errors, nil when all ok. +func (c WorkerConfiguration) Validate() error { + if c.Cluster.MinReplicationFactor < 1 { + return maskAny(fmt.Errorf("MinReplicationFactor must be >= 1")) + } + if c.Cluster.MaxReplicationFactor < 1 { + return maskAny(fmt.Errorf("MaxReplicationFactor must be >= 1")) + } + if c.Cluster.MaxReplicationFactor < c.Cluster.MinReplicationFactor { + return maskAny(fmt.Errorf("MaxReplicationFactor must be >= MinReplicationFactor")) + } + return nil +} + +type WorkerRegistrations struct { + Workers []WorkerRegistration `json:"workers"` +} + +type WorkerRegistration struct { + // ID of the worker assigned to it by the master + ID string `json:"id"` + // Endpoint of the worker + Endpoint string `json:"endpoint"` + // Expiration time of the last registration of the worker + ExpiresAt time.Time `json:"expiresAt"` + // ID of the worker when communicating with ArangoDB servers. + ServerID int64 `json:"serverID"` + // IF of the host the worker process is running on + HostID string `json:"host,omitempty"` +} + +// Validate the given registration. +// Return nil if ok, error otherwise. +func (wr WorkerRegistration) Validate() error { + if wr.ID == "" { + return maskAny(fmt.Errorf("ID empty")) + } + if wr.Endpoint == "" { + return maskAny(fmt.Errorf("Endpoint empty")) + } + if wr.ServerID == 0 { + return maskAny(fmt.Errorf("ServerID == 0")) + } + return nil +} + +// IsExpired returns true when the given worker is expired. +func (wr WorkerRegistration) IsExpired() bool { + return time.Now().After(wr.ExpiresAt) +} + +type WorkerRegistrationRequest struct { + Endpoint string `json:"endpoint"` + Token string `json:"token,omitempty"` + HostID string `json:host,omitempty"` +} + +type WorkerRegistrationResponse struct { + WorkerRegistration + // Maximum time between message in a task channel. + MessageTimeout time.Duration `json:"messageTimeout,omitempty"` +} + +type StartTaskRequest struct { + ID string `json:"id"` + tasks.TaskData + // MQ configuration of the remote cluster + RemoteMessageQueueConfig MessageQueueConfig `json:"remote-mq-config"` +} + +// OutgoingSynchronizationRequest holds the master->master request +// data for configuring an outgoing inventory stream. +type OutgoingSynchronizationRequest struct { + // ID of remote cluster + ID string `json:"id"` + // Endpoints of sync masters of the remote (target) cluster + Target Endpoint `json:"target"` + Channels struct { + // Name of MQ topic to send inventory data to. + Inventory string `json:"inventory"` + } `json:"channels"` + // MQ configuration of the remote (target) cluster + MessageQueueConfig MessageQueueConfig `json:"mq-config"` +} + +// Clone returns a deep copy of the given request. +func (r OutgoingSynchronizationRequest) Clone() OutgoingSynchronizationRequest { + c := r + c.Target = r.Target.Clone() + c.MessageQueueConfig = r.MessageQueueConfig.Clone() + return c +} + +// OutgoingSynchronizationResponse holds the answer to an +// master->master request for configuring an outgoing synchronization. +type OutgoingSynchronizationResponse struct { + // MQ configuration of the remote (source) cluster + MessageQueueConfig MessageQueueConfig `json:"mq-config"` +} + +// OutgoingSynchronizeShardRequest holds the master->master request +// data for configuring an outgoing shard synchronization stream. +type OutgoingSynchronizeShardRequest struct { + Channels struct { + // Name of MQ topic to receive control messages on. + Control string `json:"control"` + // Name of MQ topic to send data messages to. + Data string `json:"data"` + } `json:"channels"` +} + +// SynchronizationShardStatusRequest is the request body of a (Outgoing)SynchronizationShardStatus request. +type SynchronizationShardStatusRequest struct { + Entries []SynchronizationShardStatusRequestEntry `json:"entries"` +} + +// SynchronizationShardStatusRequestEntry is a single entry in a SynchronizationShardStatusRequest +type SynchronizationShardStatusRequestEntry struct { + RemoteID string `json:"remoteID"` + Database string `json:"database"` + Collection string `json:"collection"` + ShardIndex int `json:"shardIndex"` + Status SynchronizationShardStatus `json:"status"` +} + +type SynchronizationShardStatus struct { + // Current status + Status SyncStatus `json:"status"` + // Human readable status message + StatusMessage string `json:"status_message,omitempty"` + // Delay between us and other data center. + Delay time.Duration `json:"delay"` + // Time of last message received by the task handling this shard + LastMessage time.Time `json:"last_message"` + // Time of last message that resulted in a data change, received by the task handling this shard + LastDataChange time.Time `json:"last_data_change"` + // Time of when we last had a change in the status of the shard master + LastShardMasterChange time.Time `json:"last_shard_master_change"` + // Is the shard master known? + ShardMasterKnown bool `json:"shard_master_known"` +} + +// IsSame returns true when the Status & StatusMessage of both statuses +// are equal and the Delay is very close. +func (s SynchronizationShardStatus) IsSame(other SynchronizationShardStatus) bool { + if s.Status != other.Status || s.StatusMessage != other.StatusMessage || + s.LastMessage != other.LastMessage || s.LastDataChange != other.LastDataChange || + s.LastShardMasterChange != other.LastShardMasterChange || s.ShardMasterKnown != other.ShardMasterKnown { + return false + } + return !IsSignificantDelayDiff(s.Delay, other.Delay) +} + +// TaskCompletedRequest holds the info for a TaskCompleted request. +type TaskCompletedRequest struct { + Error bool `json:"error,omitempty"` +} + +// TaskAssignment contains information of the assignment of a +// task to a worker. +// It is serialized as JSON into the agency. +type TaskAssignment struct { + // ID of worker the task is assigned to + WorkerID string `json:"worker_id"` + // When the assignment was made + CreatedAt time.Time `json:"created_at"` + // How many assignments have been made + Counter int `json:"counter,omitempty"` +} + +// TaskInfo contains all information known about a task. +type TaskInfo struct { + ID string `json:"id"` + Task tasks.TaskData `json:"task"` + Assignment TaskAssignment `json:"assignment"` +} + +// IsAssigned returns true when the task in given info is assigned to a +// worker, false otherwise. +func (i TaskInfo) IsAssigned() bool { + return i.Assignment.WorkerID != "" +} + +// NeedsCleanup returns true when the entry is subject to cleanup. +func (i TaskInfo) NeedsCleanup() bool { + return i.Assignment.Counter > 0 && !i.Task.Persistent +} + +// TasksResponse is the JSON response for MasterAPI.Tasks method. +type TasksResponse struct { + Tasks []TaskInfo `json:"tasks,omitempty"` +} + +// IsSignificantDelayDiff returns true if there is a significant difference +// between the given delays. +func IsSignificantDelayDiff(d1, d2 time.Duration) bool { + if d2 == 0 { + return d1 != 0 + } + x := float64(d1) / float64(d2) + return x < 0.9 || x > 1.1 +} + +// IsChannelRelevantResponse is the JSON response for a MasterAPI.IsChannelRelevant call +type IsChannelRelevantResponse struct { + IsRelevant bool `json:"isRelevant"` +} + +// StatusAPI describes the API provided to task workers used to send status updates to the master. +type StatusAPI interface { + // SendIncomingStatus queues a given incoming synchronization status entry for sending. + SendIncomingStatus(entry SynchronizationShardStatusRequestEntry) + // SendOutgoingStatus queues a given outgoing synchronization status entry for sending. + SendOutgoingStatus(entry SynchronizationShardStatusRequestEntry) +} + +// DirectMQToken provides a token with its TTL +type DirectMQToken struct { + // Token used to authenticate with the server. + Token string `json:"token"` + // How long the token will be valid. + // Afterwards a new token has to be fetched. + TokenTTL time.Duration `json:"token-ttl"` +} + +// DirectMQTokenRequest is the JSON request body for Renew/Clone direct MQ token request. +type DirectMQTokenRequest struct { + // Token used to authenticate with the server. + Token string `json:"token"` +} + +// DirectMQTopicEndpoint provides information about an endpoint for Direct MQ messages. +type DirectMQTopicEndpoint struct { + // Endpoint of the server that can provide messages for a specific topic. + Endpoint Endpoint `json:"endpoint"` + // CA certificate used to sign the TLS connection of the server. + // This is used for verifying the server. + CACertificate string `json:"caCertificate"` + // Token used to authenticate with the server. + Token string `json:"token"` + // How long the token will be valid. + // Afterwards a new token has to be fetched. + TokenTTL time.Duration `json:"token-ttl"` +} + +// SetDirectMQTopicTokenRequest is the JSON request body for SetDirectMQTopicToken request. +type SetDirectMQTopicTokenRequest struct { + // Token used to authenticate with the server. + Token string `json:"token"` + // How long the token will be valid. + // Afterwards a new token has to be fetched. + TokenTTL time.Duration `json:"token-ttl"` +} + +// DirectMQMessage is a direct MQ message. +type DirectMQMessage struct { + Offset int64 `json:"offset"` + Message json.RawMessage `json:"message"` +} + +// GetDirectMQMessagesResponse is the JSON body for GetDirectMQMessages response. +type GetDirectMQMessagesResponse struct { + Messages []DirectMQMessage `json:"messages,omitempty"` +} + +// CommitDirectMQMessageRequest is the JSON request body for CommitDirectMQMessage request. +type CommitDirectMQMessageRequest struct { + Offset int64 `json:"offset"` +} diff --git a/deps/github.com/arangodb/arangosync/client/client.go b/deps/github.com/arangodb/arangosync/client/client.go new file mode 100644 index 000000000..9cad19b21 --- /dev/null +++ b/deps/github.com/arangodb/arangosync/client/client.go @@ -0,0 +1,389 @@ +// +// Copyright 2017 ArangoDB GmbH, Cologne, Germany +// +// The Programs (which include both the software and documentation) contain +// proprietary information of ArangoDB GmbH; they are provided under a license +// agreement containing restrictions on use and disclosure and are also +// protected by copyright, patent and other intellectual and industrial +// property laws. Reverse engineering, disassembly or decompilation of the +// Programs, except to the extent required to obtain interoperability with +// other independently created software or as specified by law, is prohibited. +// +// It shall be the licensee's responsibility to take all appropriate fail-safe, +// backup, redundancy, and other measures to ensure the safe use of +// applications if the Programs are used for purposes such as nuclear, +// aviation, mass transit, medical, or other inherently dangerous applications, +// and ArangoDB GmbH disclaims liability for any damages caused by such use of +// the Programs. +// +// This software is the confidential and proprietary information of ArangoDB +// GmbH. You shall not disclose such confidential and proprietary information +// and shall use it only in accordance with the terms of the license agreement +// you entered into with ArangoDB GmbH. +// +// Author Ewout Prangsma +// + +package client + +import ( + "bytes" + "context" + "crypto/tls" + "encoding/base64" + "encoding/json" + "io" + "io/ioutil" + "math/rand" + "net/http" + "net/url" + "sync" + "sync/atomic" + "time" + + "github.com/arangodb/arangosync/pkg/jwt" + "github.com/pkg/errors" +) + +type AuthenticationConfig struct { + JWTSecret string + BearerToken string + UserName string + Password string +} + +var ( + sharedHTTPClient = DefaultHTTPClient(nil) +) + +const ( + // AllowForwardRequestHeaderKey is a request header key. + // If this header is set, the syncmaster will forward + // requests to the current leader instead of returning a + // 503. + AllowForwardRequestHeaderKey = "X-Allow-Forward-To-Leader" +) + +// NewArangoSyncClient creates a new client implementation. +func NewArangoSyncClient(endpoints []string, authConf AuthenticationConfig, tlsConfig *tls.Config) (API, error) { + httpClient := sharedHTTPClient + sharedClient := true + if tlsConfig != nil { + httpClient = DefaultHTTPClient(tlsConfig) + sharedClient = false + } + c := &client{ + auth: authConf, + client: httpClient, + sharedClient: sharedClient, + } + c.client.Timeout = 0 + c.endpoints.config = Endpoint(endpoints) + list, err := c.endpoints.config.URLs() + if err != nil { + return nil, maskAny(err) + } + c.endpoints.urls = list + return c, nil +} + +type client struct { + endpoints struct { + mutex sync.RWMutex + config Endpoint + urls []url.URL + preferred int32 + } + auth AuthenticationConfig + client *http.Client + sharedClient bool + clientID string +} + +const ( + contentTypeJSON = "application/json" +) + +// Returns the master API (only valid when Role returns master) +func (c *client) Master() MasterAPI { + return c +} + +// Returns the worker API (only valid when Role returns worker) +func (c *client) Worker() WorkerAPI { + return c +} + +// Set the ID of the client that is making requests. +func (c *client) SetClientID(id string) { + c.clientID = id +} + +// SetShared marks the client as shared. +// Closing a shared client will not close all idle connections. +func (c *client) SetShared() { + c.sharedClient = true +} + +// Close this client +func (c *client) Close() error { + if !c.sharedClient { + if transport, ok := c.client.Transport.(*http.Transport); ok { + transport.CloseIdleConnections() + } + } + return nil +} + +// Version requests the version of an arangosync instance. +func (c *client) Version(ctx context.Context) (VersionInfo, error) { + url := c.createURLs("/_api/version", nil) + + var result VersionInfo + req, err := c.newRequests("GET", url, nil) + if err != nil { + return VersionInfo{}, maskAny(err) + } + if err := c.do(ctx, req, &result); err != nil { + return VersionInfo{}, maskAny(err) + } + + return result, nil +} + +// Role requests the role of an arangosync instance. +func (c *client) Role(ctx context.Context) (Role, error) { + url := c.createURLs("/_api/role", nil) + + var result RoleInfo + req, err := c.newRequests("GET", url, nil) + if err != nil { + return "", maskAny(err) + } + if err := c.do(ctx, req, &result); err != nil { + return "", maskAny(err) + } + + return result.Role, nil +} + +// Endpoint returns the currently used endpoint for this client. +func (c *client) Endpoint() Endpoint { + c.endpoints.mutex.RLock() + defer c.endpoints.mutex.RUnlock() + + return c.endpoints.config +} + +// SynchronizeMasterEndpoints ensures that the client is using all known master +// endpoints. +// Do not use for connections to workers. +// Returns true when endpoints have changed. +func (c *client) SynchronizeMasterEndpoints(ctx context.Context) (bool, error) { + // Fetch all endpoints + update, err := c.GetEndpoints(ctx) + if err != nil { + return false, errors.Wrap(err, "Failed to get master endpoints") + } + c.endpoints.mutex.Lock() + defer c.endpoints.mutex.Unlock() + if !c.endpoints.config.Equals(update) { + // Load changed + list, err := update.URLs() + if err != nil { + return false, errors.Wrap(err, "Failed to parse master endpoints") + } + c.endpoints.config = update + c.endpoints.urls = list + return true, nil + } + return false, nil +} + +// createURLs creates a full URLs (for all endpoints) for a request with given local path & query. +func (c *client) createURLs(urlPath string, query url.Values) []string { + c.endpoints.mutex.RLock() + defer c.endpoints.mutex.RUnlock() + + result := make([]string, len(c.endpoints.urls)) + for i, ep := range c.endpoints.urls { + u := ep // Create copy + u.Path = urlPath + if query != nil { + u.RawQuery = query.Encode() + } + result[i] = u.String() + } + return result +} + +// newRequests creates new requests with optional body and context +// Returns: request, cancel, error +func (c *client) newRequests(method string, urls []string, body interface{}) ([]*http.Request, error) { + var encoded []byte + if body != nil { + var err error + encoded, err = json.Marshal(body) + if err != nil { + return nil, maskAny(err) + } + } + + result := make([]*http.Request, len(urls)) + for i, url := range urls { + var bodyRd io.Reader + if encoded != nil { + bodyRd = bytes.NewReader(encoded) + } + req, err := http.NewRequest(method, url, bodyRd) + if err != nil { + return nil, maskAny(err) + } + req.Header.Set(AllowForwardRequestHeaderKey, "true") + if c.auth.JWTSecret != "" { + jwt.AddArangoSyncJwtHeader(req, c.auth.JWTSecret) + } else if c.auth.BearerToken != "" { + req.Header.Set("Authorization", "Bearer "+c.auth.BearerToken) + } else if c.auth.UserName != "" { + plainText := c.auth.UserName + ":" + c.auth.Password + encoded := base64.StdEncoding.EncodeToString([]byte(plainText)) + req.Header.Set("Authorization", "Basic "+encoded) + } + if c.clientID != "" { + req.Header.Set(ClientIDHeaderKey, c.clientID) + } + result[i] = req + } + return result, nil +} + +type response struct { + Body []byte + StatusCode int + Request *http.Request +} + +// do performs the given requests all at once. +// The first request to answer with a success or permanent failure is returned. +func (c *client) do(ctx context.Context, reqs []*http.Request, result interface{}) error { + if ctx == nil { + ctx = context.Background() + } + var cancel func() + if _, hasDeadline := ctx.Deadline(); !hasDeadline { + ctx, cancel = context.WithTimeout(ctx, defaultHTTPTimeout) + } else { + ctx, cancel = context.WithCancel(ctx) + } + defer cancel() + + // All requests sequencially + order := rand.Perm(len(reqs)) + var lastErr error + for _, idx := range order { + retryNext, err := c.doOnce(ctx, []*http.Request{reqs[idx]}, result) + if err == nil { + return nil + } + if retryNext { + lastErr = err + } else { + return maskAny(err) + } + } + if lastErr != nil { + return maskAny(lastErr) + } + return maskAny(errors.Wrapf(ServiceUnavailableError, "No requests available")) +} + +// doOnce performs the given requests all at once. +// The first request to answer with a success or permanent failure is returned. +// Return: retryNext, error +func (c *client) doOnce(ctx context.Context, reqs []*http.Request, result interface{}) (bool, error) { + var cancel context.CancelFunc + ctx, cancel = context.WithCancel(ctx) + resultChan := make(chan response, len(reqs)) + errorChan := make(chan error, len(reqs)) + wg := sync.WaitGroup{} + for regIdx, req := range reqs { + req = req.WithContext(ctx) + wg.Add(1) + go func(regIdx int, req *http.Request) { + defer wg.Done() + + if len(reqs) > 1 { + preferred := atomic.LoadInt32(&c.endpoints.preferred) + if int32(regIdx) != preferred { + select { + case <-time.After(time.Millisecond * 50): + // Continue + case <-ctx.Done(): + // Context cancelled + errorChan <- maskAny(ctx.Err()) + return + } + } + } + resp, err := c.client.Do(req) + if err != nil { + // Request failed + errorChan <- maskAny(err) + return + } + + // Check status + statusCode := resp.StatusCode + if statusCode >= 200 && statusCode < 500 && statusCode != 408 { + // Read content + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + errorChan <- maskAny(err) + return + } + + // Success or permanent error + resultChan <- response{ + Body: body, + StatusCode: statusCode, + Request: req, + } + // Cancel all other requests + cancel() + atomic.StoreInt32(&c.endpoints.preferred, int32(regIdx)) + return + } + // No permanent error, try next agent + }(regIdx+1, req) // regIdx+1 is intended. That way a preferred==0 results in all requests being fired at once. + } + + // Wait for go routines to finished + wg.Wait() + cancel() + close(resultChan) + close(errorChan) + if resp, ok := <-resultChan; ok { + // Use first valid response + // Read response body into memory + if resp.StatusCode != http.StatusOK { + // Unexpected status, try to parse error. + return false, maskAny(parseResponseError(resp.Body, resp.StatusCode)) + } + + // Got a success status + if result != nil { + if err := json.Unmarshal(resp.Body, result); err != nil { + method := resp.Request.Method + url := resp.Request.URL.String() + return false, errors.Wrapf(err, "Failed decoding response data from %s request to %s: %v", method, url, err) + } + } + return false, nil + } + if err, ok := <-errorChan; ok { + // Return first error + return false, maskAny(err) + } + return true, errors.Wrapf(ServiceUnavailableError, "All %d servers responded with temporary failure", len(reqs)) +} diff --git a/deps/github.com/arangodb/arangosync/client/client_cache.go b/deps/github.com/arangodb/arangosync/client/client_cache.go new file mode 100644 index 000000000..d24fc57f3 --- /dev/null +++ b/deps/github.com/arangodb/arangosync/client/client_cache.go @@ -0,0 +1,130 @@ +// +// Copyright 2017 ArangoDB GmbH, Cologne, Germany +// +// The Programs (which include both the software and documentation) contain +// proprietary information of ArangoDB GmbH; they are provided under a license +// agreement containing restrictions on use and disclosure and are also +// protected by copyright, patent and other intellectual and industrial +// property laws. Reverse engineering, disassembly or decompilation of the +// Programs, except to the extent required to obtain interoperability with +// other independently created software or as specified by law, is prohibited. +// +// It shall be the licensee's responsibility to take all appropriate fail-safe, +// backup, redundancy, and other measures to ensure the safe use of +// applications if the Programs are used for purposes such as nuclear, +// aviation, mass transit, medical, or other inherently dangerous applications, +// and ArangoDB GmbH disclaims liability for any damages caused by such use of +// the Programs. +// +// This software is the confidential and proprietary information of ArangoDB +// GmbH. You shall not disclose such confidential and proprietary information +// and shall use it only in accordance with the terms of the license agreement +// you entered into with ArangoDB GmbH. +// +// Author Ewout Prangsma +// + +package client + +import ( + "crypto/sha1" + "fmt" + "strings" + "sync" + + certificates "github.com/arangodb-helper/go-certificates" + + "github.com/arangodb/arangosync/pkg/errors" + "github.com/rs/zerolog" +) + +type ClientCache struct { + mutex sync.Mutex + clients map[string]API +} + +// GetClient returns a client used to access the source with given authentication. +func (cc *ClientCache) GetClient(log zerolog.Logger, source Endpoint, auth Authentication, insecureSkipVerify bool) (API, error) { + if len(source) == 0 { + return nil, errors.Wrapf(PreconditionFailedError, "Cannot create master client: no source configured") + } + keyData := strings.Join(source, ",") + ":" + auth.String() + key := fmt.Sprintf("%x", sha1.Sum([]byte(keyData))) + + cc.mutex.Lock() + defer cc.mutex.Unlock() + + if cc.clients == nil { + cc.clients = make(map[string]API) + } + + // Get existing client (if any) + if c, ok := cc.clients[key]; ok { + return c, nil + } + + // Client does not exist, create one + log.Debug().Msg("Creating new client") + c, err := cc.createClient(source, auth, insecureSkipVerify) + if err != nil { + return nil, maskAny(err) + } + + cc.clients[key] = c + c.SetShared() + return c, nil +} + +// createClient creates a client used to access the source with given authentication. +func (cc *ClientCache) createClient(source Endpoint, auth Authentication, insecureSkipVerify bool) (API, error) { + if len(source) == 0 { + return nil, errors.Wrapf(PreconditionFailedError, "Cannot create master client: no source configured") + } + tlsConfig, err := certificates.CreateTLSConfigFromAuthentication(AuthProxy{auth.TLSAuthentication}, insecureSkipVerify) + if err != nil { + return nil, maskAny(err) + } + ac := AuthenticationConfig{} + if auth.JWTSecret != "" { + ac.JWTSecret = auth.JWTSecret + } else if auth.ClientToken != "" { + ac.BearerToken = auth.ClientToken + } + c, err := NewArangoSyncClient(source, ac, tlsConfig) + if err != nil { + return nil, maskAny(err) + } + return c, nil +} + +// NewAuthentication creates a new Authentication from given arguments. +func NewAuthentication(tlsAuth TLSAuthentication, jwtSecret string) Authentication { + return Authentication{ + TLSAuthentication: tlsAuth, + JWTSecret: jwtSecret, + } +} + +// Authentication contains all possible authentication methods for a client. +// Order of authentication methods: +// - JWTSecret +// - ClientToken +// - ClientCertificate +type Authentication struct { + TLSAuthentication + JWTSecret string +} + +// String returns a string used to unique identify the authentication settings. +func (a Authentication) String() string { + return a.TLSAuthentication.String() + ":" + a.JWTSecret +} + +// AuthProxy is a helper that implements github.com/arangodb-helper/go-certificates#TLSAuthentication. +type AuthProxy struct { + TLSAuthentication +} + +func (a AuthProxy) CACertificate() string { return a.TLSAuthentication.CACertificate } +func (a AuthProxy) ClientCertificate() string { return a.TLSAuthentication.ClientCertificate } +func (a AuthProxy) ClientKey() string { return a.TLSAuthentication.ClientKey } diff --git a/deps/github.com/arangodb/arangosync/client/client_master.go b/deps/github.com/arangodb/arangosync/client/client_master.go new file mode 100644 index 000000000..246b0fd6a --- /dev/null +++ b/deps/github.com/arangodb/arangosync/client/client_master.go @@ -0,0 +1,582 @@ +// +// Copyright 2017 ArangoDB GmbH, Cologne, Germany +// +// The Programs (which include both the software and documentation) contain +// proprietary information of ArangoDB GmbH; they are provided under a license +// agreement containing restrictions on use and disclosure and are also +// protected by copyright, patent and other intellectual and industrial +// property laws. Reverse engineering, disassembly or decompilation of the +// Programs, except to the extent required to obtain interoperability with +// other independently created software or as specified by law, is prohibited. +// +// It shall be the licensee's responsibility to take all appropriate fail-safe, +// backup, redundancy, and other measures to ensure the safe use of +// applications if the Programs are used for purposes such as nuclear, +// aviation, mass transit, medical, or other inherently dangerous applications, +// and ArangoDB GmbH disclaims liability for any damages caused by such use of +// the Programs. +// +// This software is the confidential and proprietary information of ArangoDB +// GmbH. You shall not disclose such confidential and proprietary information +// and shall use it only in accordance with the terms of the license agreement +// you entered into with ArangoDB GmbH. +// +// Author Ewout Prangsma +// + +package client + +import ( + "context" + "net/url" + "path" + "strconv" + "time" +) + +// Get a prefix for names of channels that contain message +// going to this master. +func (c *client) ChannelPrefix(ctx context.Context) (string, error) { + url := c.createURLs("/_api/channels/prefix", nil) + + var result ChannelPrefixInfo + req, err := c.newRequests("GET", url, nil) + if err != nil { + return "", maskAny(err) + } + if err := c.do(ctx, req, &result); err != nil { + return "", maskAny(err) + } + + return result.Prefix, nil +} + +// Get the local message queue configuration. +func (c *client) GetMessageQueueConfig(ctx context.Context) (MessageQueueConfig, error) { + url := c.createURLs("/_api/mq/config", nil) + + var result MessageQueueConfig + req, err := c.newRequests("GET", url, nil) + if err != nil { + return MessageQueueConfig{}, maskAny(err) + } + if err := c.do(ctx, req, &result); err != nil { + return MessageQueueConfig{}, maskAny(err) + } + + return result, nil +} + +// Gets the current status of synchronization towards the local cluster. +func (c *client) Status(ctx context.Context) (SyncInfo, error) { + url := c.createURLs("/_api/sync", nil) + + var result SyncInfo + req, err := c.newRequests("GET", url, nil) + if err != nil { + return SyncInfo{}, maskAny(err) + } + if err := c.do(ctx, req, &result); err != nil { + return SyncInfo{}, maskAny(err) + } + + return result, nil +} + +// Health performs a quick health check. +// Returns an error when anything is wrong. If so, check Status. +func (c *client) Health(ctx context.Context) error { + url := c.createURLs("/_api/health", nil) + + req, err := c.newRequests("GET", url, nil) + if err != nil { + return maskAny(err) + } + if err := c.do(ctx, req, nil); err != nil { + return maskAny(err) + } + + return nil +} + +// Return a list of all known master endpoints of this datacenter. +func (c *client) GetEndpoints(ctx context.Context) (Endpoint, error) { + url := c.createURLs("/_api/endpoints", nil) + + req, err := c.newRequests("GET", url, nil) + if err != nil { + return nil, maskAny(err) + } + var result EndpointsResponse + if err := c.do(ctx, req, &result); err != nil { + return nil, maskAny(err) + } + + return result.Endpoints, nil +} + +// Return a list of master endpoints of the leader (syncmaster) of this datacenter. +// Length of returned list will 1 or the call will fail because no master is available. +func (c *client) GetLeaderEndpoint(ctx context.Context) (Endpoint, error) { + url := c.createURLs("/_api/leader/endpoint", nil) + + req, err := c.newRequests("GET", url, nil) + if err != nil { + return nil, maskAny(err) + } + var result EndpointsResponse + if err := c.do(ctx, req, &result); err != nil { + return nil, maskAny(err) + } + + return result.Endpoints, nil +} + +// Return a list of known masters in this datacenter. +func (c *client) Masters(ctx context.Context) ([]MasterInfo, error) { + url := c.createURLs("/_api/masters", nil) + + req, err := c.newRequests("GET", url, nil) + if err != nil { + return nil, maskAny(err) + } + var result MastersResponse + if err := c.do(ctx, req, &result); err != nil { + return nil, maskAny(err) + } + + return result.Masters, nil +} + +// Synchronize configures the master to synchronize the local cluster from a given remote cluster. +func (c *client) Synchronize(ctx context.Context, input SynchronizationRequest) error { + url := c.createURLs("/_api/sync", nil) + + req, err := c.newRequests("POST", url, input) + if err != nil { + return maskAny(err) + } + if err := c.do(ctx, req, nil); err != nil { + return maskAny(err) + } + + return nil +} + +// CancelSynchronization configures the master to stop & completely cancel the current synchronization of the +// local cluster from a remote cluster. +func (c *client) CancelSynchronization(ctx context.Context, input CancelSynchronizationRequest) (CancelSynchronizationResponse, error) { + q := make(url.Values) + + url := c.createURLs("/_api/sync", q) + var result CancelSynchronizationResponse + req, err := c.newRequests("DELETE", url, input) + if err != nil { + return result, maskAny(err) + } + if err := c.do(ctx, req, &result); err != nil { + return result, maskAny(err) + } + + return result, nil +} + +// Reset a failed shard synchronization. +func (c *client) ResetShardSynchronization(ctx context.Context, dbName, colName string, shardIndex int) error { + url := c.createURLs(path.Join("/_api/sync/database", dbName, "collection", colName, "shard", strconv.Itoa(shardIndex), "reset"), nil) + req, err := c.newRequests("PUT", url, nil) + if err != nil { + return maskAny(err) + } + if err := c.do(ctx, req, nil); err != nil { + return maskAny(err) + } + + return nil +} + +// Update the maximum allowed time between messages in a task channel. +func (c *client) SetMessageTimeout(ctx context.Context, timeout time.Duration) error { + url := c.createURLs("/_api/message-timeout", nil) + input := SetMessageTimeoutRequest{ + MessageTimeout: timeout, + } + req, err := c.newRequests("PUT", url, input) + if err != nil { + return maskAny(err) + } + if err := c.do(ctx, req, nil); err != nil { + return maskAny(err) + } + + return nil +} + +// Create tasks to start synchronization of a shard in the given db+col. +func (c *client) SynchronizeShard(ctx context.Context, dbName, colName string, shardIndex int) error { + url := c.createURLs(path.Join("/_api/sync/database", dbName, "collection", colName, "shard", strconv.Itoa(shardIndex)), nil) + + req, err := c.newRequests("POST", url, struct{}{}) + if err != nil { + return maskAny(err) + } + if err := c.do(ctx, req, nil); err != nil { + return maskAny(err) + } + + return nil +} + +// Stop tasks to synchronize a shard in the given db+col. +func (c *client) CancelSynchronizeShard(ctx context.Context, dbName, colName string, shardIndex int) error { + url := c.createURLs(path.Join("/_api/sync/database", dbName, "collection", colName, "shard", strconv.Itoa(shardIndex)), nil) + + req, err := c.newRequests("DELETE", url, nil) + if err != nil { + return maskAny(err) + } + if err := c.do(ctx, req, nil); err != nil { + return maskAny(err) + } + + return nil +} + +// Report status of the synchronization of a shard back to the master. +func (c *client) SynchronizeShardStatus(ctx context.Context, entries []SynchronizationShardStatusRequestEntry) error { + url := c.createURLs(path.Join("/_api/sync/multiple/status"), nil) + + input := SynchronizationShardStatusRequest{ + Entries: entries, + } + req, err := c.newRequests("PUT", url, input) + if err != nil { + return maskAny(err) + } + if err := c.do(ctx, req, nil); err != nil { + return maskAny(err) + } + + return nil +} + +// IsChannelRelevant checks if a MQ channel is still relevant +func (c *client) IsChannelRelevant(ctx context.Context, channelName string) (bool, error) { + url := c.createURLs(path.Join("/_api/mq/channel", url.PathEscape(channelName), "is-relevant"), nil) + + req, err := c.newRequests("GET", url, nil) + if err != nil { + return false, maskAny(err) + } + var result IsChannelRelevantResponse + if err := c.do(ctx, req, &result); err != nil { + return false, maskAny(err) + } + + return result.IsRelevant, nil +} + +// GetDirectMQTopicEndpoint returns an endpoint that the caller can use to fetch direct MQ messages +// from. +func (c *client) GetDirectMQTopicEndpoint(ctx context.Context, channelName string) (DirectMQTopicEndpoint, error) { + url := c.createURLs(path.Join("/_api/mq/direct/channel", url.PathEscape(channelName), "endpoint"), nil) + + req, err := c.newRequests("GET", url, nil) + if err != nil { + return DirectMQTopicEndpoint{}, maskAny(err) + } + var result DirectMQTopicEndpoint + if err := c.do(ctx, req, &result); err != nil { + return DirectMQTopicEndpoint{}, maskAny(err) + } + + return result, nil +} + +// RenewDirectMQToken renews a given direct MQ token. +// This method requires a directMQ token for authentication. +func (c *client) RenewDirectMQToken(ctx context.Context, token string) (DirectMQToken, error) { + url := c.createURLs("/_api/mq/direct/token/renew", nil) + + input := DirectMQTokenRequest{ + Token: token, + } + req, err := c.newRequests("POST", url, input) + if err != nil { + return DirectMQToken{}, maskAny(err) + } + var result DirectMQToken + if err := c.do(ctx, req, &result); err != nil { + return DirectMQToken{}, maskAny(err) + } + + return result, nil +} + +// CloneDirectMQToken creates a clone of a given direct MQ token. +// When the given token is revoked, the newly cloned token is also revoked. +// This method requires a directMQ token for authentication. +func (c *client) CloneDirectMQToken(ctx context.Context, token string) (DirectMQToken, error) { + url := c.createURLs("/_api/mq/direct/token/clone", nil) + + input := DirectMQTokenRequest{ + Token: token, + } + req, err := c.newRequests("POST", url, input) + if err != nil { + return DirectMQToken{}, maskAny(err) + } + var result DirectMQToken + if err := c.do(ctx, req, &result); err != nil { + return DirectMQToken{}, maskAny(err) + } + + return result, nil +} + +// Start a task that sends inventory data to a receiving remote cluster. +func (c *client) OutgoingSynchronization(ctx context.Context, input OutgoingSynchronizationRequest) (OutgoingSynchronizationResponse, error) { + url := c.createURLs("/_api/sync/outgoing", nil) + + req, err := c.newRequests("POST", url, input) + if err != nil { + return OutgoingSynchronizationResponse{}, maskAny(err) + } + var result OutgoingSynchronizationResponse + if err := c.do(ctx, req, &result); err != nil { + return OutgoingSynchronizationResponse{}, maskAny(err) + } + + return result, nil +} + +// Cancel sending synchronization data to the remote cluster with given ID. +func (c *client) CancelOutgoingSynchronization(ctx context.Context, remoteID string) error { + url := c.createURLs(path.Join("/_api/sync/outgoing", remoteID), nil) + + req, err := c.newRequests("DELETE", url, nil) + if err != nil { + return maskAny(err) + } + if err := c.do(ctx, req, nil); err != nil { + return maskAny(err) + } + + return nil +} + +// Create tasks to send synchronization data of a shard in the given db+col to a remote cluster. +func (c *client) OutgoingSynchronizeShard(ctx context.Context, remoteID, dbName, colName string, shardIndex int, input OutgoingSynchronizeShardRequest) error { + url := c.createURLs(path.Join("/_api/sync/outgoing", remoteID, "database", dbName, "collection", colName, "shard", strconv.Itoa(shardIndex)), nil) + + req, err := c.newRequests("POST", url, input) + if err != nil { + return maskAny(err) + } + if err := c.do(ctx, req, nil); err != nil { + return maskAny(err) + } + + return nil +} + +// Stop tasks to send synchronization data of a shard in the given db+col to a remote cluster. +func (c *client) CancelOutgoingSynchronizeShard(ctx context.Context, remoteID, dbName, colName string, shardIndex int) error { + url := c.createURLs(path.Join("/_api/sync/outgoing", remoteID, "database", dbName, "collection", colName, "shard", strconv.Itoa(shardIndex)), nil) + + req, err := c.newRequests("DELETE", url, nil) + if err != nil { + return maskAny(err) + } + if err := c.do(ctx, req, nil); err != nil { + return maskAny(err) + } + + return nil +} + +// Report status of the synchronization of a shard back to the master. +func (c *client) OutgoingSynchronizeShardStatus(ctx context.Context, entries []SynchronizationShardStatusRequestEntry) error { + url := c.createURLs(path.Join("/_api/sync/multiple/outgoing/status"), nil) + + input := SynchronizationShardStatusRequest{ + Entries: entries, + } + req, err := c.newRequests("PUT", url, input) + if err != nil { + return maskAny(err) + } + if err := c.do(ctx, req, nil); err != nil { + return maskAny(err) + } + + return nil +} + +// Reset a failed shard synchronization. +func (c *client) OutgoingResetShardSynchronization(ctx context.Context, remoteID, dbName, colName string, shardIndex int, newControlChannel, newDataChannel string) error { + url := c.createURLs(path.Join("/_api/sync/outgoing", remoteID, "database", dbName, "collection", colName, "shard", strconv.Itoa(shardIndex), "reset"), nil) + + input := OutgoingSynchronizeShardRequest{} + input.Channels.Control = newControlChannel + input.Channels.Data = newDataChannel + req, err := c.newRequests("PUT", url, input) + if err != nil { + return maskAny(err) + } + if err := c.do(ctx, req, nil); err != nil { + return maskAny(err) + } + + return nil +} + +// Load configuration data from the master +func (c *client) ConfigureWorker(ctx context.Context, endpoint string) (WorkerConfiguration, error) { + url := c.createURLs("/_api/worker/configure", nil) + + input := ConfigureWorkerRequest{ + Endpoint: endpoint, + } + req, err := c.newRequests("POST", url, input) + if err != nil { + return WorkerConfiguration{}, maskAny(err) + } + var result WorkerConfiguration + if err := c.do(ctx, req, &result); err != nil { + return WorkerConfiguration{}, maskAny(err) + } + + return result, nil +} + +// Return all registered workers +func (c *client) RegisteredWorkers(ctx context.Context) ([]WorkerRegistration, error) { + url := c.createURLs("/_api/worker", nil) + + var result WorkerRegistrations + req, err := c.newRequests("GET", url, nil) + if err != nil { + return nil, maskAny(err) + } + if err := c.do(ctx, req, &result); err != nil { + return nil, maskAny(err) + } + + return result.Workers, nil +} + +// Return info about a specific registered worker +func (c *client) RegisteredWorker(ctx context.Context, id string) (WorkerRegistration, error) { + url := c.createURLs(path.Join("/_api/worker", id), nil) + + var result WorkerRegistration + req, err := c.newRequests("GET", url, nil) + if err != nil { + return WorkerRegistration{}, maskAny(err) + } + if err := c.do(ctx, req, &result); err != nil { + return WorkerRegistration{}, maskAny(err) + } + + return result, nil +} + +// Register (or update registration of) a worker +func (c *client) RegisterWorker(ctx context.Context, endpoint, token, hostID string) (WorkerRegistrationResponse, error) { + url := c.createURLs("/_api/worker", nil) + + input := WorkerRegistrationRequest{ + Endpoint: endpoint, + Token: token, + HostID: hostID, + } + var result WorkerRegistrationResponse + req, err := c.newRequests("PUT", url, input) + if err != nil { + return WorkerRegistrationResponse{}, maskAny(err) + } + if err := c.do(ctx, req, &result); err != nil { + return WorkerRegistrationResponse{}, maskAny(err) + } + + return result, nil +} + +// Remove the registration of a worker +func (c *client) UnregisterWorker(ctx context.Context, id string) error { + url := c.createURLs(path.Join("/_api/worker", id), nil) + + req, err := c.newRequests("DELETE", url, nil) + if err != nil { + return maskAny(err) + } + if err := c.do(ctx, req, nil); err != nil { + return maskAny(err) + } + + return nil +} + +// Get info about a specific task +func (c *client) Task(ctx context.Context, id string) (TaskInfo, error) { + url := c.createURLs(path.Join("/_api/task/id", id), nil) + + var result TaskInfo + req, err := c.newRequests("GET", url, nil) + if err != nil { + return TaskInfo{}, maskAny(err) + } + if err := c.do(ctx, req, &result); err != nil { + return TaskInfo{}, maskAny(err) + } + + return result, nil +} + +// Get all known tasks +func (c *client) Tasks(ctx context.Context) ([]TaskInfo, error) { + url := c.createURLs("/_api/task", nil) + + var result TasksResponse + req, err := c.newRequests("GET", url, nil) + if err != nil { + return nil, maskAny(err) + } + if err := c.do(ctx, req, &result); err != nil { + return nil, maskAny(err) + } + + return result.Tasks, nil +} + +// Get all known tasks for a given channel +func (c *client) TasksByChannel(ctx context.Context, channelName string) ([]TaskInfo, error) { + url := c.createURLs(path.Join("/_api/task/channel", channelName), nil) + + var result TasksResponse + req, err := c.newRequests("GET", url, nil) + if err != nil { + return nil, maskAny(err) + } + if err := c.do(ctx, req, &result); err != nil { + return nil, maskAny(err) + } + + return result.Tasks, nil +} + +// Notify the master that a task with given ID has completed. +func (c *client) TaskCompleted(ctx context.Context, taskID string, info TaskCompletedRequest) error { + url := c.createURLs(path.Join("/_api/task", taskID, "completed"), nil) + + req, err := c.newRequests("PUT", url, info) + if err != nil { + return maskAny(err) + } + if err := c.do(ctx, req, nil); err != nil { + return maskAny(err) + } + + return nil +} diff --git a/deps/github.com/arangodb/arangosync/client/client_worker.go b/deps/github.com/arangodb/arangosync/client/client_worker.go new file mode 100644 index 000000000..44abe82aa --- /dev/null +++ b/deps/github.com/arangodb/arangosync/client/client_worker.go @@ -0,0 +1,119 @@ +// +// Copyright 2017 ArangoDB GmbH, Cologne, Germany +// +// The Programs (which include both the software and documentation) contain +// proprietary information of ArangoDB GmbH; they are provided under a license +// agreement containing restrictions on use and disclosure and are also +// protected by copyright, patent and other intellectual and industrial +// property laws. Reverse engineering, disassembly or decompilation of the +// Programs, except to the extent required to obtain interoperability with +// other independently created software or as specified by law, is prohibited. +// +// It shall be the licensee's responsibility to take all appropriate fail-safe, +// backup, redundancy, and other measures to ensure the safe use of +// applications if the Programs are used for purposes such as nuclear, +// aviation, mass transit, medical, or other inherently dangerous applications, +// and ArangoDB GmbH disclaims liability for any damages caused by such use of +// the Programs. +// +// This software is the confidential and proprietary information of ArangoDB +// GmbH. You shall not disclose such confidential and proprietary information +// and shall use it only in accordance with the terms of the license agreement +// you entered into with ArangoDB GmbH. +// +// Author Ewout Prangsma +// + +package client + +import ( + "context" + "net/url" + "path" + "time" +) + +// StartTask is called by the master to instruct the worker +// to run a task with given instructions. +func (c *client) StartTask(ctx context.Context, data StartTaskRequest) error { + url := c.createURLs("/_api/task", nil) + + req, err := c.newRequests("POST", url, data) + if err != nil { + return maskAny(err) + } + if err := c.do(ctx, req, nil); err != nil { + return maskAny(err) + } + + return nil +} + +// StopTask is called by the master to instruct the worker +// to stop all work on the given task. +func (c *client) StopTask(ctx context.Context, taskID string) error { + url := c.createURLs(path.Join("/_api/task", taskID), nil) + + req, err := c.newRequests("DELETE", url, nil) + if err != nil { + return maskAny(err) + } + if err := c.do(ctx, req, nil); err != nil { + return maskAny(err) + } + + return nil +} + +// SetDirectMQTopicToken configures the token used to access messages of a given channel. +func (c *client) SetDirectMQTopicToken(ctx context.Context, channelName, token string, tokenTTL time.Duration) error { + url := c.createURLs(path.Join("/_api/mq/direct/channel", url.PathEscape(channelName), "token"), nil) + + data := SetDirectMQTopicTokenRequest{ + Token: token, + TokenTTL: tokenTTL, + } + req, err := c.newRequests("POST", url, data) + if err != nil { + return maskAny(err) + } + if err := c.do(ctx, req, nil); err != nil { + return maskAny(err) + } + + return nil +} + +// GetDirectMQMessages return messages for a given MQ channel. +func (c *client) GetDirectMQMessages(ctx context.Context, channelName string) ([]DirectMQMessage, error) { + url := c.createURLs(path.Join("/_api/mq/direct/channel", url.PathEscape(channelName), "messages"), nil) + + var result GetDirectMQMessagesResponse + req, err := c.newRequests("GET", url, nil) + if err != nil { + return nil, maskAny(err) + } + if err := c.do(ctx, req, &result); err != nil { + return nil, maskAny(err) + } + + return result.Messages, nil +} + +// CommitDirectMQMessage removes all messages from the given channel up to an including the given offset. +func (c *client) CommitDirectMQMessage(ctx context.Context, channelName string, offset int64) error { + url := c.createURLs(path.Join("/_api/mq/direct/channel", url.PathEscape(channelName), "commit"), nil) + + data := CommitDirectMQMessageRequest{ + Offset: offset, + } + req, err := c.newRequests("POST", url, data) + if err != nil { + return maskAny(err) + } + if err := c.do(ctx, req, nil); err != nil { + return maskAny(err) + } + + return nil +} diff --git a/deps/github.com/arangodb/arangosync/client/endpoint.go b/deps/github.com/arangodb/arangosync/client/endpoint.go new file mode 100644 index 000000000..271645d81 --- /dev/null +++ b/deps/github.com/arangodb/arangosync/client/endpoint.go @@ -0,0 +1,148 @@ +// +// Copyright 2017 ArangoDB GmbH, Cologne, Germany +// +// The Programs (which include both the software and documentation) contain +// proprietary information of ArangoDB GmbH; they are provided under a license +// agreement containing restrictions on use and disclosure and are also +// protected by copyright, patent and other intellectual and industrial +// property laws. Reverse engineering, disassembly or decompilation of the +// Programs, except to the extent required to obtain interoperability with +// other independently created software or as specified by law, is prohibited. +// +// It shall be the licensee's responsibility to take all appropriate fail-safe, +// backup, redundancy, and other measures to ensure the safe use of +// applications if the Programs are used for purposes such as nuclear, +// aviation, mass transit, medical, or other inherently dangerous applications, +// and ArangoDB GmbH disclaims liability for any damages caused by such use of +// the Programs. +// +// This software is the confidential and proprietary information of ArangoDB +// GmbH. You shall not disclose such confidential and proprietary information +// and shall use it only in accordance with the terms of the license agreement +// you entered into with ArangoDB GmbH. +// +// Author Ewout Prangsma +// + +package client + +import ( + "fmt" + "net/url" + "sort" +) + +// Endpoint is a list of URL's that are considered to be off the same service. +type Endpoint []string + +// Contains returns true when x is an element of ep. +func (ep Endpoint) Contains(x string) bool { + x = normalizeSingleEndpoint(x) + for _, y := range ep { + if x == normalizeSingleEndpoint(y) { + return true + } + } + return false +} + +// IsEmpty returns true ep has no elements. +func (ep Endpoint) IsEmpty() bool { + return len(ep) == 0 +} + +// Clone returns a deep clone of the given endpoint +func (ep Endpoint) Clone() Endpoint { + return append(Endpoint{}, ep...) +} + +// Equals returns true when a and b contain +// the same elements (perhaps in different order). +func (ep Endpoint) Equals(other Endpoint) bool { + if len(ep) != len(other) { + return false + } + // Clone lists so we can sort them without affecting the original lists. + a := append([]string{}, ep.normalized()...) + b := append([]string{}, other.normalized()...) + sort.Strings(a) + sort.Strings(b) + for i, x := range a { + if x != b[i] { + return false + } + } + return true +} + +// Intersection the endpoint containing all elements included in ep and in other. +func (ep Endpoint) Intersection(other Endpoint) Endpoint { + result := make([]string, 0, len(ep)+len(other)) + for _, x := range ep { + if other.Contains(x) { + result = append(result, x) + } + } + sort.Strings(result) + return result +} + +// Validate checks all URL's, returning the first error found. +func (ep Endpoint) Validate() error { + for _, x := range ep { + if u, err := url.Parse(x); err != nil { + return maskAny(fmt.Errorf("Endpoint '%s' is invalid: %s", x, err.Error())) + } else if u.Host == "" { + return maskAny(fmt.Errorf("Endpoint '%s' is missing a host", x)) + } + } + return nil +} + +// URLs returns all endpoints as parsed URL's +func (ep Endpoint) URLs() ([]url.URL, error) { + list := make([]url.URL, 0, len(ep)) + for _, x := range ep { + u, err := url.Parse(x) + if err != nil { + return nil, maskAny(err) + } + u.Path = "" + list = append(list, *u) + } + return list, nil +} + +// Merge adds the given endpoint to the endpoint, avoiding duplicates +func (ep Endpoint) Merge(args ...string) Endpoint { + m := make(map[string]struct{}) + for _, x := range ep { + m[x] = struct{}{} + } + for _, x := range args { + m[x] = struct{}{} + } + result := make([]string, 0, len(m)) + for x := range m { + result = append(result, x) + } + sort.Strings(result) + return result +} + +// normalized returns a clone of the given endpoint that contains normalized elements +func (ep Endpoint) normalized() Endpoint { + result := make(Endpoint, len(ep)) + for i, x := range ep { + result[i] = normalizeSingleEndpoint(x) + } + return result +} + +func normalizeSingleEndpoint(ep string) string { + if u, err := url.Parse(ep); err == nil { + u.Path = "" + return u.String() + } + return ep +} diff --git a/deps/github.com/arangodb/arangosync/client/endpoint_test.go b/deps/github.com/arangodb/arangosync/client/endpoint_test.go new file mode 100644 index 000000000..93be1ecf4 --- /dev/null +++ b/deps/github.com/arangodb/arangosync/client/endpoint_test.go @@ -0,0 +1,213 @@ +// +// Copyright 2017 ArangoDB GmbH, Cologne, Germany +// +// The Programs (which include both the software and documentation) contain +// proprietary information of ArangoDB GmbH; they are provided under a license +// agreement containing restrictions on use and disclosure and are also +// protected by copyright, patent and other intellectual and industrial +// property laws. Reverse engineering, disassembly or decompilation of the +// Programs, except to the extent required to obtain interoperability with +// other independently created software or as specified by law, is prohibited. +// +// It shall be the licensee's responsibility to take all appropriate fail-safe, +// backup, redundancy, and other measures to ensure the safe use of +// applications if the Programs are used for purposes such as nuclear, +// aviation, mass transit, medical, or other inherently dangerous applications, +// and ArangoDB GmbH disclaims liability for any damages caused by such use of +// the Programs. +// +// This software is the confidential and proprietary information of ArangoDB +// GmbH. You shall not disclose such confidential and proprietary information +// and shall use it only in accordance with the terms of the license agreement +// you entered into with ArangoDB GmbH. +// +// Author Ewout Prangsma +// + +package client + +import "testing" + +func TestEndpointContains(t *testing.T) { + ep := Endpoint{"http://a", "http://b", "http://c"} + for _, x := range []string{"http://a", "http://b", "http://c", "http://a/"} { + if !ep.Contains(x) { + t.Errorf("Expected endpoint to contain '%s' but it did not", x) + } + } + for _, x := range []string{"", "http://ab", "-", "http://abc"} { + if ep.Contains(x) { + t.Errorf("Expected endpoint to not contain '%s' but it did", x) + } + } +} + +func TestEndpointIsEmpty(t *testing.T) { + ep := Endpoint{"http://a", "http://b", "http://c"} + if ep.IsEmpty() { + t.Error("Expected endpoint to be not empty, but it is") + } + ep = nil + if !ep.IsEmpty() { + t.Error("Expected endpoint to be empty, but it is not") + } + ep = Endpoint{} + if !ep.IsEmpty() { + t.Error("Expected endpoint to be empty, but it is not") + } +} + +func TestEndpointEquals(t *testing.T) { + expectEqual := []Endpoint{ + Endpoint{}, Endpoint{}, + Endpoint{}, nil, + Endpoint{"http://a"}, Endpoint{"http://a"}, + Endpoint{"http://a", "http://b"}, Endpoint{"http://b", "http://a"}, + Endpoint{"http://foo:8529"}, Endpoint{"http://foo:8529/"}, + } + for i := 0; i < len(expectEqual); i += 2 { + epa := expectEqual[i] + epb := expectEqual[i+1] + if !epa.Equals(epb) { + t.Errorf("Expected endpoint %v to be equal to %v, but it is not", epa, epb) + } + if !epb.Equals(epa) { + t.Errorf("Expected endpoint %v to be equal to %v, but it is not", epb, epa) + } + } + + expectNotEqual := []Endpoint{ + Endpoint{"http://a"}, Endpoint{}, + Endpoint{"http://z"}, nil, + Endpoint{"http://aa"}, Endpoint{"http://a"}, + Endpoint{"http://a:100"}, Endpoint{"http://a:200"}, + Endpoint{"http://a", "http://b", "http://c"}, Endpoint{"http://b", "http://a"}, + } + for i := 0; i < len(expectNotEqual); i += 2 { + epa := expectNotEqual[i] + epb := expectNotEqual[i+1] + if epa.Equals(epb) { + t.Errorf("Expected endpoint %v to be not equal to %v, but it is", epa, epb) + } + if epb.Equals(epa) { + t.Errorf("Expected endpoint %v to be not equal to %v, but it is", epb, epa) + } + } +} + +func TestEndpointClone(t *testing.T) { + tests := []Endpoint{ + Endpoint{}, + Endpoint{"http://a"}, + Endpoint{"http://a", "http://b"}, + } + for _, orig := range tests { + c := orig.Clone() + if !orig.Equals(c) { + t.Errorf("Expected endpoint %v to be equal to clone %v, but it is not", orig, c) + } + if len(c) > 0 { + c[0] = "http://modified" + if orig.Equals(c) { + t.Errorf("Expected endpoint %v to be no longer equal to clone %v, but it is", orig, c) + } + } + } +} + +func TestEndpointIntersection(t *testing.T) { + expectIntersection := []Endpoint{ + Endpoint{"http://a"}, Endpoint{"http://a"}, + Endpoint{"http://a"}, Endpoint{"http://a", "http://b"}, + Endpoint{"http://a"}, Endpoint{"http://b", "http://a"}, + Endpoint{"http://a", "http://b"}, Endpoint{"http://b", "http://foo27"}, + Endpoint{"http://foo:8529"}, Endpoint{"http://foo:8529/"}, + } + for i := 0; i < len(expectIntersection); i += 2 { + epa := expectIntersection[i] + epb := expectIntersection[i+1] + if len(epa.Intersection(epb)) == 0 { + t.Errorf("Expected endpoint %v to have an intersection with %v, but it does not", epa, epb) + } + if len(epb.Intersection(epa)) == 0 { + t.Errorf("Expected endpoint %v to have an intersection with %v, but it does not", epb, epa) + } + } + + expectNoIntersection := []Endpoint{ + Endpoint{"http://a"}, Endpoint{}, + Endpoint{"http://z"}, nil, + Endpoint{"http://aa"}, Endpoint{"http://a"}, + Endpoint{"http://a", "http://b", "http://c"}, Endpoint{"http://e", "http://f"}, + } + for i := 0; i < len(expectNoIntersection); i += 2 { + epa := expectNoIntersection[i] + epb := expectNoIntersection[i+1] + if len(epa.Intersection(epb)) > 0 { + t.Errorf("Expected endpoint %v to have no intersection with %v, but it does", epa, epb) + } + if len(epb.Intersection(epa)) > 0 { + t.Errorf("Expected endpoint %v to havenoan intersection with %v, but it does", epb, epa) + } + } +} + +func TestEndpointValidate(t *testing.T) { + validTests := []Endpoint{ + Endpoint{}, + Endpoint{"http://a"}, + Endpoint{"http://a", "http://b"}, + } + for _, x := range validTests { + if err := x.Validate(); err != nil { + t.Errorf("Expected endpoint %v to be valid, but it is not because %s", x, err) + } + } + invalidTests := []Endpoint{ + Endpoint{":http::foo"}, + Endpoint{"http/a"}, + Endpoint{"http??"}, + Endpoint{"http:/"}, + Endpoint{"http:/foo"}, + } + for _, x := range invalidTests { + if err := x.Validate(); err == nil { + t.Errorf("Expected endpoint %v to be not valid, but it is", x) + } + } +} + +func TestEndpointURLs(t *testing.T) { + ep := Endpoint{"http://a", "http://b/rel"} + expected := []string{"http://a", "http://b"} + list, err := ep.URLs() + if err != nil { + t.Errorf("URLs expected to succeed, but got %s", err) + } else { + for i, x := range list { + found := x.String() + if found != expected[i] { + t.Errorf("Unexpected URL at index %d of %v, expected '%s', got '%s'", i, ep, expected[i], found) + } + } + } +} + +func TestEndpointMerge(t *testing.T) { + tests := []Endpoint{ + Endpoint{"http://a"}, Endpoint{}, Endpoint{"http://a"}, + Endpoint{"http://z"}, nil, Endpoint{"http://z"}, + Endpoint{"http://aa"}, Endpoint{"http://a"}, Endpoint{"http://aa", "http://a"}, + Endpoint{"http://a", "http://b", "http://c"}, Endpoint{"http://e", "http://f"}, Endpoint{"http://a", "http://b", "http://c", "http://e", "http://f"}, + Endpoint{"http://a", "http://b", "http://c"}, Endpoint{"http://a", "http://f"}, Endpoint{"http://a", "http://b", "http://c", "http://f"}, + } + for i := 0; i < len(tests); i += 3 { + epa := tests[i] + epb := tests[i+1] + expected := tests[i+2] + result := epa.Merge(epb...) + if !result.Equals(expected) { + t.Errorf("Expected merge of endpoints %v & %v to be %v, but got %v", epa, epb, expected, result) + } + } +} diff --git a/deps/github.com/arangodb/arangosync/client/error.go b/deps/github.com/arangodb/arangosync/client/error.go new file mode 100644 index 000000000..038ef489c --- /dev/null +++ b/deps/github.com/arangodb/arangosync/client/error.go @@ -0,0 +1,190 @@ +// +// Copyright 2017 ArangoDB GmbH, Cologne, Germany +// +// The Programs (which include both the software and documentation) contain +// proprietary information of ArangoDB GmbH; they are provided under a license +// agreement containing restrictions on use and disclosure and are also +// protected by copyright, patent and other intellectual and industrial +// property laws. Reverse engineering, disassembly or decompilation of the +// Programs, except to the extent required to obtain interoperability with +// other independently created software or as specified by law, is prohibited. +// +// It shall be the licensee's responsibility to take all appropriate fail-safe, +// backup, redundancy, and other measures to ensure the safe use of +// applications if the Programs are used for purposes such as nuclear, +// aviation, mass transit, medical, or other inherently dangerous applications, +// and ArangoDB GmbH disclaims liability for any damages caused by such use of +// the Programs. +// +// This software is the confidential and proprietary information of ArangoDB +// GmbH. You shall not disclose such confidential and proprietary information +// and shall use it only in accordance with the terms of the license agreement +// you entered into with ArangoDB GmbH. +// +// Author Ewout Prangsma +// + +package client + +import ( + "context" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + + "github.com/pkg/errors" + + "github.com/arangodb/arangosync/pkg/retry" +) + +var ( + maskAny = errors.WithStack + // NotFoundError indicates that an object does not exist. + NotFoundError = StatusError{StatusCode: http.StatusNotFound, message: "not found"} + // ServiceUnavailableError indicates that right now the service is not available, please retry later. + ServiceUnavailableError = StatusError{StatusCode: http.StatusServiceUnavailable, message: "service unavailable"} + // BadRequestError indicates invalid arguments. + BadRequestError = StatusError{StatusCode: http.StatusBadRequest, message: "bad request"} + // PreconditionFailedError indicates that the state of the system is such that the request cannot be executed. + PreconditionFailedError = StatusError{StatusCode: http.StatusPreconditionFailed, message: "precondition failed"} + // InternalServerError indicates an unspecified error inside the server, perhaps a bug. + InternalServerError = StatusError{StatusCode: http.StatusInternalServerError, message: "internal server error"} + // UnauthorizedError indicates that the request has not the correct authorization. + UnauthorizedError = StatusError{StatusCode: http.StatusUnauthorized, message: "unauthorized"} + // RequestTimeoutError indicates that the request is taken longer than we're prepared to wait. + RequestTimeoutError = StatusError{StatusCode: http.StatusRequestTimeout, message: "request timeout"} +) + +type StatusError struct { + StatusCode int + message string +} + +func (e StatusError) Error() string { + if e.message != "" { + return e.message + } + return fmt.Sprintf("Status %d", e.StatusCode) +} + +// IsStatusError returns the status code and true +// if the given error is caused by a StatusError. +func IsStatusError(err error) (int, bool) { + err = errors.Cause(err) + if serr, ok := err.(StatusError); ok { + return serr.StatusCode, true + } + return 0, false +} + +// IsStatusErrorWithCode returns true if the given error is caused +// by a StatusError with given code. +func IsStatusErrorWithCode(err error, code int) bool { + err = errors.Cause(err) + if serr, ok := err.(StatusError); ok { + return serr.StatusCode == code + } + return false +} + +type ErrorResponse struct { + Error string +} + +type RedirectToError struct { + Location string +} + +func (e RedirectToError) Error() string { + return fmt.Sprintf("Redirect to: %s", e.Location) +} + +// IsRedirectTo returns true when the given error is caused by an +// RedirectToError. If so, it also returns the redirect location. +func IsRedirectTo(err error) (string, bool) { + err = errors.Cause(err) + if rterr, ok := err.(RedirectToError); ok { + return rterr.Location, true + } + return "", false +} + +// IsNotFound returns true if the given error is caused by a NotFoundError. +func IsNotFound(err error) bool { + return IsStatusErrorWithCode(err, http.StatusNotFound) +} + +// IsServiceUnavailable returns true if the given error is caused by a ServiceUnavailableError. +func IsServiceUnavailable(err error) bool { + return IsStatusErrorWithCode(err, http.StatusServiceUnavailable) +} + +// IsBadRequest returns true if the given error is caused by a BadRequestError. +func IsBadRequest(err error) bool { + return IsStatusErrorWithCode(err, http.StatusBadRequest) +} + +// IsPreconditionFailed returns true if the given error is caused by a PreconditionFailedError. +func IsPreconditionFailed(err error) bool { + return IsStatusErrorWithCode(err, http.StatusPreconditionFailed) +} + +// IsInternalServer returns true if the given error is caused by a InternalServerError. +func IsInternalServer(err error) bool { + return IsStatusErrorWithCode(err, http.StatusInternalServerError) +} + +// IsUnauthorized returns true if the given error is caused by a UnauthorizedError. +func IsUnauthorized(err error) bool { + return IsStatusErrorWithCode(err, http.StatusUnauthorized) +} + +// IsRequestTimeout returns true if the given error is caused by a RequestTimeoutError. +func IsRequestTimeout(err error) bool { + return IsStatusErrorWithCode(err, http.StatusRequestTimeout) +} + +// IsCanceled returns true if the given error is caused by a context.Canceled. +func IsCanceled(err error) bool { + return errors.Cause(err) == context.Canceled +} + +// ParseResponseError returns an error from given response. +// It tries to parse the body (if given body is nil, will be read from response) +// for ErrorResponse. +func ParseResponseError(r *http.Response, body []byte) error { + // Read body (if needed) + if body == nil { + defer r.Body.Close() + body, _ = ioutil.ReadAll(r.Body) + } + return parseResponseError(body, r.StatusCode) +} + +// parseResponseError returns an error from given response. +// It tries to parse the body (if given body is nil, will be read from response) +// for ErrorResponse. +func parseResponseError(body []byte, statusCode int) error { + // Parse body (if available) + var result error + if len(body) > 0 { + var errRes ErrorResponse + if err := json.Unmarshal(body, &errRes); err == nil { + // Found ErrorResponse + result = StatusError{StatusCode: statusCode, message: errRes.Error} + } + } + + if result == nil { + // No ErrorResponse found, fallback to default message + result = StatusError{StatusCode: statusCode} + } + + // Is permanent error? + if statusCode >= 400 && statusCode < 500 { + result = retry.Permanent(result) + } + + return result +} diff --git a/deps/github.com/arangodb/arangosync/client/http.go b/deps/github.com/arangodb/arangosync/client/http.go new file mode 100644 index 000000000..146108ea6 --- /dev/null +++ b/deps/github.com/arangodb/arangosync/client/http.go @@ -0,0 +1,64 @@ +// +// Copyright 2017 ArangoDB GmbH, Cologne, Germany +// +// The Programs (which include both the software and documentation) contain +// proprietary information of ArangoDB GmbH; they are provided under a license +// agreement containing restrictions on use and disclosure and are also +// protected by copyright, patent and other intellectual and industrial +// property laws. Reverse engineering, disassembly or decompilation of the +// Programs, except to the extent required to obtain interoperability with +// other independently created software or as specified by law, is prohibited. +// +// It shall be the licensee's responsibility to take all appropriate fail-safe, +// backup, redundancy, and other measures to ensure the safe use of +// applications if the Programs are used for purposes such as nuclear, +// aviation, mass transit, medical, or other inherently dangerous applications, +// and ArangoDB GmbH disclaims liability for any damages caused by such use of +// the Programs. +// +// This software is the confidential and proprietary information of ArangoDB +// GmbH. You shall not disclose such confidential and proprietary information +// and shall use it only in accordance with the terms of the license agreement +// you entered into with ArangoDB GmbH. +// +// Author Ewout Prangsma +// + +package client + +import ( + "crypto/tls" + "net" + "net/http" + "time" +) + +const ( + defaultHTTPTimeout = time.Minute * 2 +) + +// DefaultHTTPClient creates a new HTTP client configured for accessing a starter. +func DefaultHTTPClient(tlsConfig *tls.Config) *http.Client { + if tlsConfig == nil { + tlsConfig = &tls.Config{ + InsecureSkipVerify: true, + } + } + return &http.Client{ + Timeout: defaultHTTPTimeout, + Transport: &http.Transport{ + Proxy: http.ProxyFromEnvironment, + DialContext: (&net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + DualStack: true, + }).DialContext, + MaxIdleConns: 100, + MaxIdleConnsPerHost: 10, + IdleConnTimeout: 90 * time.Second, + TLSHandshakeTimeout: 90 * time.Second, + TLSClientConfig: tlsConfig, + ExpectContinueTimeout: 1 * time.Second, + }, + } +} diff --git a/deps/github.com/arangodb/arangosync/pkg/errors/aggregate_error.go b/deps/github.com/arangodb/arangosync/pkg/errors/aggregate_error.go new file mode 100644 index 000000000..11b4a23a3 --- /dev/null +++ b/deps/github.com/arangodb/arangosync/pkg/errors/aggregate_error.go @@ -0,0 +1,63 @@ +// +// Copyright 2017 ArangoDB GmbH, Cologne, Germany +// +// The Programs (which include both the software and documentation) contain +// proprietary information of ArangoDB GmbH; they are provided under a license +// agreement containing restrictions on use and disclosure and are also +// protected by copyright, patent and other intellectual and industrial +// property laws. Reverse engineering, disassembly or decompilation of the +// Programs, except to the extent required to obtain interoperability with +// other independently created software or as specified by law, is prohibited. +// +// It shall be the licensee's responsibility to take all appropriate fail-safe, +// backup, redundancy, and other measures to ensure the safe use of +// applications if the Programs are used for purposes such as nuclear, +// aviation, mass transit, medical, or other inherently dangerous applications, +// and ArangoDB GmbH disclaims liability for any damages caused by such use of +// the Programs. +// +// This software is the confidential and proprietary information of ArangoDB +// GmbH. You shall not disclose such confidential and proprietary information +// and shall use it only in accordance with the terms of the license agreement +// you entered into with ArangoDB GmbH. +// +// Author Ewout Prangsma +// + +package errors + +// AggregateError is a helper to wrap zero or more errors as a go `error`. +type AggregateError struct { + errors []error +} + +// Add returns a new error with given error added. +func (ae AggregateError) Add(e error) AggregateError { + return AggregateError{ + errors: append(ae.errors, e), + } +} + +func (ae AggregateError) Error() string { + switch len(ae.errors) { + case 0: + return "no errors" + case 1: + return ae.errors[0].Error() + default: + return ae.errors[0].Error() + ", ..." + } +} + +// AsError returns the given aggregate error if it contains 2 or more errors. +// It returns the first error if it contains exactly 1 error. +// Otherwise nil is returned. +func (ae AggregateError) AsError() error { + if len(ae.errors) == 0 { + return nil + } + if len(ae.errors) == 1 { + return ae.errors[0] + } + return ae +} diff --git a/deps/github.com/arangodb/arangosync/pkg/errors/errors.go b/deps/github.com/arangodb/arangosync/pkg/errors/errors.go new file mode 100644 index 000000000..508dce0cd --- /dev/null +++ b/deps/github.com/arangodb/arangosync/pkg/errors/errors.go @@ -0,0 +1,207 @@ +// +// Copyright 2017 ArangoDB GmbH, Cologne, Germany +// +// The Programs (which include both the software and documentation) contain +// proprietary information of ArangoDB GmbH; they are provided under a license +// agreement containing restrictions on use and disclosure and are also +// protected by copyright, patent and other intellectual and industrial +// property laws. Reverse engineering, disassembly or decompilation of the +// Programs, except to the extent required to obtain interoperability with +// other independently created software or as specified by law, is prohibited. +// +// It shall be the licensee's responsibility to take all appropriate fail-safe, +// backup, redundancy, and other measures to ensure the safe use of +// applications if the Programs are used for purposes such as nuclear, +// aviation, mass transit, medical, or other inherently dangerous applications, +// and ArangoDB GmbH disclaims liability for any damages caused by such use of +// the Programs. +// +// This software is the confidential and proprietary information of ArangoDB +// GmbH. You shall not disclose such confidential and proprietary information +// and shall use it only in accordance with the terms of the license agreement +// you entered into with ArangoDB GmbH. +// +// Author Ewout Prangsma +// + +package errors + +import ( + "context" + "fmt" + "io" + "net" + "net/url" + "os" + "syscall" + + driver "github.com/arangodb/go-driver" + errs "github.com/pkg/errors" +) + +var ( + Cause = errs.Cause + New = errs.New + WithStack = errs.WithStack + Wrap = errs.Wrap + Wrapf = errs.Wrapf +) + +// WithMessage annotates err with a new message. +// The messages of given error is hidden. +// If err is nil, WithMessage returns nil. +func WithMessage(err error, message string) error { + if err == nil { + return nil + } + return &withMessage{ + cause: err, + msg: message, + } +} + +type withMessage struct { + cause error + msg string +} + +func (w *withMessage) Error() string { return w.msg } +func (w *withMessage) Cause() error { return w.cause } + +func (w *withMessage) Format(s fmt.State, verb rune) { + switch verb { + case 'v': + if s.Flag('+') { + fmt.Fprintf(s, "%+v\n", w.Cause()) + io.WriteString(s, w.msg) + return + } + fallthrough + case 's', 'q': + io.WriteString(s, w.Error()) + } +} + +type timeout interface { + Timeout() bool +} + +// IsTimeout returns true if the given error is caused by a timeout error. +func IsTimeout(err error) bool { + if err == nil { + return false + } + if t, ok := errs.Cause(err).(timeout); ok { + return t.Timeout() + } + return false +} + +type temporary interface { + Temporary() bool +} + +// IsTemporary returns true if the given error is caused by a temporary error. +func IsTemporary(err error) bool { + if err == nil { + return false + } + if t, ok := errs.Cause(err).(temporary); ok { + return t.Temporary() + } + return false +} + +// IsEOF returns true if the given error is caused by an EOF error. +func IsEOF(err error) bool { + err = errs.Cause(err) + if err == io.EOF { + return true + } + if ok, err := libCause(err); ok { + return IsEOF(err) + } + return false +} + +// IsConnectionRefused returns true if the given error is caused by an "connection refused" error. +func IsConnectionRefused(err error) bool { + err = errs.Cause(err) + if err, ok := err.(syscall.Errno); ok { + return err == syscall.ECONNREFUSED + } + if ok, err := libCause(err); ok { + return IsConnectionRefused(err) + } + return false +} + +// IsConnectionReset returns true if the given error is caused by an "connection reset by peer" error. +func IsConnectionReset(err error) bool { + err = errs.Cause(err) + if err, ok := err.(syscall.Errno); ok { + return err == syscall.ECONNRESET + } + if ok, err := libCause(err); ok { + return IsConnectionReset(err) + } + return false +} + +// IsContextCanceled returns true if the given error is caused by a context cancelation. +func IsContextCanceled(err error) bool { + err = errs.Cause(err) + if err == context.Canceled { + return true + } + if ok, err := libCause(err); ok { + return IsContextCanceled(err) + } + return false +} + +// IsContextDeadlineExpired returns true if the given error is caused by a context deadline expiration. +func IsContextDeadlineExpired(err error) bool { + err = errs.Cause(err) + if err == context.DeadlineExceeded { + return true + } + if ok, err := libCause(err); ok { + return IsContextDeadlineExpired(err) + } + return false +} + +// IsContextCanceledOrExpired returns true if the given error is caused by a context cancelation +// or deadline expiration. +func IsContextCanceledOrExpired(err error) bool { + err = errs.Cause(err) + if err == context.Canceled || err == context.DeadlineExceeded { + return true + } + if ok, err := libCause(err); ok { + return IsContextCanceledOrExpired(err) + } + return false +} + +// libCause returns the Cause of well known go library errors. +func libCause(err error) (bool, error) { + original := err + for { + switch e := err.(type) { + case *driver.ResponseError: + err = e.Err + case *net.DNSConfigError: + err = e.Err + case *net.OpError: + err = e.Err + case *os.SyscallError: + err = e.Err + case *url.Error: + err = e.Err + default: + return err != original, err + } + } +} diff --git a/deps/github.com/arangodb/arangosync/pkg/jwt/error.go b/deps/github.com/arangodb/arangosync/pkg/jwt/error.go new file mode 100644 index 000000000..e24fff9f0 --- /dev/null +++ b/deps/github.com/arangodb/arangosync/pkg/jwt/error.go @@ -0,0 +1,33 @@ +// +// Copyright 2017 ArangoDB GmbH, Cologne, Germany +// +// The Programs (which include both the software and documentation) contain +// proprietary information of ArangoDB GmbH; they are provided under a license +// agreement containing restrictions on use and disclosure and are also +// protected by copyright, patent and other intellectual and industrial +// property laws. Reverse engineering, disassembly or decompilation of the +// Programs, except to the extent required to obtain interoperability with +// other independently created software or as specified by law, is prohibited. +// +// It shall be the licensee's responsibility to take all appropriate fail-safe, +// backup, redundancy, and other measures to ensure the safe use of +// applications if the Programs are used for purposes such as nuclear, +// aviation, mass transit, medical, or other inherently dangerous applications, +// and ArangoDB GmbH disclaims liability for any damages caused by such use of +// the Programs. +// +// This software is the confidential and proprietary information of ArangoDB +// GmbH. You shall not disclose such confidential and proprietary information +// and shall use it only in accordance with the terms of the license agreement +// you entered into with ArangoDB GmbH. +// +// Author Ewout Prangsma +// + +package jwt + +import "github.com/pkg/errors" + +var ( + maskAny = errors.WithStack +) diff --git a/deps/github.com/arangodb/arangosync/pkg/jwt/jwt.go b/deps/github.com/arangodb/arangosync/pkg/jwt/jwt.go new file mode 100644 index 000000000..83c518b63 --- /dev/null +++ b/deps/github.com/arangodb/arangosync/pkg/jwt/jwt.go @@ -0,0 +1,137 @@ +// +// Copyright 2017 ArangoDB GmbH, Cologne, Germany +// +// The Programs (which include both the software and documentation) contain +// proprietary information of ArangoDB GmbH; they are provided under a license +// agreement containing restrictions on use and disclosure and are also +// protected by copyright, patent and other intellectual and industrial +// property laws. Reverse engineering, disassembly or decompilation of the +// Programs, except to the extent required to obtain interoperability with +// other independently created software or as specified by law, is prohibited. +// +// It shall be the licensee's responsibility to take all appropriate fail-safe, +// backup, redundancy, and other measures to ensure the safe use of +// applications if the Programs are used for purposes such as nuclear, +// aviation, mass transit, medical, or other inherently dangerous applications, +// and ArangoDB GmbH disclaims liability for any damages caused by such use of +// the Programs. +// +// This software is the confidential and proprietary information of ArangoDB +// GmbH. You shall not disclose such confidential and proprietary information +// and shall use it only in accordance with the terms of the license agreement +// you entered into with ArangoDB GmbH. +// +// Author Ewout Prangsma +// + +package jwt + +import ( + "fmt" + "net/http" + "strings" + + jg "github.com/dgrijalva/jwt-go" +) + +const ( + issArangod = "arangodb" + issArangoSync = "arangosync" +) + +// AddArangodJwtHeader calculates a JWT authorization header, for authorization +// of a request to an arangod server, based on the given secret +// and adds it to the given request. +// If the secret is empty, nothing is done. +func AddArangodJwtHeader(req *http.Request, jwtSecret string) error { + if jwtSecret == "" { + return nil + } + value, err := CreateArangodJwtAuthorizationHeader(jwtSecret) + if err != nil { + return maskAny(err) + } + + req.Header.Set("Authorization", value) + return nil +} + +// CreateArangodJwtAuthorizationHeader calculates a JWT authorization header, for authorization +// of a request to an arangod server, based on the given secret. +// If the secret is empty, nothing is done. +func CreateArangodJwtAuthorizationHeader(jwtSecret string) (string, error) { + if jwtSecret == "" { + return "", nil + } + // Create a new token object, specifying signing method and the claims + // you would like it to contain. + token := jg.NewWithClaims(jg.SigningMethodHS256, jg.MapClaims{ + "iss": issArangod, + "server_id": "foo", + }) + + // Sign and get the complete encoded token as a string using the secret + signedToken, err := token.SignedString([]byte(jwtSecret)) + if err != nil { + return "", maskAny(err) + } + + return "bearer " + signedToken, nil +} + +// AddArangoSyncJwtHeader calculates a JWT authorization header, for authorization +// of a request to an arangosync server, based on the given secret +// and adds it to the given request. +// If the secret is empty, nothing is done. +func AddArangoSyncJwtHeader(req *http.Request, jwtSecret string) error { + if jwtSecret == "" { + return nil + } + // Create a new token object, specifying signing method and the claims + // you would like it to contain. + token := jg.NewWithClaims(jg.SigningMethodHS256, jg.MapClaims{ + "iss": issArangoSync, + "server_id": "foo", + }) + + // Sign and get the complete encoded token as a string using the secret + signedToken, err := token.SignedString([]byte(jwtSecret)) + if err != nil { + return maskAny(err) + } + + req.Header.Set("Authorization", "bearer "+signedToken) + return nil +} + +// VerifyArangoSyncJwtHeader verifies the bearer token in the given request with +// the given secret. +// If returns nil when verification succeed, an error if verification fails. +// If the secret is empty, nothing is done. +func VerifyArangoSyncJwtHeader(req *http.Request, jwtSecret string) error { + if jwtSecret == "" { + return nil + } + // Extract Authorization header + authHdr := strings.TrimSpace(req.Header.Get("Authorization")) + if authHdr == "" { + return maskAny(fmt.Errorf("No Authorization found")) + } + prefix := "bearer " + if !strings.HasPrefix(strings.ToLower(authHdr), prefix) { + // Missing bearer prefix + return maskAny(fmt.Errorf("No bearer prefix")) + } + tokenStr := strings.TrimSpace(authHdr[len(prefix):]) + // Parse token + claims := jg.MapClaims{ + "iss": issArangoSync, + "server_id": "foo", + } + _, err := jg.ParseWithClaims(tokenStr, claims, func(*jg.Token) (interface{}, error) { return []byte(jwtSecret), nil }) + if err != nil { + return maskAny(err) + } + + return nil +} diff --git a/deps/github.com/arangodb/arangosync/pkg/retry/retry.go b/deps/github.com/arangodb/arangosync/pkg/retry/retry.go new file mode 100644 index 000000000..8245ed552 --- /dev/null +++ b/deps/github.com/arangodb/arangosync/pkg/retry/retry.go @@ -0,0 +1,152 @@ +// +// Copyright 2017 ArangoDB GmbH, Cologne, Germany +// +// The Programs (which include both the software and documentation) contain +// proprietary information of ArangoDB GmbH; they are provided under a license +// agreement containing restrictions on use and disclosure and are also +// protected by copyright, patent and other intellectual and industrial +// property laws. Reverse engineering, disassembly or decompilation of the +// Programs, except to the extent required to obtain interoperability with +// other independently created software or as specified by law, is prohibited. +// +// It shall be the licensee's responsibility to take all appropriate fail-safe, +// backup, redundancy, and other measures to ensure the safe use of +// applications if the Programs are used for purposes such as nuclear, +// aviation, mass transit, medical, or other inherently dangerous applications, +// and ArangoDB GmbH disclaims liability for any damages caused by such use of +// the Programs. +// +// This software is the confidential and proprietary information of ArangoDB +// GmbH. You shall not disclose such confidential and proprietary information +// and shall use it only in accordance with the terms of the license agreement +// you entered into with ArangoDB GmbH. +// +// Author Ewout Prangsma +// + +package retry + +import ( + "context" + "time" + + "github.com/cenkalti/backoff" + "github.com/pkg/errors" +) + +var ( + maskAny = errors.WithStack +) + +type permanentError struct { + Err error +} + +func (e *permanentError) Error() string { + return e.Err.Error() +} + +func (e *permanentError) Cause() error { + return e.Err +} + +// Permanent makes the given error a permanent failure +// that stops the Retry loop immediately. +func Permanent(err error) error { + return &permanentError{Err: err} +} + +func isPermanent(err error) (*permanentError, bool) { + type causer interface { + Cause() error + } + + for err != nil { + if pe, ok := err.(*permanentError); ok { + return pe, true + } + cause, ok := err.(causer) + if !ok { + break + } + err = cause.Cause() + } + return nil, false +} + +// retry the given operation until it succeeds, +// has a permanent failure or times out. +func retry(ctx context.Context, op func() error, timeout time.Duration) error { + var failure error + wrappedOp := func() error { + if err := op(); err == nil { + return nil + } else { + if pe, ok := isPermanent(err); ok { + // Detected permanent error + failure = pe.Err + return nil + } else { + return err + } + } + } + + eb := backoff.NewExponentialBackOff() + eb.MaxElapsedTime = timeout + eb.MaxInterval = timeout / 3 + + var b backoff.BackOff + if ctx != nil { + b = backoff.WithContext(eb, ctx) + } else { + b = eb + } + + if err := backoff.Retry(wrappedOp, b); err != nil { + return maskAny(err) + } + if failure != nil { + return maskAny(failure) + } + return nil +} + +// Retry the given operation until it succeeds, +// has a permanent failure or times out. +func Retry(op func() error, timeout time.Duration) error { + return retry(nil, op, timeout) +} + +// RetryWithContext retries the given operation until it succeeds, +// has a permanent failure or times out. +// The timeout is the minimum between the timeout of the context and the given timeout. +// The context given to the operation will have a timeout of a percentage of the overall timeout. +// The percentage is calculated from the given minimum number of attempts. +// If the given minimum number of attempts is 3, the timeout of each `op` call if the overall timeout / 3. +// The default minimum number of attempts is 2. +func RetryWithContext(ctx context.Context, op func(ctx context.Context) error, timeout time.Duration, minAttempts ...int) error { + deadline, ok := ctx.Deadline() + if ok { + ctxTimeout := time.Until(deadline) + if ctxTimeout < timeout { + timeout = ctxTimeout + } + } + divider := 2 + if len(minAttempts) == 1 { + divider = minAttempts[0] + } + ctxOp := func() error { + lctx, cancel := context.WithTimeout(ctx, timeout/time.Duration(divider)) + defer cancel() + if err := op(lctx); err != nil { + return maskAny(err) + } + return nil + } + if err := retry(ctx, ctxOp, timeout); err != nil { + return maskAny(err) + } + return nil +} diff --git a/deps/github.com/arangodb/arangosync/tasks/task.go b/deps/github.com/arangodb/arangosync/tasks/task.go new file mode 100644 index 000000000..92e1632ad --- /dev/null +++ b/deps/github.com/arangodb/arangosync/tasks/task.go @@ -0,0 +1,153 @@ +// +// Copyright 2017 ArangoDB GmbH, Cologne, Germany +// +// The Programs (which include both the software and documentation) contain +// proprietary information of ArangoDB GmbH; they are provided under a license +// agreement containing restrictions on use and disclosure and are also +// protected by copyright, patent and other intellectual and industrial +// property laws. Reverse engineering, disassembly or decompilation of the +// Programs, except to the extent required to obtain interoperability with +// other independently created software or as specified by law, is prohibited. +// +// It shall be the licensee's responsibility to take all appropriate fail-safe, +// backup, redundancy, and other measures to ensure the safe use of +// applications if the Programs are used for purposes such as nuclear, +// aviation, mass transit, medical, or other inherently dangerous applications, +// and ArangoDB GmbH disclaims liability for any damages caused by such use of +// the Programs. +// +// This software is the confidential and proprietary information of ArangoDB +// GmbH. You shall not disclose such confidential and proprietary information +// and shall use it only in accordance with the terms of the license agreement +// you entered into with ArangoDB GmbH. +// +// Author Ewout Prangsma +// + +package tasks + +import ( + "context" + "reflect" + "time" +) + +// TaskData contains persistent data of a task. +// This data is stored as JSON object in the agency. +type TaskData struct { + // Type of task + Type TaskType `json:"type"` + // If Persistent is set, this task should be re-assigned to another worker when + // the worker, that the task was assigned to, is unregistered (or expires). + Persistent bool `json:"persistent,omitempty"` + // Channels contains names of MQ channels used for this task + Channels struct { + // Data channel is used to send data messages from sync source to sync target + Data string `json:"data,omitempty"` + // Control channel is used to send control messages from sync target to sync source. + Control string `json:"control,omitempty"` + } `json:"channels"` + // If set, contains the ID of the remote cluster this task is targeting + TargetID string `json:"target_id,omitempty"` + // If set, contains the name of the database this task is working on. + Database string `json:"database,omitempty"` + // If set, contains the name of the collection this task is working on. + Collection string `json:"collection,omitempty"` + // If set, contains the index of the shard this task is working on. + ShardIndex int `json:"shardIndex,omitempty"` +} + +// IsShardSpecific returns true when the task is intended to operate on a specific +// shard. +func (t TaskData) IsShardSpecific() bool { + return t.Database != "" && t.Collection != "" +} + +// Equals returns true when both TaskData's are identical. +func (t TaskData) Equals(other TaskData) bool { + return reflect.DeepEqual(t, other) +} + +// TaskType is a type of task. +// Values are hardcoded and should not be changed. +type TaskType string + +const ( + // TaskTypeSendInventory is a task type that sends inventory updates to the sync target. + TaskTypeSendInventory TaskType = "send-inventory" + // TaskTypeReceiveInventory is a task type that received inventory updates from the sync source and updates the local + // structure accordingly. + TaskTypeReceiveInventory TaskType = "receive-inventory" + // TaskTypeSendShard is a task type that sends synchronization updates to the sync target for a specific shard. + TaskTypeSendShard TaskType = "send-shard" + // TaskTypeReceiveShard is a task type that received synchronization updates from the sync source for a specific shard. + TaskTypeReceiveShard TaskType = "receive-shard" +) + +func (t TaskType) String() string { + return string(t) +} + +// TaskWorker is a generic interface for the implementation of a task. +type TaskWorker interface { + // Run the task. + // Do not return until completion or a fatal error occurs + Run() error + + // Stop the task. + // If waitUntilFinished is set, do not return until the task has been stopped. + Stop(waitUntilFinished bool) error + + // Update the message timeout of this task. + // This timeout is the maximum time between messages + // in a task channel. + // If no messages have been received within the + // message timeout period, the channel is considered + // broken. + // If is up to the task implementation to cope + // with a broken channel. + SetMessageTimeout(timeout time.Duration) + + // Returns true if this task does not have a valid shard master, but does need it. + HasUnknownShardMaster() bool + + // RenewTokens is called once every 5 minutes. The task worker is expected to renew all + // authentication tokens it needs. + RenewTokens(ctx context.Context) error +} + +// TLSClientAuthentication contains configuration for using client certificates or client tokens. +type TLSClientAuthentication struct { + // Client certificate used to authenticate myself. + ClientCertificate string `json:"clientCertificate"` + // Private key of client certificate used to authentication. + ClientKey string `json:"clientKey"` + // Client token used to authenticate myself. + ClientToken string `json:"clientToken"` +} + +// String returns a string representation of the given object. +func (a TLSClientAuthentication) String() string { + return a.ClientCertificate + "/" + a.ClientKey + "/" + a.ClientToken +} + +// TLSAuthentication contains configuration for using client certificates +// and TLS verification of the server. +type TLSAuthentication struct { + TLSClientAuthentication + // CA certificate used to sign the TLS connection of the server. + // This is used for verifying the server. + CACertificate string `json:"caCertificate"` +} + +// String returns a string representation of the given object. +func (a TLSAuthentication) String() string { + return a.TLSClientAuthentication.String() + "/" + a.CACertificate +} + +// MessageQueueConfig contains all deployment configuration info for a MQ. +type MessageQueueConfig struct { + Type string `json:"type"` + Endpoints []string `json:"endpoints"` + Authentication TLSAuthentication `json:"authentication"` +} diff --git a/main.go b/main.go index 56b276fb7..d5155f67b 100644 --- a/main.go +++ b/main.go @@ -160,6 +160,7 @@ func cmdMainRun(cmd *cobra.Command, args []string) { mux := http.NewServeMux() mux.HandleFunc("/health", livenessProbe.LivenessHandler) mux.HandleFunc("/ready/deployment", deploymentProbe.ReadyHandler) + mux.HandleFunc("/ready/deployment-replications", deploymentReplicationProbe.ReadyHandler) mux.HandleFunc("/ready/storage", storageProbe.ReadyHandler) mux.Handle("/metrics", prometheus.Handler()) listenAddr := net.JoinHostPort(serverOptions.host, strconv.Itoa(serverOptions.port)) diff --git a/manifests/arango-deployment-replication-dev.yaml b/manifests/arango-deployment-replication-dev.yaml index a95878fb8..fab2e22e2 100644 --- a/manifests/arango-deployment-replication-dev.yaml +++ b/manifests/arango-deployment-replication-dev.yaml @@ -94,7 +94,7 @@ spec: containers: - name: operator imagePullPolicy: IfNotPresent - image: ewoutp/kube-arangodb@sha256:ee3c19a789b7ac9e4e606617da643a7a54a2dc6d398db7a573f2120d77a45258 + image: ewoutp/kube-arangodb@sha256:fd5b99fb3637b25f9b09b5c6dc08a2b282a9cf0624cda01db9bf2939143513e2 args: - --operator.deployment-replication env: @@ -122,7 +122,7 @@ spec: periodSeconds: 10 readinessProbe: httpGet: - path: /ready/deploymentReplication + path: /ready/deployment-replication port: 8528 scheme: HTTPS initialDelaySeconds: 5 diff --git a/manifests/crd.yaml b/manifests/crd.yaml index 169262939..7cce03231 100644 --- a/manifests/crd.yaml +++ b/manifests/crd.yaml @@ -17,6 +17,24 @@ spec: --- +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: arangodeploymentreplications.replication.database.arangodb.com +spec: + group: replication.database.arangodb.com + names: + kind: ArangoDeploymentReplication + listKind: ArangoDeploymentReplicationList + plural: arangodeploymentreplications + shortNames: + - arangorepl + singular: arangodeploymentreplication + scope: Namespaced + version: v1alpha + +--- + apiVersion: apiextensions.k8s.io/v1beta1 kind: CustomResourceDefinition metadata: diff --git a/manifests/templates/deployment-replication/deployment-replication.yaml b/manifests/templates/deployment-replication/deployment-replication.yaml index debb91d3d..14a0c9e7c 100644 --- a/manifests/templates/deployment-replication/deployment-replication.yaml +++ b/manifests/templates/deployment-replication/deployment-replication.yaml @@ -45,7 +45,7 @@ spec: periodSeconds: 10 readinessProbe: httpGet: - path: /ready/deploymentReplication + path: /ready/deployment-replication port: 8528 scheme: HTTPS initialDelaySeconds: 5 diff --git a/pkg/apis/replication/v1alpha/authentication_spec.go b/pkg/apis/replication/v1alpha/authentication_spec.go new file mode 100644 index 000000000..3d85b8c77 --- /dev/null +++ b/pkg/apis/replication/v1alpha/authentication_spec.go @@ -0,0 +1,68 @@ +// +// DISCLAIMER +// +// Copyright 2018 ArangoDB GmbH, Cologne, Germany +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// +// Author Ewout Prangsma +// + +package v1alpha + +import ( + "github.com/arangodb/kube-arangodb/pkg/util" + "github.com/arangodb/kube-arangodb/pkg/util/k8sutil" +) + +// AuthenticationSpec contains the specification to authenticate the destination syncmaster +// with the source syncmasters. +type AuthenticationSpec struct { + // ClientAuthSecretName holds the name of a Secret containing a client authentication keyfile. + ClientAuthSecretName *string `json:"clientAuthSecretName,omitempty"` +} + +// GetClientAuthSecretName returns the value of clientAuthSecretName. +func (s AuthenticationSpec) GetClientAuthSecretName() string { + return util.StringOrDefault(s.ClientAuthSecretName) +} + +// Validate the given spec, returning an error on validation +// problems or nil if all ok. +func (s AuthenticationSpec) Validate() error { + if err := k8sutil.ValidateResourceName(s.GetClientAuthSecretName()); err != nil { + return maskAny(err) + } + return nil +} + +// SetDefaults fills empty field with default values. +func (s *AuthenticationSpec) SetDefaults() { +} + +// SetDefaultsFrom fills empty field with default values from the given source. +func (s *AuthenticationSpec) SetDefaultsFrom(source AuthenticationSpec) { + if s.ClientAuthSecretName == nil { + s.ClientAuthSecretName = util.NewStringOrNil(source.ClientAuthSecretName) + } +} + +// ResetImmutableFields replaces all immutable fields in the given target with values from the source spec. +// It returns a list of fields that have been reset. +// Field names are relative to `spec.`. +func (s AuthenticationSpec) ResetImmutableFields(target *AuthenticationSpec, fieldPrefix string) []string { + var result []string + return result +} diff --git a/pkg/apis/replication/v1alpha/conditions.go b/pkg/apis/replication/v1alpha/conditions.go new file mode 100644 index 000000000..86eeea935 --- /dev/null +++ b/pkg/apis/replication/v1alpha/conditions.go @@ -0,0 +1,131 @@ +// +// DISCLAIMER +// +// Copyright 2018 ArangoDB GmbH, Cologne, Germany +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// +// Author Ewout Prangsma +// + +package v1alpha + +import ( + "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// ConditionType is a strongly typed condition name +type ConditionType string + +const ( + // ConditionTypeConfigured indicates that the replication has been configured. + ConditionTypeConfigured ConditionType = "Configured" +) + +// Condition represents one current condition of a deployment or deployment member. +// A condition might not show up if it is not happening. +// For example, if a cluster is not upgrading, the Upgrading condition would not show up. +type Condition struct { + // Type of condition. + Type ConditionType `json:"type"` + // Status of the condition, one of True, False, Unknown. + Status v1.ConditionStatus `json:"status"` + // The last time this condition was updated. + LastUpdateTime metav1.Time `json:"lastUpdateTime,omitempty"` + // Last time the condition transitioned from one status to another. + LastTransitionTime metav1.Time `json:"lastTransitionTime,omitempty"` + // The reason for the condition's last transition. + Reason string `json:"reason,omitempty"` + // A human readable message indicating details about the transition. + Message string `json:"message,omitempty"` +} + +// ConditionList is a list of conditions. +// Each type is allowed only once. +type ConditionList []Condition + +// IsTrue return true when a condition with given type exists and its status is `True`. +func (list ConditionList) IsTrue(conditionType ConditionType) bool { + c, found := list.Get(conditionType) + return found && c.Status == v1.ConditionTrue +} + +// Get a condition by type. +// Returns true if found, false if not found. +func (list ConditionList) Get(conditionType ConditionType) (Condition, bool) { + for _, x := range list { + if x.Type == conditionType { + return x, true + } + } + // Not found + return Condition{}, false +} + +// Update the condition, replacing an old condition with same type (if any) +// Returns true when changes were made, false otherwise. +func (list *ConditionList) Update(conditionType ConditionType, status bool, reason, message string) bool { + src := *list + statusX := v1.ConditionFalse + if status { + statusX = v1.ConditionTrue + } + for i, x := range src { + if x.Type == conditionType { + if x.Status != statusX { + // Transition to another status + src[i].Status = statusX + now := metav1.Now() + src[i].LastTransitionTime = now + src[i].LastUpdateTime = now + src[i].Reason = reason + src[i].Message = message + } else if x.Reason != reason || x.Message != message { + src[i].LastUpdateTime = metav1.Now() + src[i].Reason = reason + src[i].Message = message + } else { + return false + } + return true + } + } + // Not found + now := metav1.Now() + *list = append(src, Condition{ + Type: conditionType, + LastUpdateTime: now, + LastTransitionTime: now, + Status: statusX, + Reason: reason, + Message: message, + }) + return true +} + +// Remove the condition with given type. +// Returns true if removed, or false if not found. +func (list *ConditionList) Remove(conditionType ConditionType) bool { + src := *list + for i, x := range src { + if x.Type == conditionType { + *list = append(src[:i], src[i+1:]...) + return true + } + } + // Not found + return false +} diff --git a/pkg/apis/replication/v1alpha/conditions_test.go b/pkg/apis/replication/v1alpha/conditions_test.go new file mode 100644 index 000000000..26dcbdbc0 --- /dev/null +++ b/pkg/apis/replication/v1alpha/conditions_test.go @@ -0,0 +1,95 @@ +// +// DISCLAIMER +// +// Copyright 2018 ArangoDB GmbH, Cologne, Germany +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// +// Author Ewout Prangsma +// + +package v1alpha + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestConditionListIsTrue(t *testing.T) { + assert.False(t, ConditionList{}.IsTrue(ConditionTypeConfigured)) + + cl := ConditionList{} + cl.Update(ConditionTypeConfigured, true, "test", "msg") + assert.True(t, cl.IsTrue(ConditionTypeConfigured)) + //assert.False(t, cl.IsTrue(ConditionTypeTerminated)) + + cl.Update(ConditionTypeConfigured, false, "test", "msg") + assert.False(t, cl.IsTrue(ConditionTypeConfigured)) + + cl.Remove(ConditionTypeConfigured) + assert.False(t, cl.IsTrue(ConditionTypeConfigured)) + assert.Equal(t, 0, len(cl)) +} + +func TestConditionListGet(t *testing.T) { + conv := func(c Condition, b bool) []interface{} { + return []interface{}{c, b} + } + + cl := ConditionList{} + assert.EqualValues(t, conv(Condition{}, false), conv(cl.Get(ConditionTypeConfigured))) + cl.Update(ConditionTypeConfigured, false, "test", "msg") + assert.EqualValues(t, conv(cl[0], true), conv(cl.Get(ConditionTypeConfigured))) +} + +func TestConditionListUpdate(t *testing.T) { + cl := ConditionList{} + assert.Equal(t, 0, len(cl)) + + assert.True(t, cl.Update(ConditionTypeConfigured, true, "test", "msg")) + assert.True(t, cl.IsTrue(ConditionTypeConfigured)) + assert.Equal(t, 1, len(cl)) + + assert.False(t, cl.Update(ConditionTypeConfigured, true, "test", "msg")) + assert.True(t, cl.IsTrue(ConditionTypeConfigured)) + assert.Equal(t, 1, len(cl)) + + assert.True(t, cl.Update(ConditionTypeConfigured, false, "test", "msg")) + assert.False(t, cl.IsTrue(ConditionTypeConfigured)) + assert.Equal(t, 1, len(cl)) + + assert.True(t, cl.Update(ConditionTypeConfigured, false, "test2", "msg")) + assert.False(t, cl.IsTrue(ConditionTypeConfigured)) + assert.Equal(t, 1, len(cl)) + + assert.True(t, cl.Update(ConditionTypeConfigured, false, "test2", "msg2")) + assert.False(t, cl.IsTrue(ConditionTypeConfigured)) + assert.Equal(t, 1, len(cl)) +} + +func TestConditionListRemove(t *testing.T) { + cl := ConditionList{} + assert.Equal(t, 0, len(cl)) + + cl.Update(ConditionTypeConfigured, true, "test", "msg") + assert.Equal(t, 1, len(cl)) + + assert.True(t, cl.Remove(ConditionTypeConfigured)) + assert.Equal(t, 0, len(cl)) + + assert.False(t, cl.Remove(ConditionTypeConfigured)) + assert.Equal(t, 0, len(cl)) +} diff --git a/pkg/apis/replication/v1alpha/endpoint_authentication_spec.go b/pkg/apis/replication/v1alpha/endpoint_authentication_spec.go new file mode 100644 index 000000000..d51119234 --- /dev/null +++ b/pkg/apis/replication/v1alpha/endpoint_authentication_spec.go @@ -0,0 +1,68 @@ +// +// DISCLAIMER +// +// Copyright 2018 ArangoDB GmbH, Cologne, Germany +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// +// Author Ewout Prangsma +// + +package v1alpha + +import ( + "github.com/arangodb/kube-arangodb/pkg/util" + "github.com/arangodb/kube-arangodb/pkg/util/k8sutil" +) + +// EndpointAuthenticationSpec contains the specification to authentication with the syncmasters +// in either source or destination endpoint. +type EndpointAuthenticationSpec struct { + // JWTSecretName holds the name of a Secret containing a JWT token. + JWTSecretName *string `json:"jwtSecretName,omitempty"` +} + +// GetJWTSecretName returns the value of jwtSecretName. +func (s EndpointAuthenticationSpec) GetJWTSecretName() string { + return util.StringOrDefault(s.JWTSecretName) +} + +// Validate the given spec, returning an error on validation +// problems or nil if all ok. +func (s EndpointAuthenticationSpec) Validate() error { + if err := k8sutil.ValidateOptionalResourceName(s.GetJWTSecretName()); err != nil { + return maskAny(err) + } + return nil +} + +// SetDefaults fills empty field with default values. +func (s *EndpointAuthenticationSpec) SetDefaults() { +} + +// SetDefaultsFrom fills empty field with default values from the given source. +func (s *EndpointAuthenticationSpec) SetDefaultsFrom(source EndpointAuthenticationSpec) { + if s.JWTSecretName == nil { + s.JWTSecretName = util.NewStringOrNil(source.JWTSecretName) + } +} + +// ResetImmutableFields replaces all immutable fields in the given target with values from the source spec. +// It returns a list of fields that have been reset. +// Field names are relative to `spec.`. +func (s EndpointAuthenticationSpec) ResetImmutableFields(target *EndpointAuthenticationSpec, fieldPrefix string) []string { + var result []string + return result +} diff --git a/pkg/apis/replication/v1alpha/endpoint_spec.go b/pkg/apis/replication/v1alpha/endpoint_spec.go new file mode 100644 index 000000000..7b73dcd18 --- /dev/null +++ b/pkg/apis/replication/v1alpha/endpoint_spec.go @@ -0,0 +1,80 @@ +// +// DISCLAIMER +// +// Copyright 2018 ArangoDB GmbH, Cologne, Germany +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// +// Author Ewout Prangsma +// + +package v1alpha + +import ( + "net/url" + + "github.com/pkg/errors" +) + +// EndpointSpec contains the specification used to reach the syncmasters +// in either source or destination mode. +type EndpointSpec struct { + MasterEndpoint []string `json:"masterEndpoint,omitempty"` + Authentication EndpointAuthenticationSpec `json:"auth"` + TLS EndpointTLSSpec `json:"tls"` +} + +// Validate the given spec, returning an error on validation +// problems or nil if all ok. +func (s EndpointSpec) Validate() error { + for _, ep := range s.MasterEndpoint { + if _, err := url.Parse(ep); err != nil { + return maskAny(errors.Wrapf(ValidationError, "Invalid master endpoint '%s': %s", ep, err)) + } + } + if err := s.Authentication.Validate(); err != nil { + return maskAny(err) + } + if err := s.TLS.Validate(); err != nil { + return maskAny(err) + } + return nil +} + +// SetDefaults fills empty field with default values. +func (s *EndpointSpec) SetDefaults() { + s.Authentication.SetDefaults() + s.TLS.SetDefaults() +} + +// SetDefaultsFrom fills empty field with default values from the given source. +func (s *EndpointSpec) SetDefaultsFrom(source EndpointSpec) { + s.Authentication.SetDefaultsFrom(source.Authentication) + s.TLS.SetDefaultsFrom(source.TLS) +} + +// ResetImmutableFields replaces all immutable fields in the given target with values from the source spec. +// It returns a list of fields that have been reset. +// Field names are relative to `spec.`. +func (s EndpointSpec) ResetImmutableFields(target *EndpointSpec, fieldPrefix string) []string { + var result []string + if list := s.Authentication.ResetImmutableFields(&target.Authentication, fieldPrefix+"auth."); len(list) > 0 { + result = append(result, list...) + } + if list := s.TLS.ResetImmutableFields(&target.TLS, fieldPrefix+"tls."); len(list) > 0 { + result = append(result, list...) + } + return result +} diff --git a/pkg/apis/replication/v1alpha/endpoint_tls_spec.go b/pkg/apis/replication/v1alpha/endpoint_tls_spec.go new file mode 100644 index 000000000..719fe6fd0 --- /dev/null +++ b/pkg/apis/replication/v1alpha/endpoint_tls_spec.go @@ -0,0 +1,68 @@ +// +// DISCLAIMER +// +// Copyright 2018 ArangoDB GmbH, Cologne, Germany +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// +// Author Ewout Prangsma +// + +package v1alpha + +import ( + "github.com/arangodb/kube-arangodb/pkg/util" + "github.com/arangodb/kube-arangodb/pkg/util/k8sutil" +) + +// EndpointTLSSpec contains the specification regarding the TLS connection to the syncmasters +// in either source or destination endpoint. +type EndpointTLSSpec struct { + // CASecretName holds the name of a Secret containing a ca.crt public key for TLS validation. + CASecretName *string `json:"caSecretName,omitempty"` +} + +// GetCASecretName returns the value of caSecretName. +func (s EndpointTLSSpec) GetCASecretName() string { + return util.StringOrDefault(s.CASecretName) +} + +// Validate the given spec, returning an error on validation +// problems or nil if all ok. +func (s EndpointTLSSpec) Validate() error { + if err := k8sutil.ValidateOptionalResourceName(s.GetCASecretName()); err != nil { + return maskAny(err) + } + return nil +} + +// SetDefaults fills empty field with default values. +func (s *EndpointTLSSpec) SetDefaults() { +} + +// SetDefaultsFrom fills empty field with default values from the given source. +func (s *EndpointTLSSpec) SetDefaultsFrom(source EndpointTLSSpec) { + if s.CASecretName == nil { + s.CASecretName = util.NewStringOrNil(source.CASecretName) + } +} + +// ResetImmutableFields replaces all immutable fields in the given target with values from the source spec. +// It returns a list of fields that have been reset. +// Field names are relative to `spec.`. +func (s EndpointTLSSpec) ResetImmutableFields(target *EndpointTLSSpec, fieldPrefix string) []string { + var result []string + return result +} diff --git a/pkg/apis/replication/v1alpha/replication_spec.go b/pkg/apis/replication/v1alpha/replication_spec.go index 54251e22b..d9abbd220 100644 --- a/pkg/apis/replication/v1alpha/replication_spec.go +++ b/pkg/apis/replication/v1alpha/replication_spec.go @@ -25,27 +25,38 @@ package v1alpha // DeploymentReplicationSpec contains the specification part of // an ArangoDeploymentReplication. type DeploymentReplicationSpec struct { + Source EndpointSpec `json:"source"` + Destination EndpointSpec `json:"destination"` + Authentication AuthenticationSpec `json:"auth"` } // Validate the given spec, returning an error on validation // problems or nil if all ok. func (s DeploymentReplicationSpec) Validate() error { - /* if err := s.StorageClass.Validate(); err != nil { - return maskAny(err) - } - if len(s.LocalPath) == 0 { - return maskAny(errors.Wrapf(ValidationError, "localPath cannot be empty")) - } - for _, p := range s.LocalPath { - if len(p) == 0 { - return maskAny(errors.Wrapf(ValidationError, "localPath cannot contain empty strings")) - } - }*/ + if err := s.Source.Validate(); err != nil { + return maskAny(err) + } + if err := s.Destination.Validate(); err != nil { + return maskAny(err) + } + if err := s.Authentication.Validate(); err != nil { + return maskAny(err) + } return nil } // SetDefaults fills empty field with default values. func (s *DeploymentReplicationSpec) SetDefaults() { + s.Source.SetDefaults() + s.Destination.SetDefaults() + s.Authentication.SetDefaults() +} + +// SetDefaultsFrom fills empty field with default values from the given source. +func (s *DeploymentReplicationSpec) SetDefaultsFrom(source DeploymentReplicationSpec) { + s.Source.SetDefaultsFrom(source.Source) + s.Destination.SetDefaultsFrom(source.Destination) + s.Authentication.SetDefaultsFrom(source.Authentication) } // ResetImmutableFields replaces all immutable fields in the given target with values from the source spec. @@ -53,5 +64,14 @@ func (s *DeploymentReplicationSpec) SetDefaults() { // Field names are relative to `spec.`. func (s DeploymentReplicationSpec) ResetImmutableFields(target *DeploymentReplicationSpec) []string { var result []string + if list := s.Source.ResetImmutableFields(&target.Source, "source."); len(list) > 0 { + result = append(result, list...) + } + if list := s.Destination.ResetImmutableFields(&target.Destination, "destination."); len(list) > 0 { + result = append(result, list...) + } + if list := s.Authentication.ResetImmutableFields(&target.Authentication, "auth."); len(list) > 0 { + result = append(result, list...) + } return result } diff --git a/pkg/apis/replication/v1alpha/replication_state.go b/pkg/apis/replication/v1alpha/replication_status.go similarity index 90% rename from pkg/apis/replication/v1alpha/replication_state.go rename to pkg/apis/replication/v1alpha/replication_status.go index c34b113ba..cb28b5865 100644 --- a/pkg/apis/replication/v1alpha/replication_state.go +++ b/pkg/apis/replication/v1alpha/replication_status.go @@ -29,4 +29,7 @@ type DeploymentReplicationStatus struct { Phase DeploymentReplicationPhase `json:"phase"` // Reason contains a human readable reason for reaching the current phase (can be empty) Reason string `json:"reason,omitempty"` // Reason for current phase + + // Conditions specific to the entire deployment replication + Conditions ConditionList `json:"conditions,omitempty"` } diff --git a/pkg/apis/replication/v1alpha/zz_generated.deepcopy.go b/pkg/apis/replication/v1alpha/zz_generated.deepcopy.go index 8919cdbe7..9075af396 100644 --- a/pkg/apis/replication/v1alpha/zz_generated.deepcopy.go +++ b/pkg/apis/replication/v1alpha/zz_generated.deepcopy.go @@ -33,8 +33,8 @@ func (in *ArangoDeploymentReplication) DeepCopyInto(out *ArangoDeploymentReplica *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = in.Spec - out.Status = in.Status + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) return } @@ -89,9 +89,55 @@ func (in *ArangoDeploymentReplicationList) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AuthenticationSpec) DeepCopyInto(out *AuthenticationSpec) { + *out = *in + if in.ClientAuthSecretName != nil { + in, out := &in.ClientAuthSecretName, &out.ClientAuthSecretName + if *in == nil { + *out = nil + } else { + *out = new(string) + **out = **in + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AuthenticationSpec. +func (in *AuthenticationSpec) DeepCopy() *AuthenticationSpec { + if in == nil { + return nil + } + out := new(AuthenticationSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Condition) DeepCopyInto(out *Condition) { + *out = *in + in.LastUpdateTime.DeepCopyInto(&out.LastUpdateTime) + in.LastTransitionTime.DeepCopyInto(&out.LastTransitionTime) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Condition. +func (in *Condition) DeepCopy() *Condition { + if in == nil { + return nil + } + out := new(Condition) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DeploymentReplicationSpec) DeepCopyInto(out *DeploymentReplicationSpec) { *out = *in + in.Source.DeepCopyInto(&out.Source) + in.Destination.DeepCopyInto(&out.Destination) + in.Authentication.DeepCopyInto(&out.Authentication) return } @@ -108,6 +154,13 @@ func (in *DeploymentReplicationSpec) DeepCopy() *DeploymentReplicationSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DeploymentReplicationStatus) DeepCopyInto(out *DeploymentReplicationStatus) { *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make(ConditionList, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } return } @@ -120,3 +173,76 @@ func (in *DeploymentReplicationStatus) DeepCopy() *DeploymentReplicationStatus { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *EndpointAuthenticationSpec) DeepCopyInto(out *EndpointAuthenticationSpec) { + *out = *in + if in.JWTSecretName != nil { + in, out := &in.JWTSecretName, &out.JWTSecretName + if *in == nil { + *out = nil + } else { + *out = new(string) + **out = **in + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EndpointAuthenticationSpec. +func (in *EndpointAuthenticationSpec) DeepCopy() *EndpointAuthenticationSpec { + if in == nil { + return nil + } + out := new(EndpointAuthenticationSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *EndpointSpec) DeepCopyInto(out *EndpointSpec) { + *out = *in + if in.MasterEndpoint != nil { + in, out := &in.MasterEndpoint, &out.MasterEndpoint + *out = make([]string, len(*in)) + copy(*out, *in) + } + in.Authentication.DeepCopyInto(&out.Authentication) + in.TLS.DeepCopyInto(&out.TLS) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EndpointSpec. +func (in *EndpointSpec) DeepCopy() *EndpointSpec { + if in == nil { + return nil + } + out := new(EndpointSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *EndpointTLSSpec) DeepCopyInto(out *EndpointTLSSpec) { + *out = *in + if in.CASecretName != nil { + in, out := &in.CASecretName, &out.CASecretName + if *in == nil { + *out = nil + } else { + *out = new(string) + **out = **in + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EndpointTLSSpec. +func (in *EndpointTLSSpec) DeepCopy() *EndpointTLSSpec { + if in == nil { + return nil + } + out := new(EndpointTLSSpec) + in.DeepCopyInto(out) + return out +} diff --git a/pkg/operator/operator_deployment_relication.go b/pkg/operator/operator_deployment_relication.go index fef7bfc3e..bff0d9d61 100644 --- a/pkg/operator/operator_deployment_relication.go +++ b/pkg/operator/operator_deployment_relication.go @@ -130,12 +130,12 @@ func (o *Operator) syncArangoDeploymentReplication(apiObject *api.ArangoDeployme // re-watch or restart could give ADD event. // If for an ADD event the cluster spec is invalid then it is not added to the local cache // so modifying that deployment will result in another ADD event - if _, ok := o.deployments[apiObject.Name]; ok { + if _, ok := o.deploymentReplications[apiObject.Name]; ok { ev.Type = kwatch.Modified } //pt.start() - err := o.handleDeploymentEvent(ev) + err := o.handleDeploymentReplicationEvent(ev) if err != nil { o.log.Warn().Err(err).Msg("Failed to handle event") } diff --git a/pkg/replication/deployment_replication.go b/pkg/replication/deployment_replication.go index f2697c83e..5aa02cb0b 100644 --- a/pkg/replication/deployment_replication.go +++ b/pkg/replication/deployment_replication.go @@ -34,6 +34,7 @@ import ( "k8s.io/client-go/kubernetes" corev1 "k8s.io/client-go/kubernetes/typed/core/v1" + "github.com/arangodb/arangosync/client" api "github.com/arangodb/kube-arangodb/pkg/apis/replication/v1alpha" "github.com/arangodb/kube-arangodb/pkg/generated/clientset/versioned" "github.com/arangodb/kube-arangodb/pkg/util/k8sutil" @@ -44,8 +45,6 @@ import ( // Config holds configuration settings for a DeploymentReplication type Config struct { Namespace string - // PodName string - // ServiceAccount string } // Dependencies holds dependent services for a DeploymentReplication @@ -87,7 +86,9 @@ type DeploymentReplication struct { eventsCli corev1.EventInterface - inspectTrigger trigger.Trigger + inspectTrigger trigger.Trigger + recentInspectionErrors int + clientCache client.ClientCache } // New creates a new DeploymentReplication from the given API object. @@ -150,7 +151,6 @@ func (dr *DeploymentReplication) run() { //log := dr.deps.Log inspectionInterval := maxInspectionInterval - recentInspectionErrors := 0 for { select { case <-dr.stopCh: @@ -170,15 +170,7 @@ func (dr *DeploymentReplication) run() { } case <-dr.inspectTrigger.Done(): - hasError := false - if hasError { - if recentInspectionErrors == 0 { - inspectionInterval = minInspectionInterval - recentInspectionErrors++ - } - } else { - recentInspectionErrors = 0 - } + inspectionInterval = dr.inspectDeploymentReplication(inspectionInterval) case <-time.After(inspectionInterval): // Trigger inspection diff --git a/pkg/replication/sync_client.go b/pkg/replication/sync_client.go new file mode 100644 index 000000000..daa1d410e --- /dev/null +++ b/pkg/replication/sync_client.go @@ -0,0 +1,95 @@ +// +// DISCLAIMER +// +// Copyright 2018 ArangoDB GmbH, Cologne, Germany +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// +// Author Ewout Prangsma +// + +package replication + +import ( + certificates "github.com/arangodb-helper/go-certificates" + "github.com/arangodb/arangosync/client" + "github.com/arangodb/arangosync/tasks" + + api "github.com/arangodb/kube-arangodb/pkg/apis/replication/v1alpha" + "github.com/arangodb/kube-arangodb/pkg/util/k8sutil" +) + +// createSyncMasterClient creates an arangosync client for the given endpoint. +func (dr *DeploymentReplication) createSyncMasterClient(epSpec api.EndpointSpec) (client.API, error) { + log := dr.deps.Log + + // Endpoint + source := dr.createArangoSyncEndpoint(epSpec) + + // Authentication + insecureSkipVerify := true + tlsAuth := tasks.TLSAuthentication{} + jwtSecret := "" + if jwtSecretName := epSpec.Authentication.GetJWTSecretName(); jwtSecretName != "" { + var err error + jwtSecret, err = k8sutil.GetJWTSecret(dr.deps.KubeCli.CoreV1(), jwtSecretName, dr.apiObject.GetNamespace()) + if err != nil { + return nil, maskAny(err) + } + } + if caSecretName := epSpec.TLS.GetCASecretName(); caSecretName != "" { + caCert, err := k8sutil.GetCACertficateSecret(dr.deps.KubeCli.CoreV1(), caSecretName, dr.apiObject.GetNamespace()) + if err != nil { + return nil, maskAny(err) + } + tlsAuth.CACertificate = caCert + insecureSkipVerify = false + } + auth := client.NewAuthentication(tlsAuth, jwtSecret) + + // Create client + c, err := dr.clientCache.GetClient(log, source, auth, insecureSkipVerify) + if err != nil { + return nil, maskAny(err) + } + return c, nil +} + +// createArangoSyncEndpoint creates the endpoints for the given spec. +func (dr *DeploymentReplication) createArangoSyncEndpoint(epSpec api.EndpointSpec) client.Endpoint { + // TODO when adding deploymentname to EndpointSpec, reflect that here + return client.Endpoint(epSpec.MasterEndpoint) +} + +// createArangoSyncTLSAuthentication creates the authentication needed to authenticate +// the destination syncmaster at the source syncmaster. +func (dr *DeploymentReplication) createArangoSyncTLSAuthentication(spec api.AuthenticationSpec) (client.TLSAuthentication, error) { + keyFileContent, err := k8sutil.GetTLSKeyfileSecret(dr.deps.KubeCli.CoreV1(), spec.GetClientAuthSecretName(), dr.apiObject.GetNamespace()) + if err != nil { + return client.TLSAuthentication{}, maskAny(err) + } + kf, err := certificates.NewKeyfile(keyFileContent) + if err != nil { + return client.TLSAuthentication{}, maskAny(err) + } + result := client.TLSAuthentication{ + TLSClientAuthentication: tasks.TLSClientAuthentication{ + ClientCertificate: kf.EncodeCertificates(), + ClientKey: kf.EncodePrivateKey(), + }, + } + // TODO when add CA cert ???? + return result, nil +} diff --git a/pkg/replication/sync_inspector.go b/pkg/replication/sync_inspector.go new file mode 100644 index 000000000..0a67b85f6 --- /dev/null +++ b/pkg/replication/sync_inspector.go @@ -0,0 +1,190 @@ +// +// DISCLAIMER +// +// Copyright 2018 ArangoDB GmbH, Cologne, Germany +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// +// Author Ewout Prangsma +// + +package replication + +import ( + "context" + "time" + + "github.com/arangodb/arangosync/client" + api "github.com/arangodb/kube-arangodb/pkg/apis/replication/v1alpha" +) + +// inspectDeploymentReplication inspects the entire deployment replication +// and configures the replication when needed. +// This function should be called when: +// - the deployment replication has changed +// - any of the underlying resources has changed +// - once in a while +// Returns the delay until this function should be called again. +func (dr *DeploymentReplication) inspectDeploymentReplication(lastInterval time.Duration) time.Duration { + log := dr.deps.Log + + spec := dr.apiObject.Spec + nextInterval := lastInterval + hasError := false + ctx := context.Background() + + // Is the deployment in failed state, if so, give up. + if dr.status.Phase == api.DeploymentReplicationPhaseFailed { + log.Debug().Msg("Deployment replication is in Failed state.") + return nextInterval + } + + // Inspect configuration status + destClient, err := dr.createSyncMasterClient(spec.Destination) + if err != nil { + log.Warn().Err(err).Msg("Failed to create destination syncmaster client") + } else { + // Fetch status of destination + updateStatusNeeded := false + configureSyncNeeded := false + cancelSyncNeeded := false + destStatus, err := destClient.Master().Status(ctx) + if err != nil { + log.Warn().Err(err).Msg("Failed to fetch status from destination syncmaster") + } else { + // Inspect destination status + if destStatus.Status.IsActive() { + if dr.isIncomingEndpoint(destStatus, spec.Source) { + // Destination is correctly configured + if dr.status.Conditions.Update(api.ConditionTypeConfigured, true, "Active", "Destination syncmaster is configured correctly and active") { + updateStatusNeeded = true + } + } else { + // Sync is active, but from different source + log.Warn().Msg("Destination syncmaster is configured for different source") + cancelSyncNeeded = true + if dr.status.Conditions.Update(api.ConditionTypeConfigured, false, "Invalid", "Destination syncmaster is configured for different source") { + updateStatusNeeded = true + } + } + } else { + // Destination has correct source, but is inactive + configureSyncNeeded = true + if dr.status.Conditions.Update(api.ConditionTypeConfigured, false, "Inactive", "Destination syncmaster is configured correctly but in-active") { + updateStatusNeeded = true + } + } + } + + // Inspect source + sourceClient, err := dr.createSyncMasterClient(spec.Source) + if err != nil { + log.Warn().Err(err).Msg("Failed to create source syncmaster client") + } else { + sourceStatus, err := sourceClient.Master().Status(ctx) + if err != nil { + log.Warn().Err(err).Msg("Failed to fetch status from source syncmaster") + } + + if sourceStatus.Status.IsActive() { + if dr.hasOutgoingEndpoint(sourceStatus, spec.Destination) { + // Source is correctly configured + } + } + } + + // Update status if needed + if updateStatusNeeded { + if err := dr.updateCRStatus(); err != nil { + log.Warn().Err(err).Msg("Failed to update status") + hasError = true + } + } + + // Cancel sync if needed + if cancelSyncNeeded { + req := client.CancelSynchronizationRequest{} + log.Info().Msg("Canceling synchronization") + if _, err := destClient.Master().CancelSynchronization(ctx, req); err != nil { + log.Warn().Err(err).Msg("Failed to cancel synchronization") + hasError = true + } else { + log.Info().Msg("Canceled synchronization") + nextInterval = time.Second * 10 + } + } + + // Configure sync if needed + if configureSyncNeeded { + source := dr.createArangoSyncEndpoint(spec.Source) + auth, err := dr.createArangoSyncTLSAuthentication(spec.Authentication) + if err != nil { + log.Warn().Err(err).Msg("Failed to configure synchronization authentication") + hasError = true + } else { + req := client.SynchronizationRequest{ + Source: source, + Authentication: auth, + } + log.Info().Msg("Configuring synchronization") + if err := destClient.Master().Synchronize(ctx, req); err != nil { + log.Warn().Err(err).Msg("Failed to configure synchronization") + hasError = true + } else { + log.Info().Msg("Configured synchronization") + nextInterval = time.Second * 10 + } + } + } + } + + // Update next interval (on errors) + if hasError { + if dr.recentInspectionErrors == 0 { + nextInterval = minInspectionInterval + dr.recentInspectionErrors++ + } + } else { + dr.recentInspectionErrors = 0 + } + if nextInterval > maxInspectionInterval { + nextInterval = maxInspectionInterval + } + return nextInterval +} + +// triggerInspection ensures that an inspection is run soon. +func (dr *DeploymentReplication) triggerInspection() { + dr.inspectTrigger.Trigger() +} + +// isIncomingEndpoint returns true when given sync status's endpoint +// intersects with the given endpoint spec. +func (dr *DeploymentReplication) isIncomingEndpoint(status client.SyncInfo, epSpec api.EndpointSpec) bool { + ep := dr.createArangoSyncEndpoint(epSpec) + return !status.Source.Intersection(ep).IsEmpty() +} + +// hasOutgoingEndpoint returns true when given sync status has an outgoing +// item that intersects with the given endpoint spec. +func (dr *DeploymentReplication) hasOutgoingEndpoint(status client.SyncInfo, epSpec api.EndpointSpec) bool { + ep := dr.createArangoSyncEndpoint(epSpec) + for _, o := range status.Outgoing { + if !o.Endpoint.Intersection(ep).IsEmpty() { + return true + } + } + return false +} diff --git a/pkg/util/k8sutil/secrets.go b/pkg/util/k8sutil/secrets.go index 4383c2598..b1b358a7a 100644 --- a/pkg/util/k8sutil/secrets.go +++ b/pkg/util/k8sutil/secrets.go @@ -86,6 +86,24 @@ func ValidateCACertificateSecret(cli corev1.CoreV1Interface, secretName, namespa return nil } +// GetCACertficateSecret loads a secret with given name in the given namespace +// and extracts the `ca.crt` field. +// If the secret does not exists the field is missing, +// an error is returned. +// Returns: certificate, error +func GetCACertficateSecret(cli corev1.CoreV1Interface, secretName, namespace string) (string, error) { + s, err := cli.Secrets(namespace).Get(secretName, metav1.GetOptions{}) + if err != nil { + return "", maskAny(err) + } + // Load `ca.crt` field + cert, found := s.Data[constants.SecretCACertificate] + if !found { + return "", maskAny(fmt.Errorf("No '%s' found in secret '%s'", constants.SecretCACertificate, secretName)) + } + return string(cert), nil +} + // GetCASecret loads a secret with given name in the given namespace // and extracts the `ca.crt` & `ca.key` field. // If the secret does not exists or one of the fields is missing, From 7c070c1523550b5d7359a8e8b4428ddc17ed9c11 Mon Sep 17 00:00:00 2001 From: Ewout Prangsma Date: Fri, 18 May 2018 21:57:23 +0200 Subject: [PATCH 3/6] Various fixes --- manifests/arango-deployment-replication-dev.yaml | 2 +- pkg/deployment/resources/pod_creator.go | 2 +- pkg/replication/deployment_replication.go | 1 + pkg/replication/sync_client.go | 16 ++++++++++++---- pkg/replication/sync_inspector.go | 2 +- 5 files changed, 16 insertions(+), 7 deletions(-) diff --git a/manifests/arango-deployment-replication-dev.yaml b/manifests/arango-deployment-replication-dev.yaml index fab2e22e2..efe9998d7 100644 --- a/manifests/arango-deployment-replication-dev.yaml +++ b/manifests/arango-deployment-replication-dev.yaml @@ -94,7 +94,7 @@ spec: containers: - name: operator imagePullPolicy: IfNotPresent - image: ewoutp/kube-arangodb@sha256:fd5b99fb3637b25f9b09b5c6dc08a2b282a9cf0624cda01db9bf2939143513e2 + image: ewoutp/kube-arangodb@sha256:2f245384090393111ec5e8d35ef35ba5bc1b7a2f66cf273476b9e3095b94c968 args: - --operator.deployment-replication env: diff --git a/pkg/deployment/resources/pod_creator.go b/pkg/deployment/resources/pod_creator.go index c771a93c7..0505cdff9 100644 --- a/pkg/deployment/resources/pod_creator.go +++ b/pkg/deployment/resources/pod_creator.go @@ -536,7 +536,7 @@ func (r *Resources) createPodForMember(spec api.DeploymentSpec, group api.Server } } owner := apiObject.AsOwner() - if err := createServerCertificate(log, kubecli.CoreV1(), serverNames, spec.TLS, tlsKeyfileSecretName, ns, &owner); err != nil && !k8sutil.IsAlreadyExists(err) { + if err := createServerCertificate(log, kubecli.CoreV1(), serverNames, spec.Sync.TLS, tlsKeyfileSecretName, ns, &owner); err != nil && !k8sutil.IsAlreadyExists(err) { return maskAny(errors.Wrapf(err, "Failed to create TLS keyfile secret")) } // Check cluster JWT secret diff --git a/pkg/replication/deployment_replication.go b/pkg/replication/deployment_replication.go index 5aa02cb0b..579fd6db9 100644 --- a/pkg/replication/deployment_replication.go +++ b/pkg/replication/deployment_replication.go @@ -151,6 +151,7 @@ func (dr *DeploymentReplication) run() { //log := dr.deps.Log inspectionInterval := maxInspectionInterval + dr.inspectTrigger.Trigger() for { select { case <-dr.stopCh: diff --git a/pkg/replication/sync_client.go b/pkg/replication/sync_client.go index daa1d410e..4045b87f5 100644 --- a/pkg/replication/sync_client.go +++ b/pkg/replication/sync_client.go @@ -55,7 +55,6 @@ func (dr *DeploymentReplication) createSyncMasterClient(epSpec api.EndpointSpec) return nil, maskAny(err) } tlsAuth.CACertificate = caCert - insecureSkipVerify = false } auth := client.NewAuthentication(tlsAuth, jwtSecret) @@ -75,8 +74,9 @@ func (dr *DeploymentReplication) createArangoSyncEndpoint(epSpec api.EndpointSpe // createArangoSyncTLSAuthentication creates the authentication needed to authenticate // the destination syncmaster at the source syncmaster. -func (dr *DeploymentReplication) createArangoSyncTLSAuthentication(spec api.AuthenticationSpec) (client.TLSAuthentication, error) { - keyFileContent, err := k8sutil.GetTLSKeyfileSecret(dr.deps.KubeCli.CoreV1(), spec.GetClientAuthSecretName(), dr.apiObject.GetNamespace()) +func (dr *DeploymentReplication) createArangoSyncTLSAuthentication(spec api.DeploymentReplicationSpec) (client.TLSAuthentication, error) { + // Fetch keyfile + keyFileContent, err := k8sutil.GetTLSKeyfileSecret(dr.deps.KubeCli.CoreV1(), spec.Authentication.GetClientAuthSecretName(), dr.apiObject.GetNamespace()) if err != nil { return client.TLSAuthentication{}, maskAny(err) } @@ -84,12 +84,20 @@ func (dr *DeploymentReplication) createArangoSyncTLSAuthentication(spec api.Auth if err != nil { return client.TLSAuthentication{}, maskAny(err) } + + // Fetch TLS CA certificate for source + caCert, err := k8sutil.GetCACertficateSecret(dr.deps.KubeCli.CoreV1(), spec.Source.TLS.GetCASecretName(), dr.apiObject.GetNamespace()) + if err != nil { + return client.TLSAuthentication{}, maskAny(err) + } + + // Create authentication result := client.TLSAuthentication{ TLSClientAuthentication: tasks.TLSClientAuthentication{ ClientCertificate: kf.EncodeCertificates(), ClientKey: kf.EncodePrivateKey(), }, + CACertificate: caCert, } - // TODO when add CA cert ???? return result, nil } diff --git a/pkg/replication/sync_inspector.go b/pkg/replication/sync_inspector.go index 0a67b85f6..abf1d61d0 100644 --- a/pkg/replication/sync_inspector.go +++ b/pkg/replication/sync_inspector.go @@ -129,7 +129,7 @@ func (dr *DeploymentReplication) inspectDeploymentReplication(lastInterval time. // Configure sync if needed if configureSyncNeeded { source := dr.createArangoSyncEndpoint(spec.Source) - auth, err := dr.createArangoSyncTLSAuthentication(spec.Authentication) + auth, err := dr.createArangoSyncTLSAuthentication(spec) if err != nil { log.Warn().Err(err).Msg("Failed to configure synchronization authentication") hasError = true From 300faa15899d0a9e765aa9a0b0c43a553df29b9f Mon Sep 17 00:00:00 2001 From: Ewout Prangsma Date: Tue, 22 May 2018 08:23:23 +0200 Subject: [PATCH 4/6] Added `deploymentName` field in EndpointSpec --- .../arango-deployment-replication-dev.yaml | 2 +- .../v1alpha/endpoint_authentication_spec.go | 6 +- pkg/apis/replication/v1alpha/endpoint_spec.go | 39 ++++++++- .../replication/v1alpha/endpoint_tls_spec.go | 6 +- .../v1alpha/zz_generated.deepcopy.go | 9 ++ pkg/replication/sync_client.go | 58 +++++++++++-- pkg/replication/sync_inspector.go | 83 ++++++++++++------- 7 files changed, 156 insertions(+), 47 deletions(-) diff --git a/manifests/arango-deployment-replication-dev.yaml b/manifests/arango-deployment-replication-dev.yaml index efe9998d7..cd26a722d 100644 --- a/manifests/arango-deployment-replication-dev.yaml +++ b/manifests/arango-deployment-replication-dev.yaml @@ -94,7 +94,7 @@ spec: containers: - name: operator imagePullPolicy: IfNotPresent - image: ewoutp/kube-arangodb@sha256:2f245384090393111ec5e8d35ef35ba5bc1b7a2f66cf273476b9e3095b94c968 + image: ewoutp/kube-arangodb@sha256:aa8211554acc1d69589a742231c49fe50496ce6fffe450ed6f5d5bc07f61eee8 args: - --operator.deployment-replication env: diff --git a/pkg/apis/replication/v1alpha/endpoint_authentication_spec.go b/pkg/apis/replication/v1alpha/endpoint_authentication_spec.go index d51119234..2dd352f22 100644 --- a/pkg/apis/replication/v1alpha/endpoint_authentication_spec.go +++ b/pkg/apis/replication/v1alpha/endpoint_authentication_spec.go @@ -25,6 +25,7 @@ package v1alpha import ( "github.com/arangodb/kube-arangodb/pkg/util" "github.com/arangodb/kube-arangodb/pkg/util/k8sutil" + "github.com/pkg/errors" ) // EndpointAuthenticationSpec contains the specification to authentication with the syncmasters @@ -41,10 +42,13 @@ func (s EndpointAuthenticationSpec) GetJWTSecretName() string { // Validate the given spec, returning an error on validation // problems or nil if all ok. -func (s EndpointAuthenticationSpec) Validate() error { +func (s EndpointAuthenticationSpec) Validate(jwtSecretNameRequired bool) error { if err := k8sutil.ValidateOptionalResourceName(s.GetJWTSecretName()); err != nil { return maskAny(err) } + if jwtSecretNameRequired && s.GetJWTSecretName() == "" { + return maskAny(errors.Wrapf(ValidationError, "Provide a jwtSecretName")) + } return nil } diff --git a/pkg/apis/replication/v1alpha/endpoint_spec.go b/pkg/apis/replication/v1alpha/endpoint_spec.go index 7b73dcd18..2a98354e0 100644 --- a/pkg/apis/replication/v1alpha/endpoint_spec.go +++ b/pkg/apis/replication/v1alpha/endpoint_spec.go @@ -25,29 +25,54 @@ package v1alpha import ( "net/url" + "github.com/arangodb/kube-arangodb/pkg/util" + "github.com/arangodb/kube-arangodb/pkg/util/k8sutil" "github.com/pkg/errors" ) // EndpointSpec contains the specification used to reach the syncmasters // in either source or destination mode. type EndpointSpec struct { - MasterEndpoint []string `json:"masterEndpoint,omitempty"` + // DeploymentName holds the name of an ArangoDeployment resource. + // If set this provides default values for masterEndpoint, auth & tls. + DeploymentName *string `json:"deploymentName,omitempty"` + // MasterEndpoints holds a list of URLs used to reach the syncmaster(s). + MasterEndpoint []string `json:"masterEndpoint,omitempty"` + // Authentication holds settings needed to authentication at the syncmaster. Authentication EndpointAuthenticationSpec `json:"auth"` - TLS EndpointTLSSpec `json:"tls"` + // TLS holds settings needed to verify the TLS connection to the syncmaster. + TLS EndpointTLSSpec `json:"tls"` +} + +// GetDeploymentName returns the value of deploymentName. +func (s EndpointSpec) GetDeploymentName() string { + return util.StringOrDefault(s.DeploymentName) +} + +// HasDeploymentName returns the true when a non-empty deployment name it set. +func (s EndpointSpec) HasDeploymentName() bool { + return s.GetDeploymentName() != "" } // Validate the given spec, returning an error on validation // problems or nil if all ok. func (s EndpointSpec) Validate() error { + if err := k8sutil.ValidateOptionalResourceName(s.GetDeploymentName()); err != nil { + return maskAny(err) + } for _, ep := range s.MasterEndpoint { if _, err := url.Parse(ep); err != nil { return maskAny(errors.Wrapf(ValidationError, "Invalid master endpoint '%s': %s", ep, err)) } } - if err := s.Authentication.Validate(); err != nil { + hasDeploymentName := s.HasDeploymentName() + if !hasDeploymentName && len(s.MasterEndpoint) == 0 { + return maskAny(errors.Wrapf(ValidationError, "Provide a deploy name or at least one master endpoint")) + } + if err := s.Authentication.Validate(!hasDeploymentName); err != nil { return maskAny(err) } - if err := s.TLS.Validate(); err != nil { + if err := s.TLS.Validate(!hasDeploymentName); err != nil { return maskAny(err) } return nil @@ -61,6 +86,9 @@ func (s *EndpointSpec) SetDefaults() { // SetDefaultsFrom fills empty field with default values from the given source. func (s *EndpointSpec) SetDefaultsFrom(source EndpointSpec) { + if s.DeploymentName == nil { + s.DeploymentName = util.NewStringOrNil(source.DeploymentName) + } s.Authentication.SetDefaultsFrom(source.Authentication) s.TLS.SetDefaultsFrom(source.TLS) } @@ -70,6 +98,9 @@ func (s *EndpointSpec) SetDefaultsFrom(source EndpointSpec) { // Field names are relative to `spec.`. func (s EndpointSpec) ResetImmutableFields(target *EndpointSpec, fieldPrefix string) []string { var result []string + if s.GetDeploymentName() != target.GetDeploymentName() { + result = append(result, fieldPrefix+"deploymentName") + } if list := s.Authentication.ResetImmutableFields(&target.Authentication, fieldPrefix+"auth."); len(list) > 0 { result = append(result, list...) } diff --git a/pkg/apis/replication/v1alpha/endpoint_tls_spec.go b/pkg/apis/replication/v1alpha/endpoint_tls_spec.go index 719fe6fd0..2ae74d3ca 100644 --- a/pkg/apis/replication/v1alpha/endpoint_tls_spec.go +++ b/pkg/apis/replication/v1alpha/endpoint_tls_spec.go @@ -25,6 +25,7 @@ package v1alpha import ( "github.com/arangodb/kube-arangodb/pkg/util" "github.com/arangodb/kube-arangodb/pkg/util/k8sutil" + "github.com/pkg/errors" ) // EndpointTLSSpec contains the specification regarding the TLS connection to the syncmasters @@ -41,10 +42,13 @@ func (s EndpointTLSSpec) GetCASecretName() string { // Validate the given spec, returning an error on validation // problems or nil if all ok. -func (s EndpointTLSSpec) Validate() error { +func (s EndpointTLSSpec) Validate(caSecretNameRequired bool) error { if err := k8sutil.ValidateOptionalResourceName(s.GetCASecretName()); err != nil { return maskAny(err) } + if caSecretNameRequired && s.GetCASecretName() == "" { + return maskAny(errors.Wrapf(ValidationError, "Provide a caSecretName")) + } return nil } diff --git a/pkg/apis/replication/v1alpha/zz_generated.deepcopy.go b/pkg/apis/replication/v1alpha/zz_generated.deepcopy.go index 9075af396..e73651076 100644 --- a/pkg/apis/replication/v1alpha/zz_generated.deepcopy.go +++ b/pkg/apis/replication/v1alpha/zz_generated.deepcopy.go @@ -202,6 +202,15 @@ func (in *EndpointAuthenticationSpec) DeepCopy() *EndpointAuthenticationSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *EndpointSpec) DeepCopyInto(out *EndpointSpec) { *out = *in + if in.DeploymentName != nil { + in, out := &in.DeploymentName, &out.DeploymentName + if *in == nil { + *out = nil + } else { + *out = new(string) + **out = **in + } + } if in.MasterEndpoint != nil { in, out := &in.MasterEndpoint, &out.MasterEndpoint *out = make([]string, len(*in)) diff --git a/pkg/replication/sync_client.go b/pkg/replication/sync_client.go index 4045b87f5..a1edf3121 100644 --- a/pkg/replication/sync_client.go +++ b/pkg/replication/sync_client.go @@ -23,9 +23,13 @@ package replication import ( + "net" + "strconv" + certificates "github.com/arangodb-helper/go-certificates" "github.com/arangodb/arangosync/client" "github.com/arangodb/arangosync/tasks" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" api "github.com/arangodb/kube-arangodb/pkg/apis/replication/v1alpha" "github.com/arangodb/kube-arangodb/pkg/util/k8sutil" @@ -36,21 +40,28 @@ func (dr *DeploymentReplication) createSyncMasterClient(epSpec api.EndpointSpec) log := dr.deps.Log // Endpoint - source := dr.createArangoSyncEndpoint(epSpec) + source, err := dr.createArangoSyncEndpoint(epSpec) + if err != nil { + return nil, maskAny(err) + } // Authentication insecureSkipVerify := true tlsAuth := tasks.TLSAuthentication{} + authJWTSecretName, tlsCASecretName, err := dr.getEndpointSecretNames(epSpec) + if err != nil { + return nil, maskAny(err) + } jwtSecret := "" - if jwtSecretName := epSpec.Authentication.GetJWTSecretName(); jwtSecretName != "" { + if authJWTSecretName != "" { var err error - jwtSecret, err = k8sutil.GetJWTSecret(dr.deps.KubeCli.CoreV1(), jwtSecretName, dr.apiObject.GetNamespace()) + jwtSecret, err = k8sutil.GetJWTSecret(dr.deps.KubeCli.CoreV1(), authJWTSecretName, dr.apiObject.GetNamespace()) if err != nil { return nil, maskAny(err) } } - if caSecretName := epSpec.TLS.GetCASecretName(); caSecretName != "" { - caCert, err := k8sutil.GetCACertficateSecret(dr.deps.KubeCli.CoreV1(), caSecretName, dr.apiObject.GetNamespace()) + if tlsCASecretName != "" { + caCert, err := k8sutil.GetCACertficateSecret(dr.deps.KubeCli.CoreV1(), tlsCASecretName, dr.apiObject.GetNamespace()) if err != nil { return nil, maskAny(err) } @@ -67,9 +78,19 @@ func (dr *DeploymentReplication) createSyncMasterClient(epSpec api.EndpointSpec) } // createArangoSyncEndpoint creates the endpoints for the given spec. -func (dr *DeploymentReplication) createArangoSyncEndpoint(epSpec api.EndpointSpec) client.Endpoint { - // TODO when adding deploymentname to EndpointSpec, reflect that here - return client.Endpoint(epSpec.MasterEndpoint) +func (dr *DeploymentReplication) createArangoSyncEndpoint(epSpec api.EndpointSpec) (client.Endpoint, error) { + if epSpec.HasDeploymentName() { + deploymentName := epSpec.GetDeploymentName() + depls := dr.deps.CRCli.DatabaseV1alpha().ArangoDeployments(dr.apiObject.GetNamespace()) + depl, err := depls.Get(deploymentName, metav1.GetOptions{}) + if err != nil { + dr.deps.Log.Debug().Err(err).Str("deployment", deploymentName).Msg("Failed to get deployment") + return nil, maskAny(err) + } + dnsName := k8sutil.CreateSyncMasterClientServiceDNSName(depl) + return client.Endpoint{"https://" + net.JoinHostPort(dnsName, strconv.Itoa(k8sutil.ArangoSyncMasterPort))}, nil + } + return client.Endpoint(epSpec.MasterEndpoint), nil } // createArangoSyncTLSAuthentication creates the authentication needed to authenticate @@ -86,7 +107,11 @@ func (dr *DeploymentReplication) createArangoSyncTLSAuthentication(spec api.Depl } // Fetch TLS CA certificate for source - caCert, err := k8sutil.GetCACertficateSecret(dr.deps.KubeCli.CoreV1(), spec.Source.TLS.GetCASecretName(), dr.apiObject.GetNamespace()) + _, tlsCASecretName, err := dr.getEndpointSecretNames(spec.Source) + if err != nil { + return client.TLSAuthentication{}, maskAny(err) + } + caCert, err := k8sutil.GetCACertficateSecret(dr.deps.KubeCli.CoreV1(), tlsCASecretName, dr.apiObject.GetNamespace()) if err != nil { return client.TLSAuthentication{}, maskAny(err) } @@ -101,3 +126,18 @@ func (dr *DeploymentReplication) createArangoSyncTLSAuthentication(spec api.Depl } return result, nil } + +// getEndpointSecretNames returns the names of secrets that hold the JWT token, TLS ca.crt. +func (dr *DeploymentReplication) getEndpointSecretNames(epSpec api.EndpointSpec) (authJWTSecretName, tlsCASecretName string, err error) { + if epSpec.HasDeploymentName() { + deploymentName := epSpec.GetDeploymentName() + depls := dr.deps.CRCli.DatabaseV1alpha().ArangoDeployments(dr.apiObject.GetNamespace()) + depl, err := depls.Get(deploymentName, metav1.GetOptions{}) + if err != nil { + dr.deps.Log.Debug().Err(err).Str("deployment", deploymentName).Msg("Failed to get deployment") + return "", "", maskAny(err) + } + return depl.Spec.Sync.Authentication.GetJWTSecretName(), depl.Spec.Sync.TLS.GetCASecretName(), nil + } + return epSpec.Authentication.GetJWTSecretName(), epSpec.TLS.GetCASecretName(), nil +} diff --git a/pkg/replication/sync_inspector.go b/pkg/replication/sync_inspector.go index abf1d61d0..4883f6b06 100644 --- a/pkg/replication/sync_inspector.go +++ b/pkg/replication/sync_inspector.go @@ -66,17 +66,22 @@ func (dr *DeploymentReplication) inspectDeploymentReplication(lastInterval time. } else { // Inspect destination status if destStatus.Status.IsActive() { - if dr.isIncomingEndpoint(destStatus, spec.Source) { - // Destination is correctly configured - if dr.status.Conditions.Update(api.ConditionTypeConfigured, true, "Active", "Destination syncmaster is configured correctly and active") { - updateStatusNeeded = true - } + isIncomingEndpoint, err := dr.isIncomingEndpoint(destStatus, spec.Source) + if err != nil { + log.Warn().Err(err).Msg("Failed to check is-incoming-endpoint") } else { - // Sync is active, but from different source - log.Warn().Msg("Destination syncmaster is configured for different source") - cancelSyncNeeded = true - if dr.status.Conditions.Update(api.ConditionTypeConfigured, false, "Invalid", "Destination syncmaster is configured for different source") { - updateStatusNeeded = true + if isIncomingEndpoint { + // Destination is correctly configured + if dr.status.Conditions.Update(api.ConditionTypeConfigured, true, "Active", "Destination syncmaster is configured correctly and active") { + updateStatusNeeded = true + } + } else { + // Sync is active, but from different source + log.Warn().Msg("Destination syncmaster is configured for different source") + cancelSyncNeeded = true + if dr.status.Conditions.Update(api.ConditionTypeConfigured, false, "Invalid", "Destination syncmaster is configured for different source") { + updateStatusNeeded = true + } } } } else { @@ -99,8 +104,13 @@ func (dr *DeploymentReplication) inspectDeploymentReplication(lastInterval time. } if sourceStatus.Status.IsActive() { - if dr.hasOutgoingEndpoint(sourceStatus, spec.Destination) { - // Source is correctly configured + hasOutgoingEndpoint, err := dr.hasOutgoingEndpoint(sourceStatus, spec.Destination) + if err != nil { + log.Warn().Err(err).Msg("Failed to check has-outgoing-endpoint") + } else { + if hasOutgoingEndpoint { + // Source is correctly configured + } } } } @@ -128,23 +138,28 @@ func (dr *DeploymentReplication) inspectDeploymentReplication(lastInterval time. // Configure sync if needed if configureSyncNeeded { - source := dr.createArangoSyncEndpoint(spec.Source) - auth, err := dr.createArangoSyncTLSAuthentication(spec) + source, err := dr.createArangoSyncEndpoint(spec.Source) if err != nil { - log.Warn().Err(err).Msg("Failed to configure synchronization authentication") + log.Warn().Err(err).Msg("Failed to create syncmaster endpoint") hasError = true } else { - req := client.SynchronizationRequest{ - Source: source, - Authentication: auth, - } - log.Info().Msg("Configuring synchronization") - if err := destClient.Master().Synchronize(ctx, req); err != nil { - log.Warn().Err(err).Msg("Failed to configure synchronization") + auth, err := dr.createArangoSyncTLSAuthentication(spec) + if err != nil { + log.Warn().Err(err).Msg("Failed to configure synchronization authentication") hasError = true } else { - log.Info().Msg("Configured synchronization") - nextInterval = time.Second * 10 + req := client.SynchronizationRequest{ + Source: source, + Authentication: auth, + } + log.Info().Msg("Configuring synchronization") + if err := destClient.Master().Synchronize(ctx, req); err != nil { + log.Warn().Err(err).Msg("Failed to configure synchronization") + hasError = true + } else { + log.Info().Msg("Configured synchronization") + nextInterval = time.Second * 10 + } } } } @@ -172,19 +187,25 @@ func (dr *DeploymentReplication) triggerInspection() { // isIncomingEndpoint returns true when given sync status's endpoint // intersects with the given endpoint spec. -func (dr *DeploymentReplication) isIncomingEndpoint(status client.SyncInfo, epSpec api.EndpointSpec) bool { - ep := dr.createArangoSyncEndpoint(epSpec) - return !status.Source.Intersection(ep).IsEmpty() +func (dr *DeploymentReplication) isIncomingEndpoint(status client.SyncInfo, epSpec api.EndpointSpec) (bool, error) { + ep, err := dr.createArangoSyncEndpoint(epSpec) + if err != nil { + return false, maskAny(err) + } + return !status.Source.Intersection(ep).IsEmpty(), nil } // hasOutgoingEndpoint returns true when given sync status has an outgoing // item that intersects with the given endpoint spec. -func (dr *DeploymentReplication) hasOutgoingEndpoint(status client.SyncInfo, epSpec api.EndpointSpec) bool { - ep := dr.createArangoSyncEndpoint(epSpec) +func (dr *DeploymentReplication) hasOutgoingEndpoint(status client.SyncInfo, epSpec api.EndpointSpec) (bool, error) { + ep, err := dr.createArangoSyncEndpoint(epSpec) + if err != nil { + return false, maskAny(err) + } for _, o := range status.Outgoing { if !o.Endpoint.Intersection(ep).IsEmpty() { - return true + return true, nil } } - return false + return false, nil } From aa7c53a4c126065c80e2789d6aff7162e4014d56 Mon Sep 17 00:00:00 2001 From: Ewout Prangsma Date: Tue, 22 May 2018 09:14:12 +0200 Subject: [PATCH 5/6] Added stop-sync finalizer --- .../arango-deployment-replication-dev.yaml | 2 +- pkg/replication/deployment_replication.go | 7 +- pkg/replication/finalizers.go | 182 ++++++++++++++++++ pkg/replication/sync_inspector.go | 182 ++++++++++-------- pkg/util/constants/constants.go | 7 +- pkg/util/k8sutil/finalizers.go | 8 +- 6 files changed, 295 insertions(+), 93 deletions(-) create mode 100644 pkg/replication/finalizers.go diff --git a/manifests/arango-deployment-replication-dev.yaml b/manifests/arango-deployment-replication-dev.yaml index cd26a722d..9860eb04f 100644 --- a/manifests/arango-deployment-replication-dev.yaml +++ b/manifests/arango-deployment-replication-dev.yaml @@ -94,7 +94,7 @@ spec: containers: - name: operator imagePullPolicy: IfNotPresent - image: ewoutp/kube-arangodb@sha256:aa8211554acc1d69589a742231c49fe50496ce6fffe450ed6f5d5bc07f61eee8 + image: ewoutp/kube-arangodb@sha256:8480ab465a40a91b9dffeff0d3cf1534e1687b38ca7ad57b254d1deefdffd81f args: - --operator.deployment-replication env: diff --git a/pkg/replication/deployment_replication.go b/pkg/replication/deployment_replication.go index 579fd6db9..4a44dd1c8 100644 --- a/pkg/replication/deployment_replication.go +++ b/pkg/replication/deployment_replication.go @@ -148,7 +148,12 @@ func (dr *DeploymentReplication) send(ev *deploymentReplicationEvent) { // It processes the event queue and polls the state of generated // resource on a regular basis. func (dr *DeploymentReplication) run() { - //log := dr.deps.Log + log := dr.deps.Log + + // Add finalizers + if err := dr.addFinalizers(); err != nil { + log.Warn().Err(err).Msg("Failed to add finalizers") + } inspectionInterval := maxInspectionInterval dr.inspectTrigger.Trigger() diff --git a/pkg/replication/finalizers.go b/pkg/replication/finalizers.go new file mode 100644 index 000000000..c138c8795 --- /dev/null +++ b/pkg/replication/finalizers.go @@ -0,0 +1,182 @@ +// +// DISCLAIMER +// +// Copyright 2018 ArangoDB GmbH, Cologne, Germany +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// Copyright holder is ArangoDB GmbH, Cologne, Germany +// +// Author Ewout Prangsma +// + +package replication + +import ( + "context" + "fmt" + "time" + + "github.com/rs/zerolog" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/arangodb/arangosync/client" + api "github.com/arangodb/kube-arangodb/pkg/apis/replication/v1alpha" + "github.com/arangodb/kube-arangodb/pkg/generated/clientset/versioned" + "github.com/arangodb/kube-arangodb/pkg/util/constants" + "github.com/arangodb/kube-arangodb/pkg/util/k8sutil" +) + +// addFinalizers adds a stop-sync finalizer to the api object when needed. +func (dr *DeploymentReplication) addFinalizers() error { + apiObject := dr.apiObject + if apiObject.GetDeletionTimestamp() != nil { + // Delete already triggered, cannot add. + return nil + } + for _, f := range apiObject.GetFinalizers() { + if f == constants.FinalizerDeplReplStopSync { + // Finalizer already added + return nil + } + } + apiObject.SetFinalizers(append(apiObject.GetFinalizers(), constants.FinalizerDeplReplStopSync)) + if err := dr.updateCRSpec(apiObject.Spec); err != nil { + return maskAny(err) + } + return nil +} + +// runFinalizers goes through the list of ArangoDeploymentReplication finalizers to see if they can be removed. +func (dr *DeploymentReplication) runFinalizers(ctx context.Context, p *api.ArangoDeploymentReplication) error { + log := dr.deps.Log.With().Str("replication-name", p.GetName()).Logger() + var removalList []string + for _, f := range p.ObjectMeta.GetFinalizers() { + switch f { + case constants.FinalizerDeplReplStopSync: + log.Debug().Msg("Inspecting stop-sync finalizer") + if err := dr.inspectFinalizerDeplReplStopSync(ctx, log, p); err == nil { + removalList = append(removalList, f) + } else { + log.Debug().Err(err).Str("finalizer", f).Msg("Cannot remove finalizer yet") + } + } + } + // Remove finalizers (if needed) + if len(removalList) > 0 { + if err := removeDeploymentReplicationFinalizers(log, dr.deps.CRCli, p, removalList); err != nil { + log.Debug().Err(err).Msg("Failed to update deployment replication (to remove finalizers)") + return maskAny(err) + } + } + return nil +} + +// inspectFinalizerDeplReplStopSync checks the finalizer condition for stop-sync. +// It returns nil if the finalizer can be removed. +func (dr *DeploymentReplication) inspectFinalizerDeplReplStopSync(ctx context.Context, log zerolog.Logger, p *api.ArangoDeploymentReplication) error { + // Inspect phase + if p.Status.Phase.IsFailed() { + log.Debug().Msg("Deployment replication is already failed, safe to remove stop-sync finalizer") + return nil + } + + // Inspect deployment deletion state in source + abort := false + depls := dr.deps.CRCli.DatabaseV1alpha().ArangoDeployments(p.GetNamespace()) + if name := p.Spec.Source.GetDeploymentName(); name != "" { + depl, err := depls.Get(name, metav1.GetOptions{}) + if k8sutil.IsNotFound(err) { + log.Debug().Msg("Source deployment is gone. Abort enabled") + abort = true + } else if err != nil { + log.Warn().Err(err).Msg("Failed to get source deployment") + return maskAny(err) + } else if depl.GetDeletionTimestamp() != nil { + log.Debug().Msg("Source deployment is being deleted. Abort enabled") + abort = true + } + } + + // Inspect deployment deletion state in destination + cleanupSource := false + if name := p.Spec.Destination.GetDeploymentName(); name != "" { + depl, err := depls.Get(name, metav1.GetOptions{}) + if k8sutil.IsNotFound(err) { + log.Debug().Msg("Destination deployment is gone. Source cleanup enabled") + cleanupSource = true + } else if err != nil { + log.Warn().Err(err).Msg("Failed to get destinaton deployment") + return maskAny(err) + } else if depl.GetDeletionTimestamp() != nil { + log.Debug().Msg("Destination deployment is being deleted. Source cleanup enabled") + cleanupSource = true + } + } + + // Cleanup source or stop sync + if cleanupSource { + // Destination is gone, cleanup source + /*sourceClient, err := dr.createSyncMasterClient(p.Spec.Source) + if err != nil { + log.Warn().Err(err).Msg("Failed to create source client") + return maskAny(err) + }*/ + //sourceClient.Master().C + return maskAny(fmt.Errorf("TODO")) + } else { + // Destination still exists, stop/abort sync + destClient, err := dr.createSyncMasterClient(p.Spec.Destination) + if err != nil { + log.Warn().Err(err).Msg("Failed to create destination client") + return maskAny(err) + } + req := client.CancelSynchronizationRequest{ + WaitTimeout: time.Minute * 3, + Force: abort, + ForceTimeout: time.Minute * 2, + } + log.Debug().Bool("abort", abort).Msg("Stopping synchronization...") + _, err = destClient.Master().CancelSynchronization(ctx, req) + if err != nil && !client.IsPreconditionFailed(err) { + log.Warn().Err(err).Bool("abort", abort).Msg("Failed to stop synchronization") + return maskAny(err) + } + return nil + } +} + +// removeDeploymentReplicationFinalizers removes the given finalizers from the given DeploymentReplication. +func removeDeploymentReplicationFinalizers(log zerolog.Logger, crcli versioned.Interface, p *api.ArangoDeploymentReplication, finalizers []string) error { + repls := crcli.ReplicationV1alpha().ArangoDeploymentReplications(p.GetNamespace()) + getFunc := func() (metav1.Object, error) { + result, err := repls.Get(p.GetName(), metav1.GetOptions{}) + if err != nil { + return nil, maskAny(err) + } + return result, nil + } + updateFunc := func(updated metav1.Object) error { + updatedRepl := updated.(*api.ArangoDeploymentReplication) + result, err := repls.Update(updatedRepl) + if err != nil { + return maskAny(err) + } + *p = *result + return nil + } + if err := k8sutil.RemoveFinalizers(log, finalizers, getFunc, updateFunc); err != nil { + return maskAny(err) + } + return nil +} diff --git a/pkg/replication/sync_inspector.go b/pkg/replication/sync_inspector.go index 4883f6b06..c3dd30f9b 100644 --- a/pkg/replication/sync_inspector.go +++ b/pkg/replication/sync_inspector.go @@ -45,120 +45,134 @@ func (dr *DeploymentReplication) inspectDeploymentReplication(lastInterval time. hasError := false ctx := context.Background() + // Add finalizers + if err := dr.addFinalizers(); err != nil { + log.Warn().Err(err).Msg("Failed to add finalizers") + } + // Is the deployment in failed state, if so, give up. if dr.status.Phase == api.DeploymentReplicationPhaseFailed { log.Debug().Msg("Deployment replication is in Failed state.") return nextInterval } - // Inspect configuration status - destClient, err := dr.createSyncMasterClient(spec.Destination) - if err != nil { - log.Warn().Err(err).Msg("Failed to create destination syncmaster client") + // Is delete triggered? + if dr.apiObject.GetDeletionTimestamp() != nil { + // Deployment replication is triggered for deletion. + if err := dr.runFinalizers(ctx, dr.apiObject); err != nil { + log.Warn().Err(err).Msg("Failed to run finalizers") + hasError = true + } } else { - // Fetch status of destination - updateStatusNeeded := false - configureSyncNeeded := false - cancelSyncNeeded := false - destStatus, err := destClient.Master().Status(ctx) + // Inspect configuration status + destClient, err := dr.createSyncMasterClient(spec.Destination) if err != nil { - log.Warn().Err(err).Msg("Failed to fetch status from destination syncmaster") + log.Warn().Err(err).Msg("Failed to create destination syncmaster client") } else { - // Inspect destination status - if destStatus.Status.IsActive() { - isIncomingEndpoint, err := dr.isIncomingEndpoint(destStatus, spec.Source) - if err != nil { - log.Warn().Err(err).Msg("Failed to check is-incoming-endpoint") - } else { - if isIncomingEndpoint { - // Destination is correctly configured - if dr.status.Conditions.Update(api.ConditionTypeConfigured, true, "Active", "Destination syncmaster is configured correctly and active") { - updateStatusNeeded = true - } + // Fetch status of destination + updateStatusNeeded := false + configureSyncNeeded := false + cancelSyncNeeded := false + destStatus, err := destClient.Master().Status(ctx) + if err != nil { + log.Warn().Err(err).Msg("Failed to fetch status from destination syncmaster") + } else { + // Inspect destination status + if destStatus.Status.IsActive() { + isIncomingEndpoint, err := dr.isIncomingEndpoint(destStatus, spec.Source) + if err != nil { + log.Warn().Err(err).Msg("Failed to check is-incoming-endpoint") } else { - // Sync is active, but from different source - log.Warn().Msg("Destination syncmaster is configured for different source") - cancelSyncNeeded = true - if dr.status.Conditions.Update(api.ConditionTypeConfigured, false, "Invalid", "Destination syncmaster is configured for different source") { - updateStatusNeeded = true + if isIncomingEndpoint { + // Destination is correctly configured + if dr.status.Conditions.Update(api.ConditionTypeConfigured, true, "Active", "Destination syncmaster is configured correctly and active") { + updateStatusNeeded = true + } + } else { + // Sync is active, but from different source + log.Warn().Msg("Destination syncmaster is configured for different source") + cancelSyncNeeded = true + if dr.status.Conditions.Update(api.ConditionTypeConfigured, false, "Invalid", "Destination syncmaster is configured for different source") { + updateStatusNeeded = true + } } } - } - } else { - // Destination has correct source, but is inactive - configureSyncNeeded = true - if dr.status.Conditions.Update(api.ConditionTypeConfigured, false, "Inactive", "Destination syncmaster is configured correctly but in-active") { - updateStatusNeeded = true + } else { + // Destination has correct source, but is inactive + configureSyncNeeded = true + if dr.status.Conditions.Update(api.ConditionTypeConfigured, false, "Inactive", "Destination syncmaster is configured correctly but in-active") { + updateStatusNeeded = true + } } } - } - // Inspect source - sourceClient, err := dr.createSyncMasterClient(spec.Source) - if err != nil { - log.Warn().Err(err).Msg("Failed to create source syncmaster client") - } else { - sourceStatus, err := sourceClient.Master().Status(ctx) + // Inspect source + sourceClient, err := dr.createSyncMasterClient(spec.Source) if err != nil { - log.Warn().Err(err).Msg("Failed to fetch status from source syncmaster") - } - - if sourceStatus.Status.IsActive() { - hasOutgoingEndpoint, err := dr.hasOutgoingEndpoint(sourceStatus, spec.Destination) + log.Warn().Err(err).Msg("Failed to create source syncmaster client") + } else { + sourceStatus, err := sourceClient.Master().Status(ctx) if err != nil { - log.Warn().Err(err).Msg("Failed to check has-outgoing-endpoint") - } else { - if hasOutgoingEndpoint { - // Source is correctly configured + log.Warn().Err(err).Msg("Failed to fetch status from source syncmaster") + } + + if sourceStatus.Status.IsActive() { + hasOutgoingEndpoint, err := dr.hasOutgoingEndpoint(sourceStatus, spec.Destination) + if err != nil { + log.Warn().Err(err).Msg("Failed to check has-outgoing-endpoint") + } else { + if hasOutgoingEndpoint { + // Source is correctly configured + } } } } - } - // Update status if needed - if updateStatusNeeded { - if err := dr.updateCRStatus(); err != nil { - log.Warn().Err(err).Msg("Failed to update status") - hasError = true + // Update status if needed + if updateStatusNeeded { + if err := dr.updateCRStatus(); err != nil { + log.Warn().Err(err).Msg("Failed to update status") + hasError = true + } } - } - // Cancel sync if needed - if cancelSyncNeeded { - req := client.CancelSynchronizationRequest{} - log.Info().Msg("Canceling synchronization") - if _, err := destClient.Master().CancelSynchronization(ctx, req); err != nil { - log.Warn().Err(err).Msg("Failed to cancel synchronization") - hasError = true - } else { - log.Info().Msg("Canceled synchronization") - nextInterval = time.Second * 10 + // Cancel sync if needed + if cancelSyncNeeded { + req := client.CancelSynchronizationRequest{} + log.Info().Msg("Canceling synchronization") + if _, err := destClient.Master().CancelSynchronization(ctx, req); err != nil { + log.Warn().Err(err).Msg("Failed to cancel synchronization") + hasError = true + } else { + log.Info().Msg("Canceled synchronization") + nextInterval = time.Second * 10 + } } - } - // Configure sync if needed - if configureSyncNeeded { - source, err := dr.createArangoSyncEndpoint(spec.Source) - if err != nil { - log.Warn().Err(err).Msg("Failed to create syncmaster endpoint") - hasError = true - } else { - auth, err := dr.createArangoSyncTLSAuthentication(spec) + // Configure sync if needed + if configureSyncNeeded { + source, err := dr.createArangoSyncEndpoint(spec.Source) if err != nil { - log.Warn().Err(err).Msg("Failed to configure synchronization authentication") + log.Warn().Err(err).Msg("Failed to create syncmaster endpoint") hasError = true } else { - req := client.SynchronizationRequest{ - Source: source, - Authentication: auth, - } - log.Info().Msg("Configuring synchronization") - if err := destClient.Master().Synchronize(ctx, req); err != nil { - log.Warn().Err(err).Msg("Failed to configure synchronization") + auth, err := dr.createArangoSyncTLSAuthentication(spec) + if err != nil { + log.Warn().Err(err).Msg("Failed to configure synchronization authentication") hasError = true } else { - log.Info().Msg("Configured synchronization") - nextInterval = time.Second * 10 + req := client.SynchronizationRequest{ + Source: source, + Authentication: auth, + } + log.Info().Msg("Configuring synchronization") + if err := destClient.Master().Synchronize(ctx, req); err != nil { + log.Warn().Err(err).Msg("Failed to configure synchronization") + hasError = true + } else { + log.Info().Msg("Configured synchronization") + nextInterval = time.Second * 10 + } } } } diff --git a/pkg/util/constants/constants.go b/pkg/util/constants/constants.go index aeead5659..dcf0d471c 100644 --- a/pkg/util/constants/constants.go +++ b/pkg/util/constants/constants.go @@ -40,7 +40,8 @@ const ( SecretTLSKeyfile = "tls.keyfile" // Key in Secret.data used to store a PEM encoded TLS certificate in the format used by ArangoDB (`--ssl.keyfile`) - FinalizerPodDrainDBServer = "dbserver.database.arangodb.com/drain" // Finalizer added to DBServers, indicating the need for draining that dbserver - FinalizerPodAgencyServing = "agent.database.arangodb.com/agency-serving" // Finalizer added to Agents, indicating the need for keeping enough agents alive - FinalizerPVCMemberExists = "pvc.database.arangodb.com/member-exists" // Finalizer added to PVCs, indicating the need to keep is as long as its member exists + FinalizerPodDrainDBServer = "dbserver.database.arangodb.com/drain" // Finalizer added to DBServers, indicating the need for draining that dbserver + FinalizerPodAgencyServing = "agent.database.arangodb.com/agency-serving" // Finalizer added to Agents, indicating the need for keeping enough agents alive + FinalizerPVCMemberExists = "pvc.database.arangodb.com/member-exists" // Finalizer added to PVCs, indicating the need to keep is as long as its member exists + FinalizerDeplReplStopSync = "replication.database.arangodb.com/stop-sync" // Finalizer added to ArangoDeploymentReplication, indicating the need to stop synchronization ) diff --git a/pkg/util/k8sutil/finalizers.go b/pkg/util/k8sutil/finalizers.go index ec4ea3b6d..b4640b6e5 100644 --- a/pkg/util/k8sutil/finalizers.go +++ b/pkg/util/k8sutil/finalizers.go @@ -52,7 +52,7 @@ func RemovePodFinalizers(log zerolog.Logger, kubecli kubernetes.Interface, p *v1 *p = *result return nil } - if err := removeFinalizers(log, finalizers, getFunc, updateFunc); err != nil { + if err := RemoveFinalizers(log, finalizers, getFunc, updateFunc); err != nil { return maskAny(err) } return nil @@ -77,17 +77,17 @@ func RemovePVCFinalizers(log zerolog.Logger, kubecli kubernetes.Interface, p *v1 *p = *result return nil } - if err := removeFinalizers(log, finalizers, getFunc, updateFunc); err != nil { + if err := RemoveFinalizers(log, finalizers, getFunc, updateFunc); err != nil { return maskAny(err) } return nil } -// removeFinalizers is a helper used to remove finalizers from an object. +// RemoveFinalizers is a helper used to remove finalizers from an object. // The functions tries to get the object using the provided get function, // then remove the given finalizers and update the update using the given update function. // In case of an update conflict, the functions tries again. -func removeFinalizers(log zerolog.Logger, finalizers []string, getFunc func() (metav1.Object, error), updateFunc func(metav1.Object) error) error { +func RemoveFinalizers(log zerolog.Logger, finalizers []string, getFunc func() (metav1.Object, error), updateFunc func(metav1.Object) error) error { attempts := 0 for { attempts++ From 61741ebe50029e25ceaf98a8039933ac4300355d Mon Sep 17 00:00:00 2001 From: Ewout Prangsma Date: Tue, 22 May 2018 13:20:58 +0200 Subject: [PATCH 6/6] Gitignored arango-deployment-replication-dev.yaml --- manifests/.gitignore | 1 + .../arango-deployment-replication-dev.yaml | 130 ------------------ 2 files changed, 1 insertion(+), 130 deletions(-) delete mode 100644 manifests/arango-deployment-replication-dev.yaml diff --git a/manifests/.gitignore b/manifests/.gitignore index 9118a229f..0c8b9cdf1 100644 --- a/manifests/.gitignore +++ b/manifests/.gitignore @@ -1,3 +1,4 @@ arango-deployment-dev.yaml +arango-deployment-replication-dev.yaml arango-storage-dev.yaml arango-test-dev.yaml diff --git a/manifests/arango-deployment-replication-dev.yaml b/manifests/arango-deployment-replication-dev.yaml deleted file mode 100644 index 9860eb04f..000000000 --- a/manifests/arango-deployment-replication-dev.yaml +++ /dev/null @@ -1,130 +0,0 @@ -## deployment-replication/rbac.yaml -## Cluster role granting access to ArangoDeploymentReplication resources. -apiVersion: rbac.authorization.k8s.io/v1beta1 -kind: ClusterRole -metadata: - name: arango-deployment-replications -rules: -- apiGroups: ["replication.database.arangodb.com"] - resources: ["arangodeploymentreplications"] - verbs: ["*"] - ---- - -## Cluster role granting access to all resources needed by the ArangoDeploymentReplication operator. -apiVersion: rbac.authorization.k8s.io/v1beta1 -kind: ClusterRole -metadata: - name: arango-deployment-replication-operator -rules: -- apiGroups: ["replication.database.arangodb.com"] - resources: ["arangodeploymentreplications"] - verbs: ["*"] -- apiGroups: ["database.arangodb.com"] - resources: ["arangodeployments"] - verbs: ["get"] -- apiGroups: ["apiextensions.k8s.io"] - resources: ["customresourcedefinitions"] - verbs: ["get"] -- apiGroups: [""] - resources: ["pods", "services", "endpoints", "persistentvolumeclaims", "events", "secrets"] - verbs: ["*"] -- apiGroups: [""] - resources: ["nodes"] - verbs: ["get"] -- apiGroups: ["apps"] - resources: ["deployments"] - verbs: ["*"] - ---- - -## Bind the cluster role granting access to ArangoDeploymentReplication resources -## to the default service account of the configured namespace. -apiVersion: rbac.authorization.k8s.io/v1beta1 -kind: RoleBinding -metadata: - name: arango-deployment-replications - namespace: default -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: ClusterRole - name: arango-deployment-replications -subjects: -- kind: ServiceAccount - name: default - namespace: default - ---- - -## Bind the cluster role granting access to all resources needed by -## the ArangoDeploymentReplication operator to the default service account -## the is being used to run the operator deployment. -apiVersion: rbac.authorization.k8s.io/v1beta1 -kind: ClusterRoleBinding -metadata: - name: arango-deployment-replication-operator-default -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: ClusterRole - name: arango-deployment-replication-operator -subjects: -- kind: ServiceAccount - name: default - namespace: default - ---- - -## deployment-replication/deployment-replication.yaml - -apiVersion: extensions/v1beta1 -kind: Deployment -metadata: - name: arango-deployment-replication-operator - namespace: default -spec: - replicas: 1 - strategy: - type: Recreate - template: - metadata: - labels: - name: arango-deployment-replication-operator - app: arango-deployment-replication-operator - spec: - containers: - - name: operator - imagePullPolicy: IfNotPresent - image: ewoutp/kube-arangodb@sha256:8480ab465a40a91b9dffeff0d3cf1534e1687b38ca7ad57b254d1deefdffd81f - args: - - --operator.deployment-replication - env: - - name: MY_POD_NAMESPACE - valueFrom: - fieldRef: - fieldPath: metadata.namespace - - name: MY_POD_NAME - valueFrom: - fieldRef: - fieldPath: metadata.name - - name: MY_POD_IP - valueFrom: - fieldRef: - fieldPath: status.podIP - ports: - - name: metrics - containerPort: 8528 - livenessProbe: - httpGet: - path: /health - port: 8528 - scheme: HTTPS - initialDelaySeconds: 5 - periodSeconds: 10 - readinessProbe: - httpGet: - path: /ready/deployment-replication - port: 8528 - scheme: HTTPS - initialDelaySeconds: 5 - periodSeconds: 10 -