From 14267f3bd1988862214f2d058168313ad305911a Mon Sep 17 00:00:00 2001 From: Ewout Prangsma Date: Thu, 31 May 2018 10:51:38 +0200 Subject: [PATCH 1/5] Updated specs wrt ArangoDeploymentReplication authentication & access packages --- .../DeploymentReplicationResource.md | 155 ++++++++++++++---- .../Kubernetes/DeploymentResource.md | 11 ++ 2 files changed, 137 insertions(+), 29 deletions(-) diff --git a/docs/Manual/Deployment/Kubernetes/DeploymentReplicationResource.md b/docs/Manual/Deployment/Kubernetes/DeploymentReplicationResource.md index 6e64f1784..e2ee79083 100644 --- a/docs/Manual/Deployment/Kubernetes/DeploymentReplicationResource.md +++ b/docs/Manual/Deployment/Kubernetes/DeploymentReplicationResource.md @@ -5,7 +5,7 @@ The ArangoDB Replication Operator creates and maintains ArangoDB This replication specification is a `CustomResource` following a `CustomResourceDefinition` created by the operator. -Example minimal replication definition: +Example minimal replication definition for 2 ArangoDB cluster with sync in the same Kubernetes cluster: ```yaml apiVersion: "replication.database.arangodb.com/v1alpha" @@ -15,17 +15,46 @@ metadata: spec: source: deploymentName: cluster-a + auth: + keyfileSecretName: cluster-a-sync-auth destination: deploymentName: cluster-b - auth: - clientAuthSecretName: client-auth-cert ``` This definition results in: - the arangosync `SyncMaster` in deployment `cluster-b` is called to configure a synchronization - from `cluster-a` to `cluster-b`, using the client authentication certificate stored in - `Secret` `client-auth-cert`. + from the syncmasters in `cluster-a` to the syncmasters in `cluster-b`, + using the client authentication certificate stored in `Secret` `cluster-a-sync-auth`. + To access `cluster-a`, the JWT secret found in the deployment of `cluster-a` is used. + To access `cluster-b`, the JWT secret found in the deployment of `cluster-b` is used. + +Example replication definition for replicating from a source that is outside the current Kubernetes cluster +to a destination that is in the same Kubernetes cluster: + +```yaml +apiVersion: "replication.database.arangodb.com/v1alpha" +kind: "ArangoDeploymentReplication" +metadata: + name: "replication-from-a-to-b" +spec: + source: + endpoint: ["https://163.172.149.229:31888", "https://51.15.225.110:31888", "https://51.15.229.133:31888"] + auth: + keyfileSecretName: cluster-a-sync-auth + tls: + caSecretName: cluster-a-sync-ca + destination: + deploymentName: cluster-b +``` + +This definition results in: + +- the arangosync `SyncMaster` in deployment `cluster-b` is called to configure a synchronization + from the syncmasters located at the given list of endpoint URL's to the syncmasters `cluster-b`, + using the client authentication certificate stored in `Secret` `cluster-a-sync-auth`. + To access `cluster-a`, the keyfile (containing a client authentication certificate) is used. + To access `cluster-b`, the JWT secret found in the deployment of `cluster-b` is used. ## Specification reference @@ -38,13 +67,7 @@ with sync enabled. This cluster configured as the replication source. -### `spec.source.deploymentNamespace: string` - -This setting specifies the Kubernetes namespace of an `ArangoDeployment` resource specified in `spec.source.deploymentName`. - -If this setting is empty, the namespace of the `ArangoDeploymentReplication` is used. - -### `spec.source.masterEndpoints: []string` +### `spec.source.endpoint: []string` This setting specifies zero or more master endpoint URL's of the source cluster. @@ -53,12 +76,23 @@ that is reachable from the Kubernetes cluster the `ArangoDeploymentReplication` Specifying this setting and `spec.source.deploymentName` at the same time is not allowed. -### `spec.source.auth.jwtSecretName: string` +### `spec.source.auth.keyfileSecretName: string` -This setting specifies the name of a `Secret` containing a JWT `token` used to authenticate +This setting specifies the name of a `Secret` containing a client authentication certificate called `tls.keyfile` used to authenticate with the SyncMaster at the specified source. -This setting is required, unless `spec.source.deploymentName` has been set. +If `spec.source.auth.userSecretName` has not been set, +the client authentication certificate found in the secret with this name is also used to configure +the synchronization and fetch the synchronization status. + +This setting is required. + +### `spec.source.auth.userSecretName: string` + +This setting specifies the name of a `Secret` containing a `username` & `password` used to authenticate +with the SyncMaster at the specified source in order to configure synchronization and fetch synchronization status. + +The user identified by the username must have write access in the `_system` database of the source ArangoDB cluster. ### `spec.source.tls.caSecretName: string` @@ -74,13 +108,7 @@ with sync enabled. This cluster configured as the replication destination. -### `spec.destination.deploymentNamespace: string` - -This setting specifies the Kubernetes namespace of an `ArangoDeployment` resource specified in `spec.destination.deploymentName`. - -If this setting is empty, the namespace of the `ArangoDeploymentReplication` is used. - -### `spec.destination.masterEndpoints: []string` +### `spec.destination.endpoint: []string` This setting specifies zero or more master endpoint URL's of the destination cluster. @@ -89,12 +117,27 @@ that is reachable from the Kubernetes cluster the `ArangoDeploymentReplication` Specifying this setting and `spec.destination.deploymentName` at the same time is not allowed. -### `spec.destination.auth.jwtSecretName: string` +### `spec.destination.auth.keyfileSecretName: string` -This setting specifies the name of a `Secret` containing a JWT `token` used to authenticate +This setting specifies the name of a `Secret` containing a client authentication certificate called `tls.keyfile` used to authenticate with the SyncMaster at the specified destination. -This setting is required, unless `spec.destination.deploymentName` has been set. +If `spec.destination.auth.userSecretName` has not been set, +the client authentication certificate found in the secret with this name is also used to configure +the synchronization and fetch the synchronization status. + +This setting is required, unless `spec.destination.deploymentName` or `spec.destination.auth.userSecretName` has been set. + +Specifying this setting and `spec.destination.userSecretName` at the same time is not allowed. + +### `spec.destination.auth.userSecretName: string` + +This setting specifies the name of a `Secret` containing a `username` & `password` used to authenticate +with the SyncMaster at the specified destination in order to configure synchronization and fetch synchronization status. + +The user identified by the username must have write access in the `_system` database of the destination ArangoDB cluster. + +Specifying this setting and `spec.destination.keyfileSecretName` at the same time is not allowed. ### `spec.destination.tls.caSecretName: string` @@ -103,8 +146,62 @@ the TLS connection created by the SyncMaster at the specified destination. This setting is required, unless `spec.destination.deploymentName` has been set. -### `spec.auth.clientAuthSecretName: string` +## Authentication details + +The authentication settings in a `ArangoDeploymentReplication` resource are used for two distinct purposes. + +The first use is the authentication of the syncmasters at the destination with the syncmasters at the source. +This is always done using a client authentication certificate which is found in a `tls.keyfile` field +in a secret identified by `spec.source.auth.keyfileSecretName`. + +The second use is the authentication of the ArangoDB Replication operator with the syncmasters at the source +or destination. These connections are made to configure synchronization, stop configuration and fetch the status +of the configuration. +The method used for this authentication is derived as follows (where `X` is either `source` or `destination`): + +- If `spec.X.userSecretName` is set, the username + password found in the `Secret` identified by this name is used. +- If `spec.X.keyfileSecretName` is set, the client authentication certificate (keyfile) found in the `Secret` identifier by this name is used. +- If `spec.X.deploymentName` is set, the JWT secret found in the deployment is used. + +## Creating client authentication certificate keyfiles + +The client authentication certificates needed for the `Secrets` identified by `spec.source.auth.keyfileSecretName` & `spec.destination.auth.keyfileSecretName` +are normal ArangoDB keyfiles that can be created by the `arangosync create client-auth keyfile` command. +In order to do so, you must have access to the client authentication CA of the source/destination. + +If the client authentication CA at the source/destination also contains a private key (`ca.key`), the ArangoDeployment operator +can be used to create such a keyfile for you, without the need to have `arangosync` installed locally. +Read the following paragraphs for instructions on how to do that. + +## Creating and using access packages + +An access package is a YAML file that contains: + +- A client authentication certificate, wrapped in a `Secret` in a `tls.keyfile` data field. +- A TLS certificate authority public key, wrapped in a `Secret` in a `ca.crt` data field. + +The format of the access package is such that it can be inserted into a Kubernetes cluster using the standard `kubectl` tool. + +To create an access package that can be used to authenticate with the ArangoDB SyncMasters of an `ArangoDeployment`, +add a name of a non-existing `Secret` to the `spec.sync.externalAccess.accessPackageSecretNames` field of the `ArangoDeployment`. +In response, a `Secret` is created in that Kubernetes cluster, with the given name, that contains a `accessPackage.yaml` data field +that contains a Kubernetes resource specification that can be inserted into the other Kubernetes cluster. + +The process for creating and using an access package to authentication at the source cluster is as follows: + +- Edit the `ArangoDeployment` resource of the source cluster, set `spec.sync.externalAccess.accessPackageSecretNames` to `["my-access-package"]` +- Wait for the `ArangoDeployment` operator to create a `Secret` named `my-access-package`. +- Extract the access package from the Kubernetes source cluster using: + +```bash +kubectl get secret my-access-package --template='{{index .data "accessPackage.yaml"}}' | base64 -D > accessPackage.yaml +``` + +- Insert the secrets found in the access package in the Kubernetes destination cluster using: + +```bash +kubectl apply -f accessPackage.yaml +``` -This setting specifies the name of a `Secret` containing a client authentication certificate, -used to authenticate the SyncMaster in the destination cluster with the SyncMaster in the -source cluster. +As a result, the destination Kubernetes cluster will have 2 additional `Secrets`. One containing a client authentication certificate +formatted ad a keyfile. Another containing the public key of the TLS CA certificate of the source cluster. diff --git a/docs/Manual/Deployment/Kubernetes/DeploymentResource.md b/docs/Manual/Deployment/Kubernetes/DeploymentResource.md index 27e1383ee..1e7713337 100644 --- a/docs/Manual/Deployment/Kubernetes/DeploymentResource.md +++ b/docs/Manual/Deployment/Kubernetes/DeploymentResource.md @@ -241,6 +241,17 @@ If not set, this setting defaults to: - If `spec.sync.externalAccess.loadBalancerIP` is set, it defaults to `https://:<8629>`. - Otherwise it defaults to `https://:<8629>`. +### `spec.sync.externalAccess.accessPackageSecretNames: []string` + +This setting specifies the names of zero of more `Secrets` that will be created by the deployment +operator containing "access packages". An access package contains those `Secrets` that are needed +to access the SyncMasters of this `ArangoDeployment`. + +By removing a name from this setting, the corresponding `Secret` is also deleted. + +See [the `ArangoDeploymentReplication` specification](./DeploymentReplicationResource.md) for more information +on access packages. + ### `spec.sync.auth.jwtSecretName: string` This setting specifies the name of a kubernetes `Secret` that contains From 2d62c6e5e94a07a7b081fde1f4b33abeb74f8161 Mon Sep 17 00:00:00 2001 From: Ewout Prangsma Date: Thu, 31 May 2018 11:43:05 +0200 Subject: [PATCH 2/5] Updated code to reflect spec changes --- .../arangosync/client/client_cache.go | 9 ++- .../v1alpha/sync_external_access_spec.go | 18 ++++- .../v1alpha/zz_generated.deepcopy.go | 5 ++ .../v1alpha/authentication_spec.go | 68 ------------------- .../v1alpha/endpoint_authentication_spec.go | 39 ++++++++--- pkg/apis/replication/v1alpha/endpoint_spec.go | 12 ++-- .../replication/v1alpha/replication_spec.go | 17 ++--- .../v1alpha/zz_generated.deepcopy.go | 43 ++++-------- pkg/replication/sync_client.go | 57 ++++++++++++---- pkg/util/constants/constants.go | 3 + pkg/util/k8sutil/secrets.go | 22 ++++++ 11 files changed, 149 insertions(+), 144 deletions(-) delete mode 100644 pkg/apis/replication/v1alpha/authentication_spec.go diff --git a/deps/github.com/arangodb/arangosync/client/client_cache.go b/deps/github.com/arangodb/arangosync/client/client_cache.go index d24fc57f3..c066925c5 100644 --- a/deps/github.com/arangodb/arangosync/client/client_cache.go +++ b/deps/github.com/arangodb/arangosync/client/client_cache.go @@ -85,7 +85,10 @@ func (cc *ClientCache) createClient(source Endpoint, auth Authentication, insecu return nil, maskAny(err) } ac := AuthenticationConfig{} - if auth.JWTSecret != "" { + if auth.Username != "" { + ac.UserName = auth.Username + ac.Password = auth.Password + } else if auth.JWTSecret != "" { ac.JWTSecret = auth.JWTSecret } else if auth.ClientToken != "" { ac.BearerToken = auth.ClientToken @@ -113,11 +116,13 @@ func NewAuthentication(tlsAuth TLSAuthentication, jwtSecret string) Authenticati type Authentication struct { TLSAuthentication JWTSecret string + Username string + Password string } // String returns a string used to unique identify the authentication settings. func (a Authentication) String() string { - return a.TLSAuthentication.String() + ":" + a.JWTSecret + return a.TLSAuthentication.String() + ":" + a.JWTSecret + ":" + a.Username + ":" + a.Password } // AuthProxy is a helper that implements github.com/arangodb-helper/go-certificates#TLSAuthentication. diff --git a/pkg/apis/deployment/v1alpha/sync_external_access_spec.go b/pkg/apis/deployment/v1alpha/sync_external_access_spec.go index 22ef15b80..e81662898 100644 --- a/pkg/apis/deployment/v1alpha/sync_external_access_spec.go +++ b/pkg/apis/deployment/v1alpha/sync_external_access_spec.go @@ -27,12 +27,15 @@ import ( "net" "net/url" "strconv" + + "github.com/arangodb/kube-arangodb/pkg/util/k8sutil" ) // SyncExternalAccessSpec holds configuration for the external access provided for the sync deployment. type SyncExternalAccessSpec struct { ExternalAccessSpec - MasterEndpoint []string `json:"masterEndpoint,omitempty"` + MasterEndpoint []string `json:"masterEndpoint,omitempty"` + AccessPackageSecretNames []string `json:accessPackageSecretNames,omitempty"` } // GetMasterEndpoint returns the value of masterEndpoint. @@ -40,6 +43,11 @@ func (s SyncExternalAccessSpec) GetMasterEndpoint() []string { return s.MasterEndpoint } +// GetAccessPackageSecretNames returns the value of accessPackageSecretNames. +func (s SyncExternalAccessSpec) GetAccessPackageSecretNames() []string { + return s.AccessPackageSecretNames +} + // ResolveMasterEndpoint returns the value of `--master.endpoint` option passed to arangosync. func (s SyncExternalAccessSpec) ResolveMasterEndpoint(syncServiceHostName string, syncServicePort int) []string { if len(s.MasterEndpoint) > 0 { @@ -61,6 +69,11 @@ func (s SyncExternalAccessSpec) Validate() error { return maskAny(fmt.Errorf("Failed to parse master endpoint '%s': %s", ep, err)) } } + for _, name := range s.AccessPackageSecretNames { + if err := k8sutil.ValidateResourceName(name); err != nil { + return maskAny(fmt.Errorf("Invalid name '%s' in accessPackageSecretNames: %s", name, err)) + } + } return nil } @@ -75,6 +88,9 @@ func (s *SyncExternalAccessSpec) SetDefaultsFrom(source SyncExternalAccessSpec) if s.MasterEndpoint == nil && source.MasterEndpoint != nil { s.MasterEndpoint = append([]string{}, source.MasterEndpoint...) } + if s.AccessPackageSecretNames == nil && source.AccessPackageSecretNames != nil { + s.AccessPackageSecretNames = append([]string{}, source.AccessPackageSecretNames...) + } } // ResetImmutableFields replaces all immutable fields in the given target with values from the source spec. diff --git a/pkg/apis/deployment/v1alpha/zz_generated.deepcopy.go b/pkg/apis/deployment/v1alpha/zz_generated.deepcopy.go index e0f332710..4523cafe0 100644 --- a/pkg/apis/deployment/v1alpha/zz_generated.deepcopy.go +++ b/pkg/apis/deployment/v1alpha/zz_generated.deepcopy.go @@ -653,6 +653,11 @@ func (in *SyncExternalAccessSpec) DeepCopyInto(out *SyncExternalAccessSpec) { *out = make([]string, len(*in)) copy(*out, *in) } + if in.AccessPackageSecretNames != nil { + in, out := &in.AccessPackageSecretNames, &out.AccessPackageSecretNames + *out = make([]string, len(*in)) + copy(*out, *in) + } return } diff --git a/pkg/apis/replication/v1alpha/authentication_spec.go b/pkg/apis/replication/v1alpha/authentication_spec.go deleted file mode 100644 index 3d85b8c77..000000000 --- a/pkg/apis/replication/v1alpha/authentication_spec.go +++ /dev/null @@ -1,68 +0,0 @@ -// -// 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/endpoint_authentication_spec.go b/pkg/apis/replication/v1alpha/endpoint_authentication_spec.go index 2dd352f22..f2c537ee6 100644 --- a/pkg/apis/replication/v1alpha/endpoint_authentication_spec.go +++ b/pkg/apis/replication/v1alpha/endpoint_authentication_spec.go @@ -31,23 +31,37 @@ import ( // 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"` + // KeyfileSecretName holds the name of a Secret containing a client authentication + // certificate formatted at keyfile in a `tls.keyfile` field. + KeyfileSecretName *string `json:"keyfileSecretName,omitempty"` + // UserSecretName holds the name of a Secret containing a `username` & `password` + // field used for basic authentication. + // The user identified by the username must have write access in the `_system` database + // of the ArangoDB cluster at the endpoint. + UserSecretName *string `json:"userSecretName,omitempty"` } -// GetJWTSecretName returns the value of jwtSecretName. -func (s EndpointAuthenticationSpec) GetJWTSecretName() string { - return util.StringOrDefault(s.JWTSecretName) +// GetKeyfileSecretName returns the value of keyfileSecretName. +func (s EndpointAuthenticationSpec) GetKeyfileSecretName() string { + return util.StringOrDefault(s.KeyfileSecretName) +} + +// GetUserSecretName returns the value of userSecretName. +func (s EndpointAuthenticationSpec) GetUserSecretName() string { + return util.StringOrDefault(s.UserSecretName) } // Validate the given spec, returning an error on validation // problems or nil if all ok. -func (s EndpointAuthenticationSpec) Validate(jwtSecretNameRequired bool) error { - if err := k8sutil.ValidateOptionalResourceName(s.GetJWTSecretName()); err != nil { +func (s EndpointAuthenticationSpec) Validate(keyfileSecretNameRequired bool) error { + if err := k8sutil.ValidateOptionalResourceName(s.GetKeyfileSecretName()); err != nil { return maskAny(err) } - if jwtSecretNameRequired && s.GetJWTSecretName() == "" { - return maskAny(errors.Wrapf(ValidationError, "Provide a jwtSecretName")) + if err := k8sutil.ValidateOptionalResourceName(s.GetUserSecretName()); err != nil { + return maskAny(err) + } + if keyfileSecretNameRequired && s.GetKeyfileSecretName() == "" { + return maskAny(errors.Wrapf(ValidationError, "Provide a keyfileSecretName")) } return nil } @@ -58,8 +72,11 @@ 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) + if s.KeyfileSecretName == nil { + s.KeyfileSecretName = util.NewStringOrNil(source.KeyfileSecretName) + } + if s.UserSecretName == nil { + s.UserSecretName = util.NewStringOrNil(source.UserSecretName) } } diff --git a/pkg/apis/replication/v1alpha/endpoint_spec.go b/pkg/apis/replication/v1alpha/endpoint_spec.go index 2a98354e0..7d2e51153 100644 --- a/pkg/apis/replication/v1alpha/endpoint_spec.go +++ b/pkg/apis/replication/v1alpha/endpoint_spec.go @@ -36,8 +36,8 @@ type EndpointSpec struct { // 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"` + // Endpoint holds a list of URLs used to reach the syncmaster(s). + Endpoint []string `json:"endpoint,omitempty"` // Authentication holds settings needed to authentication at the syncmaster. Authentication EndpointAuthenticationSpec `json:"auth"` // TLS holds settings needed to verify the TLS connection to the syncmaster. @@ -56,20 +56,20 @@ func (s EndpointSpec) HasDeploymentName() bool { // Validate the given spec, returning an error on validation // problems or nil if all ok. -func (s EndpointSpec) Validate() error { +func (s EndpointSpec) Validate(isSourceEndpoint bool) error { if err := k8sutil.ValidateOptionalResourceName(s.GetDeploymentName()); err != nil { return maskAny(err) } - for _, ep := range s.MasterEndpoint { + for _, ep := range s.Endpoint { if _, err := url.Parse(ep); err != nil { return maskAny(errors.Wrapf(ValidationError, "Invalid master endpoint '%s': %s", ep, err)) } } hasDeploymentName := s.HasDeploymentName() - if !hasDeploymentName && len(s.MasterEndpoint) == 0 { + if !hasDeploymentName && len(s.Endpoint) == 0 { return maskAny(errors.Wrapf(ValidationError, "Provide a deploy name or at least one master endpoint")) } - if err := s.Authentication.Validate(!hasDeploymentName); err != nil { + if err := s.Authentication.Validate(isSourceEndpoint || !hasDeploymentName); err != nil { return maskAny(err) } if err := s.TLS.Validate(!hasDeploymentName); err != nil { diff --git a/pkg/apis/replication/v1alpha/replication_spec.go b/pkg/apis/replication/v1alpha/replication_spec.go index d9abbd220..8d6fd403f 100644 --- a/pkg/apis/replication/v1alpha/replication_spec.go +++ b/pkg/apis/replication/v1alpha/replication_spec.go @@ -25,21 +25,17 @@ 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"` + Source EndpointSpec `json:"source"` + Destination EndpointSpec `json:"destination"` } // Validate the given spec, returning an error on validation // problems or nil if all ok. func (s DeploymentReplicationSpec) Validate() error { - if err := s.Source.Validate(); err != nil { + if err := s.Source.Validate(true); err != nil { return maskAny(err) } - if err := s.Destination.Validate(); err != nil { - return maskAny(err) - } - if err := s.Authentication.Validate(); err != nil { + if err := s.Destination.Validate(false); err != nil { return maskAny(err) } return nil @@ -49,14 +45,12 @@ func (s DeploymentReplicationSpec) Validate() error { 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. @@ -70,8 +64,5 @@ func (s DeploymentReplicationSpec) ResetImmutableFields(target *DeploymentReplic 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/zz_generated.deepcopy.go b/pkg/apis/replication/v1alpha/zz_generated.deepcopy.go index e73651076..5ee8f91f7 100644 --- a/pkg/apis/replication/v1alpha/zz_generated.deepcopy.go +++ b/pkg/apis/replication/v1alpha/zz_generated.deepcopy.go @@ -89,31 +89,6 @@ 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 @@ -137,7 +112,6 @@ func (in *DeploymentReplicationSpec) DeepCopyInto(out *DeploymentReplicationSpec *out = *in in.Source.DeepCopyInto(&out.Source) in.Destination.DeepCopyInto(&out.Destination) - in.Authentication.DeepCopyInto(&out.Authentication) return } @@ -177,8 +151,17 @@ func (in *DeploymentReplicationStatus) DeepCopy() *DeploymentReplicationStatus { // 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.KeyfileSecretName != nil { + in, out := &in.KeyfileSecretName, &out.KeyfileSecretName + if *in == nil { + *out = nil + } else { + *out = new(string) + **out = **in + } + } + if in.UserSecretName != nil { + in, out := &in.UserSecretName, &out.UserSecretName if *in == nil { *out = nil } else { @@ -211,8 +194,8 @@ func (in *EndpointSpec) DeepCopyInto(out *EndpointSpec) { **out = **in } } - if in.MasterEndpoint != nil { - in, out := &in.MasterEndpoint, &out.MasterEndpoint + if in.Endpoint != nil { + in, out := &in.Endpoint, &out.Endpoint *out = make([]string, len(*in)) copy(*out, *in) } diff --git a/pkg/replication/sync_client.go b/pkg/replication/sync_client.go index ef31ce64b..145496c4b 100644 --- a/pkg/replication/sync_client.go +++ b/pkg/replication/sync_client.go @@ -48,17 +48,38 @@ func (dr *DeploymentReplication) createSyncMasterClient(epSpec api.EndpointSpec) // Authentication insecureSkipVerify := true tlsAuth := tasks.TLSAuthentication{} - authJWTSecretName, tlsCASecretName, err := dr.getEndpointSecretNames(epSpec) + clientAuthKeyfileSecretName, userSecretName, authJWTSecretName, tlsCASecretName, err := dr.getEndpointSecretNames(epSpec) if err != nil { return nil, maskAny(err) } + username := "" + password := "" jwtSecret := "" - if authJWTSecretName != "" { + if userSecretName != "" { + var err error + username, password, err = k8sutil.GetBasicAuthSecret(dr.deps.KubeCli.CoreV1(), userSecretName, dr.apiObject.GetNamespace()) + if err != nil { + return nil, maskAny(err) + } + } else if authJWTSecretName != "" { var err error jwtSecret, err = k8sutil.GetTokenSecret(dr.deps.KubeCli.CoreV1(), authJWTSecretName, dr.apiObject.GetNamespace()) if err != nil { return nil, maskAny(err) } + } else if clientAuthKeyfileSecretName != "" { + keyFileContent, err := k8sutil.GetTLSKeyfileSecret(dr.deps.KubeCli.CoreV1(), clientAuthKeyfileSecretName, dr.apiObject.GetNamespace()) + if err != nil { + return nil, maskAny(err) + } + kf, err := certificates.NewKeyfile(keyFileContent) + if err != nil { + return nil, maskAny(err) + } + tlsAuth.TLSClientAuthentication = tasks.TLSClientAuthentication{ + ClientCertificate: kf.EncodeCertificates(), + ClientKey: kf.EncodePrivateKey(), + } } if tlsCASecretName != "" { caCert, err := k8sutil.GetCACertficateSecret(dr.deps.KubeCli.CoreV1(), tlsCASecretName, dr.apiObject.GetNamespace()) @@ -68,6 +89,8 @@ func (dr *DeploymentReplication) createSyncMasterClient(epSpec api.EndpointSpec) tlsAuth.CACertificate = caCert } auth := client.NewAuthentication(tlsAuth, jwtSecret) + auth.Username = username + auth.Password = password // Create client c, err := dr.clientCache.GetClient(log, source, auth, insecureSkipVerify) @@ -90,14 +113,20 @@ func (dr *DeploymentReplication) createArangoSyncEndpoint(epSpec api.EndpointSpe dnsName := k8sutil.CreateSyncMasterClientServiceDNSName(depl) return client.Endpoint{"https://" + net.JoinHostPort(dnsName, strconv.Itoa(k8sutil.ArangoSyncMasterPort))}, nil } - return client.Endpoint(epSpec.MasterEndpoint), nil + return client.Endpoint(epSpec.Endpoint), nil } // createArangoSyncTLSAuthentication creates the authentication needed to authenticate // the destination syncmaster at the source syncmaster. func (dr *DeploymentReplication) createArangoSyncTLSAuthentication(spec api.DeploymentReplicationSpec) (client.TLSAuthentication, error) { + // Fetch secret names of source + clientAuthKeyfileSecretName, _, _, tlsCASecretName, err := dr.getEndpointSecretNames(spec.Source) + if err != nil { + return client.TLSAuthentication{}, maskAny(err) + } + // Fetch keyfile - keyFileContent, err := k8sutil.GetTLSKeyfileSecret(dr.deps.KubeCli.CoreV1(), spec.Authentication.GetClientAuthSecretName(), dr.apiObject.GetNamespace()) + keyFileContent, err := k8sutil.GetTLSKeyfileSecret(dr.deps.KubeCli.CoreV1(), clientAuthKeyfileSecretName, dr.apiObject.GetNamespace()) if err != nil { return client.TLSAuthentication{}, maskAny(err) } @@ -107,10 +136,6 @@ func (dr *DeploymentReplication) createArangoSyncTLSAuthentication(spec api.Depl } // Fetch TLS CA certificate for source - _, 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) @@ -127,17 +152,23 @@ 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) { +// getEndpointSecretNames returns the names of secrets that hold the: +// - client authentication certificate keyfile, +// - user (basic auth) secret, +// - JWT secret name, +// - TLS ca.crt +func (dr *DeploymentReplication) getEndpointSecretNames(epSpec api.EndpointSpec) (clientAuthCertKeyfileSecretName, userSecretName, jwtSecretName, tlsCASecretName string, err error) { + clientAuthCertKeyfileSecretName = epSpec.Authentication.GetKeyfileSecretName() + userSecretName = epSpec.Authentication.GetUserSecretName() 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 "", "", "", "", maskAny(err) } - return depl.Spec.Sync.Authentication.GetJWTSecretName(), depl.Spec.Sync.TLS.GetCASecretName(), nil + return clientAuthCertKeyfileSecretName, userSecretName, depl.Spec.Sync.Authentication.GetJWTSecretName(), depl.Spec.Sync.TLS.GetCASecretName(), nil } - return epSpec.Authentication.GetJWTSecretName(), epSpec.TLS.GetCASecretName(), nil + return clientAuthCertKeyfileSecretName, userSecretName, "", epSpec.TLS.GetCASecretName(), nil } diff --git a/pkg/util/constants/constants.go b/pkg/util/constants/constants.go index 8307516b7..027d16df8 100644 --- a/pkg/util/constants/constants.go +++ b/pkg/util/constants/constants.go @@ -39,6 +39,9 @@ const ( SecretTLSKeyfile = "tls.keyfile" // Key in Secret.data used to store a PEM encoded TLS certificate in the format used by ArangoDB (`--ssl.keyfile`) + SecretUsername = "username" // Key in Secret.data used to store a username used for basic authentication + SecretPassword = "password" // Key in Secret.data used to store a password used for basic authentication + 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 diff --git a/pkg/util/k8sutil/secrets.go b/pkg/util/k8sutil/secrets.go index 6acf0e81c..d5283cac3 100644 --- a/pkg/util/k8sutil/secrets.go +++ b/pkg/util/k8sutil/secrets.go @@ -233,3 +233,25 @@ func CreateTokenSecret(cli corev1.CoreV1Interface, secretName, namespace, token } return nil } + +// GetBasicAuthSecret loads a secret with given name in the given namespace +// and extracts the `username` & `password` field. +// If the secret does not exists or one of the fields is missing, +// an error is returned. +// Returns: username, password, error +func GetBasicAuthSecret(cli corev1.CoreV1Interface, secretName, namespace string) (string, string, error) { + s, err := cli.Secrets(namespace).Get(secretName, metav1.GetOptions{}) + if err != nil { + return "", "", maskAny(err) + } + // Load `ca.crt` field + username, found := s.Data[constants.SecretUsername] + if !found { + return "", "", maskAny(fmt.Errorf("No '%s' found in secret '%s'", constants.SecretUsername, secretName)) + } + password, found := s.Data[constants.SecretPassword] + if !found { + return "", "", maskAny(fmt.Errorf("No '%s' found in secret '%s'", constants.SecretPassword, secretName)) + } + return string(username), string(password), nil +} From 9999e55fde60c929727b2caa46c7f41db3d3fa42 Mon Sep 17 00:00:00 2001 From: Ewout Prangsma Date: Thu, 31 May 2018 13:20:49 +0200 Subject: [PATCH 3/5] Added creation of sync access package --- pkg/deployment/access_package.go | 180 +++++++++++++++++++++++++ pkg/deployment/deployment_inspector.go | 6 + pkg/util/constants/constants.go | 2 + pkg/util/k8sutil/events.go | 10 ++ 4 files changed, 198 insertions(+) create mode 100644 pkg/deployment/access_package.go diff --git a/pkg/deployment/access_package.go b/pkg/deployment/access_package.go new file mode 100644 index 000000000..8e31e4d0b --- /dev/null +++ b/pkg/deployment/access_package.go @@ -0,0 +1,180 @@ +// +// 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 deployment + +import ( + "strings" + "time" + + certificates "github.com/arangodb-helper/go-certificates" + "github.com/ghodss/yaml" + "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/arangodb/kube-arangodb/pkg/util/constants" + "github.com/arangodb/kube-arangodb/pkg/util/k8sutil" +) + +const ( + clientAuthValidFor = time.Hour * 24 * 365 // 1yr + clientAuthCurve = "P256" +) + +// createAccessPackages creates a arangosync access packages specified +// in spec.sync.externalAccess.accessPackageSecretNames. +func (d *Deployment) createAccessPackages() error { + spec := d.apiObject.Spec + + if !spec.Sync.IsEnabled() { + // We're only relevant when sync is enabled + return nil + } + + for _, apSecretName := range spec.Sync.ExternalAccess.AccessPackageSecretNames { + if err := d.ensureAccessPackage(apSecretName); err != nil { + return maskAny(err) + } + } + + return nil +} + +// ensureAccessPackage creates an arangosync access package with given name +// it is does not already exist. +func (d *Deployment) ensureAccessPackage(apSecretName string) error { + log := d.deps.Log + ns := d.GetNamespace() + secrets := d.deps.KubeCli.CoreV1().Secrets(ns) + spec := d.apiObject.Spec + + if _, err := secrets.Get(apSecretName, metav1.GetOptions{}); err == nil { + // Secret already exists + return nil + } + + // Fetch client authentication CA + clientAuthSecretName := spec.Sync.Authentication.GetClientCASecretName() + clientAuthCert, clientAuthKey, err := k8sutil.GetCASecret(d.deps.KubeCli.CoreV1(), clientAuthSecretName, ns) + if err != nil { + log.Debug().Err(err).Msg("Failed to get client-auth CA secret") + return maskAny(err) + } + + // Fetch TLS CA public key + tlsCASecretName := spec.Sync.TLS.GetCASecretName() + tlsCACert, err := k8sutil.GetCACertficateSecret(d.deps.KubeCli.CoreV1(), tlsCASecretName, ns) + if err != nil { + log.Debug().Err(err).Msg("Failed to get TLS CA secret") + return maskAny(err) + } + + // Create keyfile + ca, err := certificates.LoadCAFromPEM(clientAuthCert, clientAuthKey) + if err != nil { + log.Debug().Err(err).Msg("Failed to parse client-auth CA") + return maskAny(err) + } + + // Create certificate + options := certificates.CreateCertificateOptions{ + ValidFor: clientAuthValidFor, + ECDSACurve: clientAuthCurve, + IsClientAuth: true, + } + cert, key, err := certificates.CreateCertificate(options, &ca) + if err != nil { + log.Debug().Err(err).Msg("Failed to create client-auth keyfile") + return maskAny(err) + } + keyfile := strings.TrimSpace(cert) + "\n" + strings.TrimSpace(key) + + // Create secrets (in memory) + keyfileSecret := v1.Secret{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "Secret", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: apSecretName + "-auth", + Labels: map[string]string{ + "remote-deployment": d.apiObject.GetName(), + }, + }, + Data: map[string][]byte{ + constants.SecretTLSKeyfile: []byte(keyfile), + }, + Type: "Opaque", + } + tlsCASecret := v1.Secret{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "Secret", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: apSecretName + "-ca", + Labels: map[string]string{ + "remote-deployment": d.apiObject.GetName(), + }, + }, + Data: map[string][]byte{ + constants.SecretCACertificate: []byte(tlsCACert), + }, + Type: "Opaque", + } + + // Serialize secrets + keyfileYaml, err := yaml.Marshal(keyfileSecret) + if err != nil { + log.Debug().Err(err).Msg("Failed to encode client-auth keyfile Secret") + return maskAny(err) + } + tlsCAYaml, err := yaml.Marshal(tlsCASecret) + if err != nil { + log.Debug().Err(err).Msg("Failed to encode TLS CA Secret") + return maskAny(err) + } + allYaml := strings.TrimSpace(string(keyfileYaml)) + "\n---\n" + strings.TrimSpace(string(tlsCAYaml)) + + // Create secret containing access package + secret := &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: apSecretName, + }, + Data: map[string][]byte{ + constants.SecretAccessPackageYaml: []byte(allYaml), + }, + } + // Attach secret to owner + secret.SetOwnerReferences(append(secret.GetOwnerReferences(), d.apiObject.AsOwner())) + if _, err := secrets.Create(secret); err != nil { + // Failed to create secret + log.Debug().Err(err).Str("secret-name", apSecretName).Msg("Failed to create access package Secret") + return maskAny(err) + } + + // Write log entry & create event + log.Info().Str("secret-name", apSecretName).Msg("Created access package Secret") + d.CreateEvent(k8sutil.NewAccessPackageCreatedEvent(d.apiObject, apSecretName)) + + return nil +} diff --git a/pkg/deployment/deployment_inspector.go b/pkg/deployment/deployment_inspector.go index d7a1c1403..2e83edb25 100644 --- a/pkg/deployment/deployment_inspector.go +++ b/pkg/deployment/deployment_inspector.go @@ -129,6 +129,12 @@ func (d *Deployment) inspectDeployment(lastInterval time.Duration) time.Duration d.CreateEvent(k8sutil.NewErrorEvent("Pod creation failed", err, d.apiObject)) } + // Create access packages + if err := d.createAccessPackages(); err != nil { + hasError = true + d.CreateEvent(k8sutil.NewErrorEvent("AccessPackage creation failed", err, d.apiObject)) + } + // At the end of the inspect, we cleanup terminated pods. if d.resources.CleanupTerminatedPods(); err != nil { hasError = true diff --git a/pkg/util/constants/constants.go b/pkg/util/constants/constants.go index 027d16df8..8c43f6d79 100644 --- a/pkg/util/constants/constants.go +++ b/pkg/util/constants/constants.go @@ -42,6 +42,8 @@ const ( SecretUsername = "username" // Key in Secret.data used to store a username used for basic authentication SecretPassword = "password" // Key in Secret.data used to store a password used for basic authentication + SecretAccessPackageYaml = "accessPackage.yaml" // Key in Secret.data used to store a YAML encoded access package + 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 diff --git a/pkg/util/k8sutil/events.go b/pkg/util/k8sutil/events.go index c0210add8..571cd7c80 100644 --- a/pkg/util/k8sutil/events.go +++ b/pkg/util/k8sutil/events.go @@ -125,6 +125,16 @@ func NewSecretsRestoredEvent(apiObject APIObject) *v1.Event { return event } +// NewAccessPackageCreatedEvent creates an event indicating that a secret containing an access package +// has been created. +func NewAccessPackageCreatedEvent(apiObject APIObject, apSecretName string) *v1.Event { + event := newDeploymentEvent(apiObject) + event.Type = v1.EventTypeNormal + event.Reason = "Access package created" + event.Message = fmt.Sprintf("An access package named %s has been created", apSecretName) + return event +} + // NewErrorEvent creates an even of type error. func NewErrorEvent(reason string, err error, apiObject APIObject) *v1.Event { event := newDeploymentEvent(apiObject) From 665d984854239d416a409f5ddc7c4dae990542d5 Mon Sep 17 00:00:00 2001 From: Ewout Prangsma Date: Thu, 31 May 2018 13:50:22 +0200 Subject: [PATCH 4/5] Added cleanup of obsolete access packages --- .../Kubernetes/DeploymentResource.md | 2 + pkg/deployment/access_package.go | 42 +++++++++++++++++-- pkg/util/k8sutil/events.go | 10 +++++ 3 files changed, 50 insertions(+), 4 deletions(-) diff --git a/docs/Manual/Deployment/Kubernetes/DeploymentResource.md b/docs/Manual/Deployment/Kubernetes/DeploymentResource.md index 1e7713337..0d7fb55c6 100644 --- a/docs/Manual/Deployment/Kubernetes/DeploymentResource.md +++ b/docs/Manual/Deployment/Kubernetes/DeploymentResource.md @@ -248,6 +248,8 @@ operator containing "access packages". An access package contains those `Secrets to access the SyncMasters of this `ArangoDeployment`. By removing a name from this setting, the corresponding `Secret` is also deleted. +Note that to remove all access packages, leave an empty array in place (`[]`). +Completely removing the setting results in not modifying the list. See [the `ArangoDeploymentReplication` specification](./DeploymentReplicationResource.md) for more information on access packages. diff --git a/pkg/deployment/access_package.go b/pkg/deployment/access_package.go index 8e31e4d0b..499a7bdd9 100644 --- a/pkg/deployment/access_package.go +++ b/pkg/deployment/access_package.go @@ -36,26 +36,60 @@ import ( ) const ( - clientAuthValidFor = time.Hour * 24 * 365 // 1yr - clientAuthCurve = "P256" + clientAuthValidFor = time.Hour * 24 * 365 // 1yr + clientAuthCurve = "P256" + labelKeyOriginalDeployment = "original-deployment-name" ) // createAccessPackages creates a arangosync access packages specified // in spec.sync.externalAccess.accessPackageSecretNames. func (d *Deployment) createAccessPackages() error { + log := d.deps.Log spec := d.apiObject.Spec + secrets := d.deps.KubeCli.CoreV1().Secrets(d.GetNamespace()) if !spec.Sync.IsEnabled() { // We're only relevant when sync is enabled return nil } + // Create all access packages that we're asked to build + apNameMap := make(map[string]struct{}) for _, apSecretName := range spec.Sync.ExternalAccess.AccessPackageSecretNames { + apNameMap[apSecretName] = struct{}{} if err := d.ensureAccessPackage(apSecretName); err != nil { return maskAny(err) } } + // Remove all access packages that we did build, but are no longer needed + secretList, err := secrets.List(metav1.ListOptions{}) + if err != nil { + log.Debug().Err(err).Msg("Failed to list secrets") + return maskAny(err) + } + for _, secret := range secretList.Items { + if d.isOwnerOf(&secret) { + if _, found := secret.Data[constants.SecretAccessPackageYaml]; found { + // Secret is an access package + if _, wanted := apNameMap[secret.GetName()]; !wanted { + // We found an obsolete access package secret. Remove it. + if err := secrets.Delete(secret.GetName(), &metav1.DeleteOptions{ + Preconditions: &metav1.Preconditions{UID: &secret.UID}, + }); err != nil && !k8sutil.IsNotFound(err) { + // Not serious enough to stop everything now, just log and create an event + log.Warn().Err(err).Msg("Failed to remove obsolete access package secret") + d.CreateEvent(k8sutil.NewErrorEvent("Access Package cleanup failed", err, d.apiObject)) + } else { + // Access package removed, notify user + log.Info().Str("secret-name", secret.GetName()).Msg("Removed access package Secret") + d.CreateEvent(k8sutil.NewAccessPackageDeletedEvent(d.apiObject, secret.GetName())) + } + } + } + } + } + return nil } @@ -117,7 +151,7 @@ func (d *Deployment) ensureAccessPackage(apSecretName string) error { ObjectMeta: metav1.ObjectMeta{ Name: apSecretName + "-auth", Labels: map[string]string{ - "remote-deployment": d.apiObject.GetName(), + labelKeyOriginalDeployment: d.apiObject.GetName(), }, }, Data: map[string][]byte{ @@ -133,7 +167,7 @@ func (d *Deployment) ensureAccessPackage(apSecretName string) error { ObjectMeta: metav1.ObjectMeta{ Name: apSecretName + "-ca", Labels: map[string]string{ - "remote-deployment": d.apiObject.GetName(), + labelKeyOriginalDeployment: d.apiObject.GetName(), }, }, Data: map[string][]byte{ diff --git a/pkg/util/k8sutil/events.go b/pkg/util/k8sutil/events.go index 571cd7c80..93fae7e83 100644 --- a/pkg/util/k8sutil/events.go +++ b/pkg/util/k8sutil/events.go @@ -135,6 +135,16 @@ func NewAccessPackageCreatedEvent(apiObject APIObject, apSecretName string) *v1. return event } +// NewAccessPackageDeletedEvent creates an event indicating that a secret containing an access package +// has been deleted. +func NewAccessPackageDeletedEvent(apiObject APIObject, apSecretName string) *v1.Event { + event := newDeploymentEvent(apiObject) + event.Type = v1.EventTypeNormal + event.Reason = "Access package deleted" + event.Message = fmt.Sprintf("An access package named %s has been deleted", apSecretName) + return event +} + // NewErrorEvent creates an even of type error. func NewErrorEvent(reason string, err error, apiObject APIObject) *v1.Event { event := newDeploymentEvent(apiObject) From 2745c9fe1eb5587e7b939d906477a9c366aabe81 Mon Sep 17 00:00:00 2001 From: Ewout Prangsma Date: Tue, 5 Jun 2018 16:46:19 +0200 Subject: [PATCH 5/5] Text changes --- .../Deployment/Kubernetes/DeploymentReplicationResource.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/Manual/Deployment/Kubernetes/DeploymentReplicationResource.md b/docs/Manual/Deployment/Kubernetes/DeploymentReplicationResource.md index e2ee79083..a6c61ef2e 100644 --- a/docs/Manual/Deployment/Kubernetes/DeploymentReplicationResource.md +++ b/docs/Manual/Deployment/Kubernetes/DeploymentReplicationResource.md @@ -187,7 +187,7 @@ add a name of a non-existing `Secret` to the `spec.sync.externalAccess.accessPac In response, a `Secret` is created in that Kubernetes cluster, with the given name, that contains a `accessPackage.yaml` data field that contains a Kubernetes resource specification that can be inserted into the other Kubernetes cluster. -The process for creating and using an access package to authentication at the source cluster is as follows: +The process for creating and using an access package for authentication at the source cluster is as follows: - Edit the `ArangoDeployment` resource of the source cluster, set `spec.sync.externalAccess.accessPackageSecretNames` to `["my-access-package"]` - Wait for the `ArangoDeployment` operator to create a `Secret` named `my-access-package`. @@ -203,5 +203,5 @@ kubectl get secret my-access-package --template='{{index .data "accessPackage.ya kubectl apply -f accessPackage.yaml ``` -As a result, the destination Kubernetes cluster will have 2 additional `Secrets`. One containing a client authentication certificate -formatted ad a keyfile. Another containing the public key of the TLS CA certificate of the source cluster. +As a result, the destination Kubernetes cluster will have 2 additional `Secrets`. One contains a client authentication certificate +formatted as a keyfile. Another contains the public key of the TLS CA certificate of the source cluster.